Plotly

This widget can be used to generate graphs. It is build based on plotly.js, which is a high-level declarative charting library. Plotly.js ships with over 40 chart types.

Refer to the Plotly documentation here for details on how to use any of the available chart types in WebStudio.

This widget is data driven. The data structure should conform to the Plotly documentation provided at the beginning of each different type of chart’s section.

Model

{
    "type": "plotly",
    "data": [],          // See Plotly documentation
    "plotlyOptions" : {}
}

Plotly Options

This section is used to apply plotly specific configuration and layout settings.

{
    "plotlyOptions" : {
        "config" : {},
        "layout" : {},
        "layoutByTheme" : { // Light and dark theme layout
            "mergeMode" : "deep",
            "light" : {},
            "dark" : {}
        }
    }
}
Field Description

config

The configuration options determine how users can interact with the chart.

{
    "plotlyOptions": {
        "config": {
            "scrollZoom": true,     // enable scroll and zoom
            "editable": true,       // tiles and legend names are editable
            "staticPlot": true,     // disable all interactions
            "displayModeBar": true, // either always or never display mode bar
            "displaylogo": true     // disable or enable Plotly logo on mode bar
        }
    }
}

Setting the staticPlot property to true overrides the ones which enable interaction such as editable and scrollZoom.

layout

Use this property to manipulate the appearance of the plotly chart. Refer to the layout documentation for details.

Some of the commonly set options are:

  • Paper Background Color

  • Plot Background

  • Show Legend

  • Title (font)

  • X-axis (title, tickangle, gridwidth)

  • Y-axis (title, tickangle, gridwidth)

Example:

{
    "plotlyOptions": {
        "layout": {
            "paper_bgcolor": "steelblue",
            "plot_bgcolor": "steelblue",
            "showlegend": false,
            "title": "Click Here<br>to Edit Chart Title",
            "font":{
                "family": "Raleway, sans-serif"
                },
            "xaxis": {
                "tickangle": -45,
                "gridwidth": 2,
                "title": {
                    "text" : "normalized moisture"
                    },
                },
            "yaxis": {
                "tickangle": -45,
                "gridwidth": 2,
                "title": {
                    "text": "normalized pressure"
                }
            }
        }
    }
}

The layout configuration, which is optional, can also be used to define temples, allowing many static settings such line and fill colors, names etc. that would otherwise reside in the data section, to be set independently from the plot data. This in turn helps with apply data retrieved from the core and limits the amount of model config needed in the widget dataSource action pipeline (where ideally there would be none).

layoutByTheme

Used to configure theme specific layout properties, allowing the plotly defaults which work best in "light" mode, to be overridden to match the active display theme.

light
dark

The light and dark elements are both optional. When present they override properties already declared in the layout section or define them when not. In essence the layout and theme-specific light or dark properties are merged according to the selected mergeMode. described below.

Due to the fact that the default settings for plotly graphs work reasonably well out of the box with the light theme, it is common to define only the dark mode options.

{
    "layoutByTheme": {
        "dark": {
            "font": {
                "color": "#d5d5d6"
            },
            "plot_bgcolor": "#252526",
            "paper_bgcolor": "#212123"
        }
    }
}

mergeMode

In order to arrive at the effective layout to apply to the chart, the layout and layoutByTheme properties need to be merged.

As a general rule, theme specific settings are used in preference to the theme agnostic ones.

The following mergeMode options, each with their own pros and cons, are available:

  • shallow: Starting with the layout settings, add/overwrite all properties declared at root level in the theme specific property. The example below shows the outcome of a shallow merge:

    {
        "plotlyOptions": {
            "layout": {
                "paper_bgcolor": "white",
                "margin": {
                    "l": 40,
                    "r": 40,
                    "t": 10,
                    "b": 40
                },
                "font": {
                    "size": 12
                }
            },
            "layoutByTheme": {
                "dark": {
                    "font": {
                        "color": "#d5d5d6"
                    },
                    "paper_bgcolor": "#212123"
                }
            }
        }
    }

    Results in:

    {
        "plotlyOptions": {
            "layout": {
                "paper_bgcolor": "#212123",
                "margin": {
                    "l": 40,
                    "r": 40,
                    "t": 10,
                    "b": 40
                },
                "font": {
                    "color": "#d5d5d6"
                }
            }
        }
    }

    Observe that the font.size setting of the original layout is lost since the dark.font property, which doesn’t contain size, replaced it.

  • deep: The deep merge, which is the default setting, attempts to combine the settings from the initial and themed layouts at every level of the model hierarchy. Applied to the example above this yields the following:

    {
        "plotlyOptions": {
            "layout": {
                "paper_bgcolor": "#212123",
                "margin": {
                    "l": 40,
                    "r": 40,
                    "t": 10,
                    "b": 40
                },
                "font": {
                    "size" : 12, // Size is now retained.
                    "color": "#d5d5d6"
                }
            }
        }
    }
