Skip to content

Advanced

Advanced topics

Taipy exposes advanced features that the Plotly library provides. This section demonstrates some of those features, when they are needed and how to use them in your Taipy application.

Multiple traces with different dataset sizes

The Adding a trace example explained how to create multiple traces on the same chart.
However, a strong limitation was that all series have to have the same size.
This limitation is due to the technical nature of a Pandas DataFrame, which must have all its columns holding the same number of items.

To allow for plotting series of different sizes, we set the data property of the chart control to an array of dictionaries (converted to Pandas DataFrames internally): each dictionary that holds different series that must all be the same size to be used in the chart control, but different dictionaries in the data array may have different sizes.
The data column names must then be prefixed by their index in the data array (starting at index 0) followed by a slash: "1/value" indicates the key (or the column) called "value" in the second element of the array set to the data property of the chart control.

Here is an example that demonstrates this. We want to plot two different data sets that represent the same function (x squared) on two different intervals.

  • The first trace is very coarse, with a data point at every other x unit between -10 and 10.
  • The second trace is far more detailed, with ten data points between each x unit, on the interval [-4, 4].

Here is the code that does just that:

# The first data set uses the x interval [-10..10],
# with one point at every other unit
x1_range = [x*2 for x in range(-5, 6)]

# The second data set uses the x interval [-4..4],
# with ten point between every unit
x2_range = [x/10 for x in range(-40, 41)]

# Definition of the two data sets
data = [
    # Coarse data set
    {
        "x": x1_range,
        "Coarse": [x*x for x in x1_range]
    },
    # Fine data set
    {
        "x": x2_range,
        "Fine": [x*x for x in x2_range]
    }
]

As you can see, the array data has two elements:

  • At index 0: A dictionary that has two properties: "x" is set to the array [-10, -8, ..., 8, 10], which is the x range for the coarse plot, and "y" is the series of all these values, squared.
    Both these arrays hold 11 elements.
  • At index 1: A dictionary that has two properties: "x" is set to the array [-4.0, -3.9, ..., 3.9, 4.0], which is the x range for the fine plot, and "y" is the series of all these values, squared, just like in the first element.
    Both these arrays hold 81 elements.

To use these series in the chart control, the x and y properties must indicate which trace they refer to and be set with the indexed data set syntax as shown below:

Definition

<|{data}|chart|x[1]=0/x|y[1]=0/Coarse|x[2]=1/x|y[2]=1/Fine|>
<taipy:chart x[1]="0/x" y[1]="0/Coarse" x[2]="1/x" y[2]="1/Fine">{data}</taipy:chart>
import taipy.gui.builder as tgb
...
tgb.chart("{data}", x__1="0/x", y__1="0/Coarse", x__2="1/x", y__2="1/Fine")

Trace number 1 (the Coarse trace) is defined by x[1] and y[1], and uses the series data[0]["x"] and data[0]["Coarse"];
Trace number 2 (the Fine trace) is defined by x[2] and y[2], and uses the series data[1]["x"] and data[1]["Fine"].

Here is what the resulting plot looks like:

Unbalanced data sets

Large datasets

When binding a chart component to a large dataset, performance becomes a critical consideration. Large datasets can consume significant system resources, resulting in slower rendering times, reduced responsiveness, and overall degraded performance, especially in interactive applications.
These issues can negatively impact the user experience, particularly when users need to interact with or manipulate the chart.

Consider a scenario where an application needs to visualize large datasets, allowing users to trigger calculations and make decisions based on the data.
Below is a basic example of a large dataset represented in Python:

x_values = ...
Y_values = ...
data = pd.DataFrame({"X": x_values, "Y": y_values})

In this example, x_values could be a sequence of 50,000 integers, while y_values is generated from a noisy log-sine function, also containing 50,000 samples.

To represent this data using Taipy GUI, you can define a chart control as follows:

Definition

<|{data}|chart|type=markers|x=X|y=Y|>
<taipy:chart type="markers" x="X" y="Y">{data}</taipy:chart>
import taipy.gui.builder as tgb
...
tgb.chart("{data}", type="markers", x="X", y="Y")

