Skip to content

Modifying Existing Charts

Subscription Tier Required

This feature requires the Premier subscription tier or higher.

GS strives to provide reasonable defaults and built-in customizations for analysis. However, there are situations where additional customization is required. This can include:

  • Overlaying additional information on a chart.
  • Changing the colors of a chart.
  • Adding additional hover text.

Warning

Changing the data of a chart will disable some built-in GS functionality. For example, modifying the data of a Control Chart will disable the ability to click on a point to edit the data.

This guide will walk through displaying error lines on a Control Chart, based on the accuracy of the equipment used to record the data.

Prerequisites

This guide requires having SPC data for a Characteristic which includes Traceability that records the device used to take the reading. We will keep a hardcoded lookup of device tolerances in code, although this could easily be swapped out for a lookup in a third party API.

If you do not have data, you may import the required entities and data using the provided files:

Creating the Dashboard

To begin, create a new Dashboard. The fastest way to create a new Dashboard is to navigate to the Quick Chart menu entry, which acts as a shortcut for a new Dashboard.

Adding a Retrieval

Next, add a Retrieval:

  1. Press the Add Retrieval button. An image showing the location of the Add Retrieval button on the Dashboard
  2. Fill in the Retrieval Name with Inside Diameter. An image showing the Name and Script ID fields
  3. Modify the Date field to a range which includes data for your Characteristic. If the provided data was imported in the last day, this may be left as Current Day. An image showing the date field
  4. Change the Retrieval Type to Characteristic.
  5. Select Inside Diameter Characteristic in the Characteristics field. An image showing the date field
  6. Press Confirm. An image showing the confirm button

Add a Control Chart

Add a Control Chart, selecting the Inside Diameter Retrieval. Resize the Control Chart to the full width of the Dashboard. Change the Label to Inside Diameter and the Script ID to errorBars.

An image showing the Scripted Chart button

Scripting

Now that the Dashboard has been set up, you can begin scripting. Create a new Dashboard Script and give it a memorable name. If this interface is unfamiliar, review the Dashboard Scripting Principles and Code Editor articles.

An image showing the location of the Dashboard Script action

An image showing the location of the add button in the Dashboard Script overlay

The tasks we will use scripting to accomplish are:

  1. Tell the Control Chart to include the underlying data when accessed.
  2. Listening to when the Control Chart is about to draw.
  3. Iterate through the data.
  4. Look up the tolerance based on the Measuring Device Traceability.
  5. Modify the data to include the error bars based on the tolerance.

Including Underlying Data

By default, Control Charts will only include the minimum fields needed to draw the chart. However, for our purposes, we need access to the Traceability. In order to include the Traceability, we must update the Control Chart when the Dashboard is ready:

gsApi.dashboard.onReady(async () => {
    await gsApi.dashboard.controlChart('errorBars').updateProperties({
        includeData: true
    });
});

Listening to Before Draw

In the newly created Dashboard Script, bind to the onBeforeDraw event:

gsApi.dashboard.controlChart('errorBars').onBeforeDraw(async (e) => {

});

This event will trigger after the Control Chart has retrieved data, but before it draws. The event object passed into the bound function contains a data object, which has the plotlyData property. Modifying the plotlyData will change how the Control Chart is drawn.

Iterating Over the Data

Next, iterate over the Control Details. Each Control Detail corresponds to one group that has been created using the Split By property, and corresponds to a single chart being drawn. For example, if the Split By of the Control Chart is set to the Machine Traceability, there will be one Control Detail for each distinct Machine in the data set:

gsApi.dashboard.controlChart('errorBars').onBeforeDraw(async (e) => {
    e.data.detail.forEach((controlDetail, index) => {

    });
});

For each Control Detail, obtain the corresponding Plotly scatter plots. The data object in the event contains a plotlyData array. This array contains one element for each Control Detail, and can be obtained by using the same index as the current Control Detail.

The Plotly Data consists of a Plotly layout and a list of Plotly traces. Each trace corresponds to a separate element drawn on the chart, like a single line or set of bars. For Control Charts, the scatter traces which represent the data will follow the format of data:group, where group is the value for Group By. The startsWith function may be used to filter the scatter traces to only include ones that contain the data points for the Control Chart:

e.data.detail.forEach((controlDetail, index) => {
    const detailPlot = e.data.plotlyData[index];

    const scatterPlots = detailPlot.data
        .filter((data) => data.type === 'scatter' && data.name?.startsWith('data'));
});

Next, iterate over every retrieved Scatter Plot and each point within that Plot:

scatterPlots.forEach((plot) => {
    plot.x?.forEach((xValue) => {

    });
});

Type Casting

You may notice that attempting to access the x property of the plot results in an error message. This is due to the code editor not knowing that the scatterPlots variable only contains scatter traces.

In order to tell the code editor that we have restricted the list of plots to only include scatter plots, begin by importing the correct type from Plotly at the top of the file:

/** @typedef {import('plotly.js-dist-min/index').PlotData} PlotData */

Information

The built-in GS types are available globally and do not require importing. Plotly types require a special import from the plotly.js-dist-min/index module. See the Typescript documentation form more information on importing type definitions.