One complication to be aware of with the deep merge is when dealing with arrays. Array elements are matched between the models by their position or index in the array. Failure to align matching properties by their position in the layout and themed layout sections will almost certainly produce unexpected results.

Data

The structure of the plotly data element is highly chart specific and typically contains more information than just trace values. See plotly docs for details.

This can present a challenge when trying to render data from the core. Below are examples of possible strategies used to retrieve and process data. The examples show an implementation with a scatter plot but the approach should be applicable to most chart types. Refer to the example compilation to see these in action

Use custom Lua to tailor the data read and process it to match the chart widget model:

This typically involved combining trace data with properties required by the plotly chart. In the example below, mode, type and name are added in addition to the object x and y values.

In this example, a custom library (eg. myPlotlyLib) is created and installed on an object in the core (eg. /System/Core/Examples) to contain a function called getScatterData(). The function arguments consist of an array of one or more tag paths, start and end time and a few optional settings to control the aggregation.

The Lua function to retrieve the plot data might look something like this:

local myPlotlyLib = {}

function myPlotlyLib:getScatterData( args, _, _ )
    local items = args.items or {} -- array of object paths to get data for
    if type(items) == "string" then items = { items } end -- support a single path string.
    local endTime = args.endTime or syslib.now() -- default end time to now
    local intervals = args.intervals or 100 -- default to 100 points
    local startTime = args.startTime or (endTime - (1000 * 60 * 60 )) -- default to 1 hour ago
    local aggregate = args.aggregate or "AGG_TYPE_INTERPOLATIVE"

    local newTrace = function( name, x, y)
        return {
            mode = "lines",
            type = "scatter",
            name = name,
            x = x,
            y = y
        }
    end

    if #items == 0 then
        -- No paths were provided
        return { newTrace() }
    else
        -- Read the historic data
        local data = syslib.gethistory( items, startTime, endTime, intervals, {aggregate} )
        local retVal = {}
        -- Pack it into a plotly data structure.
        for i, path in ipairs( items ) do
            local obj = syslib.getobject( path )
            if ( obj ~= nil ) then
                local name = obj.ObjectName
                local x = {}
                local y = {}
                -- Map T and V to X and Y
                for j, vqt in ipairs(data[i]) do
                    x[j] = vqt.T
                    y[j] = vqt.V
                end
                table.insert( retVal, newTrace( name, x, y) )
            else
                table.insert( retVal, newTrace() )
            end
        end
        return retVal
    end
end

return myPlotlyLib

On the WebStudio side, the library can be used in an advanced endpoint call of the dataSource pipeline which goes through the following steps:

  • Initialize the query time range: The startTime and endTime are set and applied to the pipeline message payload

  • Invoke the library function: The message payload is merged with the function farg and the lib is invoked

{
    "type": "plotly",
    "dataSource": [
        {
            "type": "gettime", // Initialize the start and end-times to pass to the lib call
            "set": [
                {
                    "name": "startTime",
                    "value": "*-1d",
                    "asEpoch": true // Provide the value as an epoch number
                },
                {
                    "name": "endTime",
                    "value": "*",
                    "asEpoch": true
                }
            ]
        },
        {
            "type": "function",
            "ctx": "/system",
            "lib": "myPlotlyLib",
            "func": "getScatterData",
            "farg": {
                // Provide function arguments.
                "items": [
                    "/System/Core/Examples/Demo Data/Process Data/DC4711",
                    "/System/Core/Examples/Demo Data/Process Data/DC666"
                ],
                "intervals": 100
            }
        }
    ],
    "plotlyOptions": {
    },
    // ...
}

The data returned from the lua function will look like this, and matches what the scatter chart expects:

[
  {
    "mode": "lines",
    "name": "DC4711",
    "type": "scatter",
    "x": [], // Data not shown
    "y": []
  },
  {
    "mode": "lines",
    "name": "DC666",
    "type": "scatter",
    "x": [], // data not shown
    "y": []
  }
]

Query the data with syslib endpoints and transform it in a pipeline:

This approach has the benefit that no custom Lua script is needed but comes at the expense of having to transform the data and add required properties on the client side using transform actions.

The dataSource of the plotly widget in the fragment below has been modified to execute the following steps.

  • Initialize the query time range: The startTime and endTime are set as before

  • Invoke the syslib library function: The message payload is merged with the function farg and the lib is invoked
    The readhistoricaldata call from the syslib.api library is used to ready the values.

  • Transform the returned data: A transform action is used to get the data in the right shape for the widget.