The initial chart display with 50,000 data points will look like this:

Initial dataset

As seen in the chart, with 50,000 data points, it becomes difficult to interpret the information due to over-plotting. Furthermore, rendering such a large dataset takes significant time, and the entire dataset must be transmitted to the frontend, further affecting performance.

To improve both application performance and chart readability, it is necessary to reduce the number of data points rendered. This can be achieved through techniques such as downsampling, aggregation, or filtering, which can limit the volume of data without losing critical insights

Solution 1: Linear interpolation

One approach to handle large datasets is to apply linear interpolation. This technique reduces the number of data points by approximating the values between points along a straight line, effectively downsampling the dataset.

Using Python and the NumPy package, this solution can be implemented easily and efficiently:

1
2
3
x_values = x_values[::100]
y_values = y_values.reshape(-1, 100)
y_values = np.mean(y_values, axis=1)

  • line 1: The original x_values array is reduced by selecting one value for every 100 points.
  • line 2: The y_values array is reshaped, grouping every 100 consecutive data points.
  • line 3: The mean of each group of 100 points is calculated, resulting in a smaller dataset.

This results in a dataset with 100 times fewer points, while still preserving the overall shape of the original data.

Here is the updated chart using the downsampled (interpolated) dataset:

Downsampled dataset

While linear interpolation significantly reduces the dataset size and improves chart performance, it has some limitations. Specifically, it smooths out the data, which can obscure important details, such as sharp changes or high-frequency fluctuations.
With this technique, the noise present in the original sine function has been smoothed away, which might not be desirable for data analysts or scientists who need to observe finer data characteristics.

Solution 2: Sub-sampling

Sub-sampling is a simple technique where a representative subset of the original data is selected, for example, by picking every 100th data point. This directly reduces the number of points, enhancing performance.

With NumPy and Python's slicing syntax, sub-sampling is straightforward:

x_values = x_values[::100]
y_values = y_values[::100]
Both x_values and y_values are reduced by selecting every 100th element from the original arrays.

As a result, the dataset data is reduced to 1/100th of its original size, while still retaining the overall shape of the data.

Here is the chart with the sub-sampled dataset:

Sub-sampled dataset

However, sub-sampling has its limitations. It may skip over significant trends or abrupt changes in the data, especially if key points are not selected. While it's a quick and efficient solution, it may result in the loss of critical details in datasets with high-frequency variations or sudden transitions.
As you can see, only a very few number of noisy data points remain after the sampling, which is expected.

Solution 3: Decimation

Decimation is a more refined approach to reducing dataset size while preserving essential information and trends. By selectively removing data points based on specific criteria, such as frequency content or statistical significance, decimation balances performance with data integrity.

The chart control has a decimator property that accepts an instance of a subclass of Decimator. This class transforms the dataset declared in the data property, reducing the number of points to be displayed while ensuring that key data features are retained.

To use decimation, you must instantiate a decimator:

decimator = MinMaxDecimator(200)

In this case, the decimator limits the displayed points to 200, which is an extreme reduction, but it highlights the effect. For practical usage, typical values range from 1000 to 2000, depending on the horizontal resolution of the screen: it's often unnecessary to render more points than a monitor can display.

Several decimator types are available, including MinMaxDecimator, LTTB, RDP, and ScatterDecimator. Each of these implements different algorithms, better suited for specific shapes of data.

To apply the decimator to the chart, set the decimator variable to the decimator property:

Definition

<|{data}|chart|type=markers|x=X|y=Y|decimator={decimator}|>
<taipy:chart type="markers" x="X" y="Y" decimator="{decimator}">{data}</taipy:chart>
import taipy.gui.builder as tgb
...
tgb.chart("{data}", type="markers", x="X", y="Y", decimator="{decimator}")

Here is the resulting chart after applying decimation:

Decimation applied

The chart retains more of the dataset's characteristics compared to simpler methods like sub-sampling or linear interpolation. The global sine-like shape is preserved, and the noise remains visible.
At the same time, performance is greatly improved: only 200 points are rendered instead of 50,000, a reduction by a factor of 250.