Next, add the @type annotation to the call when the scatter plots are filtered:

const scatterPlots = /** @type {Partial<PlotData>[]} **/(plot.data
    .filter((data) => data.type === 'scatter' && data.name?.startsWith('data')));

Information

The above type annotation is entirely optional, and omitting it will not prevent the code from running.

Looking up Measurement Tolerance

For each data point, we will need to look up the Measurement Device Traceability. This information is not typically included in the Control Detail, and will need to be retrieved from the Data Detail which we included in the including underlying data section. This data is set as an array on the event data object, and will contain one element for each split. Find the corresponding underlying data using the same index as the detailPlot:

const detailPlot = e.data.plotlyData[index];
const includedData = e.data.data?.[index];
if (!includedData) {
    // If this wasn't set for some reason, skip this detail
    return;
}

The included data is not split out into Groups like the Control Details and scatter traces are, so a running index will need to be used to look up the correct data point:

let runningDataPointIndex = 0;
scatterPlots.forEach((plot) => {
    plot.x?.forEach((xValue) => {
        const dataPoint = includedData.data[runningDataPointIndex];
        runningDataPointIndex++;
    });
});

Next, retrieve and store the ID of the Measurement Device Traceability:

let measurementDeviceId;
gsApi.dashboard.onReady(async () => {
    measurementDeviceId = await gsApi.entity.getTraceabilityIdByName('Measurement Device');
    await gsApi.dashboard.controlChart('errorBars').updateProperties({
        includeData: true
    });
});

Then, use this ID to look up the value of the measurement device:

const dataPoint = includedData.data[runningDataPointIndex];
const measurementDevice = dataPoint.traceability[measurementDeviceId]?.value;
runningDataPointIndex++;

Now that you have the value of the Measurement Device, create a mapping of Measurement Devices to the tolerance. For the purpose of this guide, this mapping is written directly into the code. However, it could easily be retrieved from an API or a file stored in GS.

let measurementDeviceId;
const deviceTolerances = {
    'Brand A Calipers': 0.0003,
    'Brand B Calipers': 0.00025,
    'Brand C Calipers': 0.0001,
    'Brand D Calipers': 0.0004
};

And the tolerance may be obtained with:

deviceTolerances[measurementDevice];

Adding the Error Bars

Now that the tolerance for each point has been obtained, add an array to keep track of the error level for each point, and set it on the plot after it has been constructed:

scatterPlots.forEach((plot) => {
    const errorBars = [];
    plot.x?.forEach((xValue) => {
        ...
    });
    plot.error_y = {
        type: 'data',
        array: errorBars,
        visible: true
    };

Inside the loop, assign the looked up tolerance to the error bar. If the Measurement Device Traceability is not set on the data point for some reason, use null instead:

const dataPoint = includedData.data[runningDataPointIndex];
const measurementDevice = dataPoint.traceability.find(x => x.id === measurementDeviceId)?.value;
if (measurementDevice) {
    errorBars.push(deviceTolerances[measurementDevice]);
} else {
    errorBars.push(null);
}
runningDataPointIndex++;

Testing the Dashboard

Save and View the Dashboard. If you have not previously saved the Dashboard, fill in the Name overlay with Error Bars. The Dashboard should load with the Control Chart, including error bars.

An image showing the final control chart

Additional Exercises

On your own, attempt to:

  • Load the device tolerances from a file uploaded to GS.
  • Include the error bars on the range chart.

Final Code

/** @typedef {import('plotly.js-dist-min/index').PlotData} PlotData */
let measurementDeviceId;
const deviceTolerances = {
    'Brand A Calipers': 0.0003,
    'Brand B Calipers': 0.00025,
    'Brand C Calipers': 0.0001,
    'Brand D Calipers': 0.0004
};
gsApi.dashboard.onReady(async () => {
    measurementDeviceId = await gsApi.entity.getTraceabilityIdByName('Measurement Device');
    await gsApi.dashboard.controlChart('errorBars').updateProperties({
        includeData: true
    });
});


gsApi.dashboard.controlChart('errorBars').onBeforeDraw(async (e) => {
    e.data.detail.forEach((controlDetail, index) => {
        const detailPlot = e.data.plotlyData[index];
        const includedData = e.data.data?.[index];
        if (!includedData) {
            // If this wasn't set for some reason, skip this detail
            return;
        }

        const scatterPlots = /** @type {Partial<PlotData>[]} **/(detailPlot.data
            .filter((data) => data.type === 'scatter' && data.name?.startsWith('data')));

        let runningDataPointIndex = 0;
        scatterPlots.forEach((plot) => {
            const errorBars = [];
            plot.x?.forEach((xValue) => {
                const dataPoint = includedData.data[runningDataPointIndex];
                const measurementDevice = dataPoint.traceability.find(x => x.id === measurementDeviceId)?.value;
                if (measurementDevice) {
                    errorBars.push(deviceTolerances[measurementDevice]);
                } else {
                    errorBars.push(null);
                }
                runningDataPointIndex++;
            });
            plot.error_y = {
                type: 'data',
                array: errorBars,
                visible: true
            };
        });
    });
});