"dataSource": [
    {
        "type": "gettime",
        "set": [
            {
                "name": "start_time",
                "value": "*-1d"
            },
            {
                "name": "end_time",
                "value": "*"
            }
        ]
    },
    {
        "type": "function",
        "lib": "syslib.api",
        "func": "readhistoricaldata",
        "farg": {
            "processed_as_item_values": false, // Get the data back as separate V, Q and T arrays
            "intervals_no": 100,
            "items": [
                {
                    "p": "/System/Core/Examples/Demo Data/Process Data/DC4711",
                    "aggregate": "AGG_TYPE_INTERPOLATIVE"
                },
                {
                    "p": "/System/Core/Examples/Demo Data/Process Data/DC666",
                    "aggregate": "AGG_TYPE_INTERPOLATIVE"
                }
            ]
        }
    },
    {
        "type": "transform",
        "aggregateOne": [
            {
                "$project": {
                    "data": {
                        "$map": {
                            "input": "$data.items",
                            "in": {
                                "x": "$$this.intervals.T", // Map x and y values.
                                "y": "$$this.intervals.V",
                                "mode": "lines", // add chart properties
                                "type": "scatter",
                                "name": { // Use the last segment of the path as the name
                                    "$last": {
                                        "$split": [
                                            "$$this.p",
                                            "/"
                                        ]
                                    }
                                }
                            }
                        }
                    }
                }
            }
        ]
    }
]

Use query endpoints together with plotlyOptions.layout templates