Note that decimation does not alter nor duplicate the original dataset.
You can zoom in and out using the chart's built-in zoom tool to reveal or hide more details dynamically.

For example, the following image shows the chart zoomed in on a specific area:

Selecting an area to zoom in

And here is the result after zooming in:

Chart after zoom

As you zoom in, more details are revealed, while still adhering to the 200-point limit, ensuring smooth performance and responsive interactions.

Adding annotations

You can add text annotations on top of a chart using the annotations property of the dictionary set to the layout property of the chart control.
These annotations can be connected to an arrow that points to an exact location in the chart coordinates system, and have many customization capabilities. You may look at the documentation on the annotations documentation page on Plotly's website.

Here is an example demonstrating how you can add annotations to your chart controls:

# Function to plot: x^3/3 - x
def f(x):
    return x*x*x/3-x

# x values: [-2.2, ..., 2.2]
x = [(x-10)/4.5 for x in range(0, 21)]

data = {
    "x": x,
    # y: [f(-2.2), ..., f(2.2)]
    "y": [f(x) for x in x]
}

layout = {
    # Chart title
    "title": "Local extrema",
    "annotations": [
        # Annotation for local maximum (x = -1)
        {
            "text": "Local <b>max</b>",
            "font": {
                "size": 20
            },
            "x": -1,
            "y": f(-1)
        },
        # Annotation for local minimum (x = 1)
        {
            "text": "Local <b>min</b>",
            "font": {
                "size": 20
            },
            "x": 1,
            "y": f(1),
            "xanchor": "left"
        }
    ]
}

The important part is the definition of the layout object, the rest of the code being very standard.
You can see how we create a dictionary for every annotation we want to add, including the text information, location, font setting, etc.

Adding the annotations to the chart control is a matter of setting the layout property of the chart control to that object:

Definition

<|{data}|chart|layout={layout}|>
<taipy:chart layout="{layout}">{data}</taipy:chart>
import taipy.gui.builder as tgb
...
tgb.chart("{data}", layout="{layout}")

Here is how the annotated chart control shows on the page:

Text annotations

Adding shapes

Shapes can be rendered on top of a chart at defined locations.
To create shapes, you must add shape descriptors to the shapes property of the dictionary set to the layout property of the chart control.
The documentation for shape descriptors can be found in the shapes documentation page on Plotly's website.

Here is some code that creates shapes on top of a chart control:

# Function to plot: x^3/3-x
def f(x):
    return x*x*x/3-x

# x values: [-2.2, ..., 2.2]
x = [(x-10)/4.5 for x in range(0, 21)]

data = {
    "x": x,
    # y: [f(-2.2), ..., f(2.2)]
    "y": [f(x) for x in x]
}

shape_size = 0.1

layout = {
    "shapes": [
        # Shape for local maximum (x = -1)
        {
            "x0": -1-shape_size,
            "y0": f(-1)-2*shape_size,
            "x1": -1+shape_size,
            "y1": f(-1)+2*shape_size,
            "fillcolor": "green",
            "opacity": 0.5
        },
        # Shape for local minimum (x = 1)
        {
            "x0": 1-shape_size,
            "y0": f(1)-2*shape_size,
            "x1": 1+shape_size,
            "y1": f(1)+2*shape_size,
            "fillcolor": "red",
            "opacity": 0.5
        }
    ]
}

The part you should be looking at is the setting of the property shapes in the layout dictionary: each shape is defined as a dictionary that holds all the parameters that configure how and where to render it.
Note how we indicate the size of the shape, using the shape_size variable.

Here is the definition of the chart control:

Definition

<|{data}|chart|layout={layout}|>
<taipy:chart layout="{layout}">{data}</taipy:chart>
import taipy.gui.builder as tgb
...
tgb.chart("{data}", layout="{layout}")

And here is what the resulting chart looks like:

Shapes on chart

Tracking selection

The chart control has the selected property that can be used to track data point selection in the chart control.
This property is dynamic and indexed; the bound variable holds an array of point indexes. It is updated when the user selects points on the chart.

