How to change the dash, stroke or point style of a specific section of a line chart in JavaScript

·

9 min read

How to change the dash, stroke or point style of a specific section of a line chart in JavaScript

23rd September, 2024

In this blog post will cover how to change the style of a specific part of a line, scatter, column or mountain chart in JavaScript.

Perhaps you want to create a JavaScript Line Chart where part of the line is dashed and part is solid.

Or perhaps a JavaScript scatter chart where different point-markers (e.g. circle, cross, triangle, square) are displayed on different parts of the chart series.

Above: React Chart with Multi Series Style example, showing how to dynamically switch styles in a javascript/react chart

This could be an important use-case in real-tine analytics, where you want to show additional states on a JavaScript chart, for example, data with low confidence could be displayed as a dashed line or semi-transparent line, whereas data with high confidence could be displayed as a solid line.

Solution 1 – Multiple Series

Until now, various JavaScript chart libraries can only achieve changing the style of a line chart dynamically using multiple series.

For example, in order to achieve a JavaScript line chart where different segments are dashed and solid, you would need to have two separate line series on the same 2D chart.

two-series-dashed-dotted-javascript-chart

Many chart libraries such as chart.js will allow you to display a line chart with both solid and dashed line by using this technique, for example having two datasets with two different styles on the same chart (see StackOverflow: Can we draw a line chart with both solid and dotted line in it).

var lineChartData = {
    labels: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
    datasets: [{
        label: "My First dataset",
        data: [1, 8, 3, 4, 2, 3, 4],
        borderColor: '#66f',
        borderDash: [20, 30]
    },{
        label: "My First dataset",
        data: [1, 8, 3, 4, 2, , ],
        borderColor: '#66f',
    }]
};

var ctx = document.getElementById("chart").getContext("2d");
var myChart = new Chart(ctx, {
    type: "line",
    data: lineChartData,
    options: {
        elements: {
            line: {
                fill: false
            }
        }
    }
});

This results in the following output (JSFiddle: jsfiddle.net/uwb8357r)

jsfiddle-two-charts-chartjs-dotted-dashed

Apache eCharts also has a similar way of handling splitting of series of different styles. For example, this stackoverflow post titled How to Change the style of a specific section/part of a line to dotted/dashed describes how you can change the style of a specific section/part of a line chart to dotted/dashed lines in eCharts, and the only solution is to have two series, one for each line. One for solid line, one for dashed line, with two separate datasets.

This is not a bad solution, however in the case where you have multiple series on a chart, or where you have a legend, or want to handle selection / clicking / hovering callbacks or drilldowns on a chart, having extra series present in the data model can confuse your software developers by making the code more complex than it needs to be and more difficult to understand or maintain.

It would be a better solution to have a single line series with multiple styles applied to the same series. However, how can this be done in JavaScript?

Solution 2 – Using the Render Data Transform API in SciChart.js

SciChart.js – a High Performance JavaScript and React Chart library for scientific, engineering, medical and financial applications features an API called Render Data Transforms.

This API allows you to transform your data immediately before it is drawn, affecting the final visual output on the screen while keeping your data unchanged.

It also allows you to perform visual transformations in a JavaScript chart, for example:

  • Interpolating data to insert extra points (spline, Bezier transformation)

  • Switching styles on a series, for example having dashed/solid lines as part of the same line chart

  • Splitting line segments, for example inserting extra points to deal with color-transitions at thresholds where a line goes above/below a threshold value

One such use-case of the RenderDataTransforms API would be to allow you to switch styles between solid and dashed lines in a JavaScript chart, using the same dataset & single series.

For this worked example we’ll use the same dataset as the chart.js example previousl provided, however with an additional third parameter called dash which is 0 or 1 (where 0 means solid line, 1 means dashed line).

X = [0,1,2,3,4,5,6]
Y = [1,8,3,4,2,3,4]
Dash = [0,0,0,0,1,1,1]

It’s important to note that a 1 in the Dash[] array means this point has a dashed line, however a dashed line segment must be defined by two points. Therefore an array for dashes of [0,0,0,0,1,1,1] would actually result in two dashed segments at the end of the chart.

In SciChart.js, X,Y data can be stored in a XyDataSeries, and additional information can be stored in a flexible JavaScript object called Metadata. To declare a data series with the above data, use this code:

// Create the data. X,Y values are numeric
// dashValues are mapped to metadata (javascript objects)
const xValues = [0,1,2,3,4,5,6];
const yValues = [1,8,3,4,2,3,4];
const metadata = [0,0,0,0,1,1,1].map(num => ({ dash: num }));