In this implementation the approach is to place all properties which are not read from the backend into a layout template` inside the model.

In our example the mode and name properties are moved to the plotlyOptions.layout section of the model.

{
    "dataSource": [
        {
            "type": "gettime",
            "set": [
                {
                    "name": "start_time",
                    "value": "*-1d"
                },
                {
                    "name": "end_time",
                    "value": "*"
                }
            ]
        },
        {
            "type": "function",
            "lib": "syslib.api",
            "func": "readhistoricaldata",
            "farg": {
                "processed_as_item_values": false,
                "intervals_no": 100,
                "items": [
                    {
                        "p": "/System/Core/Examples/Demo Data/Process Data/DC4711",
                        "aggregate": "AGG_TYPE_INTERPOLATIVE"
                    },
                    {
                        "p": "/System/Core/Examples/Demo Data/Process Data/DC666",
                        "aggregate": "AGG_TYPE_INTERPOLATIVE"
                    }
                ]
            }
        },
        {
            "type": "transform",
            "aggregateOne": [
                {
                    "$project": {
                        "data": {
                            "$map": {
                                "input": "$data.items",
                                "in": { // The transform logic is only concerned with the X and Y values
                                    "x": "$$this.intervals.T",
                                    "y": "$$this.intervals.V"
                                }
                            }
                        }
                    }
                }
            ]
        }
    ],
    "plotlyOptions": {
        "layout": {
            "xaxis": {
                "showgrid": true,
                "type": "date", // ensures X values are interpreted as date values
                "autorange": true
            },
            "template": {
                "data": {
                    "scatter": [
                        {
                            "mode": "lines",
                            "name": "DC4711"
                        },
                        {
                            "mode": "lines",
                            "name": "DC666"
                        }
                    ]
                }
            }
        }
    }
}

onClick events

Plotly Click Events documentation.

Plotly chart can handle on click triggered event. Based on a click event on a Plotly chart, the onClick action defined in the Widget model is executed.

When onClick action is defined, received data format depends on Plotly Display ModeBar mode selection. Default mode is Compare data on hover, with this selection all data points that are defined on the same x axis will be received. Another option is Show closest data on hover, when this mode is selected only selected data point is received.

Charts that do not support click events:

  • Gauge charts

  • Bullet chart

  • Indicator

{
    "actions": {
        "onClick": [
            {
                "type": "send",
                "to": "debugger"
            },
            {
                "type": "transform",
                "aggregateOne": [
                    {
                        "$project": {
                            "text": "$points.0.label"
                        }
                    }
                ]
            }
        ]
    }
}

Scatter Plots

Plotly Scatter Plots documentation.

  • Mode (Markers, markers+text, line+markers)

  • Name

  • Marker (size, color, opacity)

  • Text (labels, text position, text font)

{
    "data": [
        {
            "type": "scatter",
            "x": [
                1,
                2
            ],
            "y": [
                10,
                15
            ],
            "mode": "markers+text",
            "name": "Team A",
            "text": [
                "A-1",
                "A-2"
            ],
            "textposition": "top center",
            "textfont": {
                "family": "Raleway, sans-serif"
            },
            "marker": {
                "size": 12
            }
        }
    ]
}

Line Charts

Plotly Line Charts documentation.

  • Line (lines, markers, lines+markers)

  • Name

  • Marker (size, color, opacity)

  • Line (color, opacity, width)

{
    "data": [
        {
            "type": "scatter",
            "x": [
                1,
                2
            ],
            "y": [
                10,
                15
            ],
            "mode": "lines+markers",
            "marker": {
                "color": "red",
                "size": 8
            },
            "line": {
                "color": "red",
                "width": 1
            }
        }
    ]
}

Bar Charts

Plotly Bar Charts documentation.

  • Orientation (h, v)

  • Name

  • Text

  • Text Position

  • Hover Info

  • Marker (color, opacity, line(color, opacity, width))

{
    "data": [
        {
            "type": "bar",
            "x": [
                "Line 1",
                "Line 2"
            ],
            "y": [
                20,
                14
            ],
            "name": "Cologne",
            "text": [
                "20",
                "14"
            ],
            "textposition": "auto",
            "hoverinfo": "none",
            "marker": {
                "color": "blue",
                "opacity": 0.6,
                "line": {
                    "color": "blue",
                    "width": 1.5
                }
            }
        }
    ]
}

Layout Options:

  • Bar Mode (group, stack)

  • Bar Gap

{
    "plotlyOptions": {
        "layout": {
            "barmode": "group",
            "bargap" :0.05
        }
    }
}

Pie Charts

Plotly Pie Charts documentation.

  • Labels

  • Domain (row and column or x and y)

  • Hole (for donut chart)

  • Text Info (label, percent, label+percent)

  • Inside Text Orientation (radial)

  • Auto margin

  • Text Position (outside (inside is default))

{
    "data": [
        {
            "values": [
                10,
                30,
                60
            ],
            "labels": [
                "1st",
                "2nd",
                "3rd"
            ],
            "type": "pie",
            "textinfo": "label+percent",
            "textposition": "outside",
            "insidetextorientation": "radial",
            "automargin": true,
            "hole": 0.4,
            "domain": {
                "row": 0,
                "column": 0
            }
        }
    ]
}

Layout Options:

  • Height

  • Width

  • Grid (rows, columns)

{
    "plotlyOptions": {
        "layout": {
            "height": 400,
            "width": 500,
            "grid": {
                "rows": 2,
                "columns": 2
            }
        }
    }
}

Filled Area Plots

Plotly Filled Area Plots documentation.

  • Fill (tozeroy, tonexty, toself)

  • Fill Color

  • Mode (none)

  • Stack Group

  • Group Norm (percent)

  • Hoveron (points+fills)

  • Line (color, opacity)

  • Text

  • Hoverinfo (text)

{
    "data": [
        {
            "type": "scatter",
            "x": [
                1,
                2
            ],
            "y": [
                0,
                2
            ],
            "fill": "tonexty",
            "mode": "none"
        }
    ]
}

Layout Options:

  • X-axis (range)

  • Y-axis (range)

{
    "plotlyOptions": {
        "layout": {
            "xaxis": {
                "range": [
                    0,
                    5
                ]
            },
            "yaxis": {
                "range": [
                    0,
                    3
                ]
            }
        }
    }
}

Box Plot

Plotly Box Plot documentation.

  • Box Points

  • Jitter

  • Point Pos

  • Whisker Width

  • Fill Color

  • Marker (color, opacity)

  • Line

  • Box points (false, outliers, suspectedoutliers),

  • Box mean (true, sd)

  • Orientation (h, v)

{
    "data": [
        {
            "type": "box",
            "y": [
                0,
                1,
                1,
                2,
                3,
                5,
                8,
                13,
                21
            ],
            "boxpoints": "all",
            "jitter": 0.3,
            "pointpos": 0,
            "whiskerwidth": 0.2,
            "fillcolor": "white",
            "boxpoints": "Outliers",
            "marker": {
                "size": 5
            },
            "line": {
                "width": 1
            }
        }
    ]
}

Layout Options:

  • Box Mode (group, stack, overlay)

{
    "plotlyOptions": {
        "layout": {
            "boxmode": "group"
        }
    }
}

Histogram

Plotly Histograms documentation.

  • Name

  • Marker (color, opacity line(color, opacity, width))

  • Auto Bin x

  • Auto Bin y

{
    "data": [
        {
            "type": "histogram",
            "y": [
                20,
                21,
                23,
                21,
                22,
                28,
                29,
                30,
                19,
                33
            ],
            "marker": {
                "color": "blue",
                "opacity": 0.2
            }
        },
        {
            "type": "histogram",
            "y": [
                25,
                26,
                28,
                20,
                21,
                23,
                21,
                22,
                28,
                29
            ],
            "marker": {
                "color": "red",
                "opacity": 0.2
            }
        }
    ]
}

Layout Options:

  • Bar Mode (group, stack, overlay)

  • Bar Gap

  • Bar Group Gap

{
    "plotlyOptions": {
        "layout": {
            "paper_bgcolor": "white",
            "barmode": "overlay",
            "bargap": 0.05,
            "bargroupgap": 0.2
        }
    }
}