Here is some code that calculates the mean of the values of the selected points of a chart control:

# x = [0..20]
x = list(range(0, 21))

data = {
    "x": x,
    # A list of random values within [1, 10]
    "y": [random.uniform(1, 10) for _ in x]
}

layout = {
    # Force the Box select tool
    "dragmode": "select",
    # Remove all margins around the plot
    "margin": {"l":0,"r":0,"b":0,"t":0}
}

config = {
    # Hide Plotly's mode bar
    "displayModeBar": False
}

selected_indices = []

mean_value = 0.

def on_change(state, var, val):
    if var == "selected_indices":
        state.mean_value = numpy.mean([data["y"][idx] for idx in val]) if len(val) else 0

The part you should be looking at is the use of the default callback property on_change: it detects the changes in selected_indices and calculates the mean, which updates the text in the title.

We also create two objects (layout and config) used to remove the mode bar above the chart and force the Box select tool.

Here is the definition of the chart and text controls:

Definition

## Mean of <|{len(selected_indices)}|raw|> selected points: <|{mean_value}|format=%.2f|raw|>

<|{data}|chart|selected={selected_indices}|layout={layout}|plot_config={config}|>
<h2>Mean of
  <taipy:text raw="true">{len(selected_indices)}</taipy:text>
  selected points:
  <taipy:text format="%.2f" raw="true">{mean_value}</taipy:text>
  </h2>

<taipy:chart selected="{selected_indices}" layout="{layout}"
    plot_config="{config}">{data}</taipy:chart>
import taipy.gui.builder as tgb
...
tgb.chart("{data}", selected="{selected_indices}", layout="{layout}", plot_config="{config}")

And here is what the resulting page looks like:

Data points selection

Using the Ploty Python library

The chart control of Taipy GUI lets you create graphs that can be more or less complex using the control's properties. There are situations, though, when it would be more straightforward to code the chart using the Plotly Open Source Graphing Library for Python or even the high-level Ploty Express API.

An obvious use case where you would want to do that is when you were able to find a code sample based on Plotly Python that looks very similar to what you want to achieve with Taipy GUI, and you don't want to rewrite the sample using the chart's properties. It would be really easier if a copy-paste of the sample code suffice.
The figure property is available to do precisely that: create the figure (an instance of plotly.graph_objects.Figure) and set it as the figure property value. The resulting chart will be exactly what you expect.

Note that thanks for figure being a dynamic property, you can use Python to generate a new graph and have your page update automatically.

Here is an example of injecting a figure defined using the Plotly Python API in a Taipy GUI page.
We want to create three (violin plots)[https://plotly.com/python/violin/] representing three different probability distributions available in the (Numpy)[https://numpy.org/] package. A single figure will be created, to which we will add three traces: one for each probability distribution we want to represent.

Here is the code that does just that:

import numpy as np
import plotly.graph_objects as go

from taipy.gui import Gui

# Create the Plotly figure object
figure = go.Figure()

# Add trace for Normal Distribution
figure.add_trace(go.Violin(name="Normal",
                           y=np.random.normal(loc=0, scale=1, size=1000),
                           box_visible=True, meanline_visible=True))
# Add trace for Exponential Distribution
figure.add_trace(go.Violin(name="Exponential",
                           y=np.random.exponential(scale=1, size=1000),
                           box_visible=True, meanline_visible=True))
# Add trace for Uniform Distribution
figure.add_trace(go.Violin(name="Uniform",
                           y=np.random.uniform(low=0, high=1, size=1000),
                           box_visible=True, meanline_visible=True))

# Updating layout for better visualization
figure.update_layout(title="Different Probability Distributions")

To have the chart control directly use this Plotly figure, all you must do is set the figure property to the object you have just created:

Definition

<|chart|figure={figure}|>
<taipy:chart figure="{figure}"/>
import taipy.gui.builder as tgb
...
tgb.chart(figure="{figure}")

When the page is displayed, the Plotly magic operates:

Plotly Python figure