const dataSeries = new XyDataSeries(wasmContext, { xValues, yValues, metadata });

Next, we need to setup a chart which will render the data as a line series.

  // Create a SciChartSurface with X,Y Axis
  const { sciChartSurface, wasmContext } = await SciChartSurface.create(divElementId, { 
    theme: new SciChartJsNavyTheme() }
  );
  sciChartSurface.xAxes.add(new NumericAxis(wasmContext));
  sciChartSurface.yAxes.add(new NumericAxis(wasmContext, { growBy: new NumberRange(0.1, 0.1)}));

  // Create the data. X,Y values are numeric
  // dashValues are mapped to metadata (javascript objects)
  const xValues = [0,1,2,3,4,5,6];
  const yValues = [1,8,3,4,2,3,4];
  const metadata = [0,0,0,0,1,1,1].map(num => ({ dash: num }));

  const dataSeries = new XyDataSeries(wasmContext, {
      xValues,
      yValues,
      metadata
    });  
  const lineSeries = new FastLineRenderableSeries(wasmContext, {
    dataSeries,
    strokeThickness: 3,
    stroke: "SteelBlue",
    pointMarker: new EllipsePointMarker(wasmContext, { width: 7, height: 7, strokeThickness:0, fill: "SteelBlue"})
  });

  sciChartSurface.renderableSeries.add(lineSeries);

To split the style in the series based on the metadata[i].dash value, we need to use a RenderDataTransform. We can also inject something called a DrawingProvider into the line series to draw the alternate points. Let’s setup this code below:

class DashedOrSolidRenderDataTransform extends XyyBaseRenderDataTransform {
  protected runTransformInternal(renderPassData: RenderPassData): IPointSeries {
  // Guard in case the incoming data is empty
  // If you want to do nothing and draw the original data, you don't need to copy it, you can just return renderPassData.pointSeries
  if (!renderPassData.pointSeries) {
    return this.pointSeries;
  }
  // It is important to reuse this.pointSeries.  Do NOT create a new pointSeries on each transform
  const {
    xValues: oldX,
    yValues: oldY,
    indexes: oldI,
    resampled,
  } = renderPassData.pointSeries;
  const { xValues, yValues, y1Values, indexes } = this.pointSeries;

  // Clear the target vectors
  xValues.clear();
  yValues.clear();
  y1Values.clear();
  indexes.clear();

  // indexRange tells the drawing to only use a subset of the data.  If data has been resampled, then always use all of it
  const iStart = resampled ? 0 : renderPassData.indexRange.min;
        const iEnd = resampled ? oldX.size() - 1 : renderPassData.indexRange?.max;
        const ds = this.parentSeries.dataSeries as XyDataSeries;
        let prevDash = false;
        for (let i = iStart; i <= iEnd; i++) {
            const index = resampled ? oldI.get(i) : i;
            const md = ds.getMetadataAt(index);
            xValues.push_back(oldX.get(i));
            indexes.push_back(oldI.get(i));
            // Add normal point if the current or previous is not dashed
            yValues.push_back((!md.dash || !prevDash) ? oldY.get(i) : NaN);   
            // Add dashed point if the current or previous is dashed
            y1Values.push_back(prevDash || md.dash ? oldY.get(i) : NaN);

            if (md.dash && !prevDash) {
              // Add a break in the normal if the current is dashed but the previous was not
              xValues.push_back(oldX.get(i));
              yValues.push_back(NaN);
              y1Values.push_back(oldY.get(i))
            } else if (!md.dash && prevDash) {
              // Add a break in the dashed if the current is not dashed but the previous was
              xValues.push_back(oldX.get(i));
              yValues.push_back(oldY.get(i));
              y1Values.push_back(NaN);
            }
            prevDash = md.dash;
        }
    // Return the transformed point series
        return this.pointSeries;
  }
}

JavaScript

Copy

The Render Data Transform must be applied to a series like this before the series is added to the chart:

// Add the new drawingProvider to the series. The selector ps.y1Values means use the alternate data provided by the render transform
const dashedDrawingProvider = new LineSeriesDrawingProvider(wasmContext, lineSeries, (ps) => ps.y1Values);
lineSeries.drawingProviders.push(dashedDrawingProvider);

dashedDrawingProvider.getProperties = (parentSeries) => {
 const { stroke, strokeThickness, opacity, isDigitalLine, lineType, drawNaNAs } = parentSeries;
 return {
     stroke,
     strokeThickness,
     strokeDashArray: [3, 4], // Here's where we swap out stroke-dashing
     isDigitalLine,
     lineType,
     drawNaNAs,
     containsNaN: true,        
  };          
};

// Create the transform and add it to the series. Pass only the lineDrawingProviders
lineSeries.renderDataTransform = new DashedOrSolidRenderDataTransform(lineSeries, wasmContext, [lineSeries.drawingProviders[0], dashedDrawingProvider]);

Simplifying the Solution by Encapsulating Logic into a Custom Series

The solution provided relies on a lot of boilerplate code, which can be simplified greatly be encapsulating into a Custom Series.

One of the benefits of SciChart.js is that it’s an extremely extensible JavaScript Chart Library, meaning that you can write your own series types, extensions, interaction types or custom rendering in JavaScript, allowing you to infinitely configure the charts in your application to meet user requirements.

Here’s a class for a custom series called DashDottedLineSeries which is based on the FastLineRenderableSeries and the RenderDataTransforms API.

class DashDottedLineSeries extends FastLineRenderableSeries {
  constructor(wasmContext, options) {
    super(wasmContext, options);
    this.alternateStrokeDashArray = [3,4];
    this.alternateStroke = "Orange";

    // Add the new drawingProvider to the series
    const dashedDrawingProvider = new LineSeriesDrawingProvider(wasmContext, this, (ps) => ps.y1Values);
    this.drawingProviders.push(dashedDrawingProvider);

    dashedDrawingProvider.getProperties = (parentSeries) => {
      const { stroke, strokeThickness, opacity, isDigitalLine, lineType, drawNaNAs } = parentSeries;
      return {
          stroke: this.alternateStroke,
          strokeThickness,
          strokeDashArray: this.alternateStrokeDashArray,
          isDigitalLine,
          lineType,
          drawNaNAs,
          containsNaN: true,        
      };
    };

    // Create the transform and add it to the series. Pass only the lineDrawingProviders
    this.renderDataTransform = new DashedOrSolidRenderDataTransform(this, wasmContext, [this.drawingProviders[0], dashedDrawingProvider]);  
  }    
}

Now the usage comes down simply to this:

// Create a SciChartSurface with X,Y Axis
const { sciChartSurface, wasmContext } = await SciChartSurface.create(divElementId, { 
    theme: new SciChartJsNavyTheme() 
});
sciChartSurface.xAxes.add(new NumericAxis(wasmContext));
sciChartSurface.yAxes.add(new NumericAxis(wasmContext));

// Create the data. X,Y values are numeric
// dashValues are mapped to metadata (javascript objects)
const xValues = [0,1,2,3,4,5,6];
const yValues = [1,8,3,4,2,3,4];
const metadata = [0,0,0,0,0,1,1].map(num => ({ dash: num }));

const dataSeries = new XyDataSeries(wasmContext, {
  xValues,
  yValues,
  metadata
});  
const lineSeries = new DashDottedLineSeries(wasmContext, {
    dataSeries,
    strokeThickness: 3,
    stroke: "SteelBlue",
    pointMarker: new EllipsePointMarker(wasmContext, { width: 7, height: 7, strokeThickness:0, fill: "SteelBlue"})
});
sciChartSurface.renderableSeries.add(lineSeries);

Conclusion

In JavaScript or React it’s possible to create data-analytics applications where certain sections of a line chart are dotted/dashed while other sections are solid. It’s also possible to dynamically change the stroke color, stroke thickness, or pointmarker of a line chart in JavaScript for sections of the data with higher/lower confidence, or to signify events or areas of importance in the data.

This can be achieved in SciChart.js by using a Render Data Transforms API, which allows you to inject styles or extra data-points into a JavaScript chart immediately before draw.

Finally, to simplify your application code, logic for the Render Transform can be encapsulated into a custom series, which can then be re-used throughout your application.

Contact us to learn more

SciChart.js is a fully WebGL accelerated JavaScript Chart and React Chart Library designed for complex, mission critical data visualization applications. Now with a FREE community edition. If you have a question or would like to learn more about our products & services, please contact us:

CONTACT US | GET SCICHART.JS FREE

By Andrew Burnett-Thompson | Sep 23, 2024

CEO / Founder of SciChart. Masters (MEng) and PhD in Electronics & Signal Processing.Follow me on LinkedIn for more SciChart content, or twitter at @drandrewbt.