avatarJennifer Fu

Summary

The provided content is a comprehensive guide on using Highcharts, a JavaScript charting library, to create custom graphs, including node-based graphs, within a React application.

Abstract

The article offers a step-by-step tutorial on exploring the drawing capabilities of Highcharts, emphasizing its use in creating custom charts and shapes within a React environment. It begins with an introduction to Highcharts, noting its enterprise-grade quality, excellent documentation, and the need for a commercial license for non-personal use. The guide walks through setting up a React project with Vite, installing Highcharts and its React official wrapper, and demonstrating how to draw a basic line chart. It then delves into advanced features, showing how to add interactive buttons to switch chart types and how to draw primitive shapes like rectangles, circles, and labels directly onto the chart or independently. The article also illustrates how to construct node-based graphs using labels and paths, including the dynamic creation of arrows and curves. The final example, available in a GitHub repository, showcases a complex node-based graph with multiple nodes and arrows. The conclusion praises Highcharts for its versatility and extensive range of chart types, recommending it for commercial applications when budget permits.

Opinions

  • Highcharts is highly regarded for its comprehensive documentation and extensive examples, making it a valuable tool for developers.
  • The library's rendering layer is praised for its flexibility, allowing direct access to draw various shapes and text.
  • The use of SVGRenderer for creating buttons, shapes, and labels is highlighted as a powerful feature for customizing charts.
  • The author suggests that Highcharts is a worthwhile investment for commercial projects due to its robust capabilities, despite the cost for commercial licensing.
  • The article implies that Highcharts integrates well with React, enhancing the developer experience in building interactive and complex visualizations.
  • The author expresses satisfaction with the collaboration with S Sreeram and the team on Domino products, acknowledging their contributions.
  • The reader is encouraged to engage with the author's broader body of work and the PlainEnglish.io community for more web development content.

Exploring Highcharts Drawing Capabilities

A step-by-step guide on Highcharts custom graphs

Image by author

Introduction

Highcharts is an enterprise-grade JavaScript charting library based on SVG. It comes with excellent documentation and innumerable examples. The only drawback is that the license is proprietary. It is free for personal/non-commercial uses, but you must pay for commercial applications.

Besides plotting charts, Highcharts allows direct access to the rendering layer to draw primitive shapes like buttons, circles, rectangles, paths, or text directly on a chart, or independent from any chart.

Let’s take a look at how it works.

A Basic Chart

We are going to use a Vite application to explore Highcharts. The following command creates a React project:

% yarn create vite react-custom-graphs --template react
% cd react-custom-graphs

Install highcharts and highcharts-react-official.

% yarn add highcharts highcharts-react-official

After the installation, these packages become part of dependencies in package.json:

"dependencies": {
  "highcharts": "^11.1.0",
  "highcharts-react-official": "^3.2.0"
}

We have the working environment set to explore Highcharts.

As we have explained in a previous article, it is recommended to include the following imports for every chart:

import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import HighchartsExporting from 'highcharts/modules/exporting';
import HighchartsAccessibility from 'highcharts/modules/accessibility';
  • highcharts: It is the JavaScript charting library.
  • highcharts-react-official: It is the official Highcharts-supported wrapper for React.
  • highcharts/modules/exporting: It is the Highcharts exporting module that provides a menu with export-related menu items to View in full screen, Print chart, Download PNG image, Download JPEG image, Download PDF document, and Download SVG vector image.
  • highcharts/modules/accessibility: It is the Highcharts accessibility module that provides accessibility for the generated chart HTML.

Modify index.html to make the application titled Exploring Highcharts:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Exploring Highcharts</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

Here is the modified src/App.js that draws a line chart:

import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import HighchartsExporting from 'highcharts/modules/exporting';
import HighchartsAccessibility from 'highcharts/modules/accessibility';
HighchartsExporting(Highcharts);
HighchartsAccessibility(Highcharts);

const getOptions = () => ({
  chart: {
    type: 'line',
    width: 800,
    height: 600,
  },
  title: {
    text: 'Line Chart',
  },
  yAxis: {
    title: {
      text: 'Values',
    },
  },
  series: [
    {
      data: [1, 2, 1, 4, 3, 6],
    },
    {
      data: [2, 7, 0, 4, 6, 2],
    },
  ],
  credits: {
    enabled: false,
  },
});

function App() {

  return (
    <div id="container">
      <HighchartsReact highcharts={Highcharts} options={getOptions()} />
    </div>
  )
}

export default App;

Execute yarn dev, and we see the line chart.

Image by author

Chart With Buttons

Highcharts allows direct access to the rendering layer to draw primitive shapes like buttons, circles, rectangles, paths or text directly on a chart. Highcharts.SVGRenderer supports the following API to add buttons:

button(text, x, y, callback [, theme] [, hoverState] [, selectState] [, disabledState] [, shape] [, useHTML])

text: string - The text or HTML to draw.

x: number - The x position of the button's left side.

y: number - The y position of the button's top side.

callback: Highcharts.EventCallbackFunction.<Highcharts.SVGElement> - The function to execute on button click or touch.

theme?: Highcharts.SVGAttributes - SVG attributes for the normal state.

hoverState?: Highcharts.SVGAttributes - SVG attributes for the hover state.

selectState?: Highcharts.SVGAttributes - SVG attributes for the pressed state.

disabledState?: Highcharts.SVGAttributes - SVG attributes for the disabled state.

shape?: Highcharts.SymbolKeyValue - The shape type. The default value is rect.

useHTML?: boolean - Whether to use HTML to render the label. The default value is false.

Here is the modified src/App.js:

import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import HighchartsExporting from 'highcharts/modules/exporting';
import HighchartsAccessibility from 'highcharts/modules/accessibility';
HighchartsExporting(Highcharts);
HighchartsAccessibility(Highcharts);

const getOptions = () => ({
  chart: {
    type: 'line',
    width: 800,
    height: 600,
    events: {
      load: function () {
        const chart = this;
        const renderer = chart.renderer;

        // the button to switch to a line chart
        const lineButton = renderer
          .button(
            'Line', // text
            chart.plotLeft, // x
            10, // y
            function () {
              // called upon clicking the line button
              chart.series.map((item) =>
                item.update({
                  type: 'line',
                })
              );
            }
          )
          .add();

        // the button to switch to a column chart
        renderer
          .button(
            'Column', // text
            chart.plotLeft + lineButton.width + 10, // x
            10, // y
            function () {
              // called upon clicking the column button
              chart.series.map((item) =>
                item.update({
                  type: 'column',
                })
              );
            }
          )
          .add();
      },
    },
  },
  title: {
    text: 'Custom Chart',
  },
  yAxis: {
    title: {
      text: 'Values',
    },
  },
  series: [
    {
      data: [1, 2, 1, 4, 3, 6],
    },
    {
      data: [2, 7, 0, 4, 6, 2],
    },
  ],
  credits: {
    enabled: false,
  },
});

function App() {
  return (
    <div id="container">
      <HighchartsReact highcharts={Highcharts} options={getOptions()} />
    </div>
  );
}

export default App; 

Execute yarn dev, and we see that the chart type changes upon clicking the Line or Column button.

Image by author

Chart With Shapes

Highcharts can draw primitive shapes without any chart. Highcharts.SVGRenderer supports the following API to add text:

text( [str] [, x] [, y] [, useHTML])

str?: string - The text of (subset) HTML to draw.

x?: number - The x position of the text's lower left corner.

y?: number - The y position of the text's lower left corner.

useHTML?: boolean - Use HTML to render the text. The default value is false.

Highcharts.SVGRenderer supports the following API to add rectangles:

rect( [x] [, y] [, width] [, height] [, r] [, strokeWidth])

x?: number - Left position.

y?: number - Top position.

width?: number - Width of the rectangle.

height?: number - Height of the rectangle.

r?: number - Border corner radius.

strokeWidth?: number - A stroke width can be supplied to allow crisp drawing.

Highcharts.SVGRenderer supports the following API to add circles:

circle( [x] [, y] [, r])

x?: number - The center x position.

y?: number - The center y position.

r?: number - The radius.

Highcharts.SVGRenderer supports the following API to add labels:

label(str, x [, y] [, shape] [, anchorX] [, anchorY] [, useHTML] [, baseline] [, className])

str: string - The initial text string or (subset) HTML to render.

x: number - The x position of the label's left side.

y?: number - The y position of the label's top side or baseline, depending on the baseline parameter.

shape?: string - The shape of the label's border/background, if any. Defaults to rect. Other possible values are callout or other shapes defined in Highcharts.SVGRenderer#symbols.

anchorX?: number - In case the shape has a pointer, like a flag, this is the coordinates it should be pinned to.

anchorY?: number - In case the shape has a pointer, like a flag, this is the coordinates it should be pinned to.

useHTML?: boolean - Whether to use HTML to render the label. The default value is false.

baseline?: boolean - Whether to position the label relative to the text baseline, like renderer.text, or to the upper border of the rectangle. The default value is false.

className?: string - Class name for the group.

Highcharts.SVGRenderer supports the following API to add paths:

path( [path])

path: Highcharts.SVGPathArray - An SVG path definition in array form.

Here is the modified src/App.js:

import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import HighchartsExporting from 'highcharts/modules/exporting';
import HighchartsAccessibility from 'highcharts/modules/accessibility';
HighchartsExporting(Highcharts);
HighchartsAccessibility(Highcharts);

const getOptions = () => ({
  chart: {
    type: 'line',
    width: 800,
    height: 600,
    events: {
      load: function () {
        const chart = this;
        const renderer = chart.renderer;

        // add text
        renderer
          .text('Shape Examples', 50, 60)
          .css({
            fontWeight: 'bold',
            fontSize: '20px',
            stroke: 'green',
          })
          .add();

        // add a rectangle
        renderer
          .rect(100, 100, 300, 300, 5)
          .attr({
            'stroke-width': 2,
            stroke: 'red',
            fill: 'yellow',
          })
          .add();

        // add a label over the above rectangle
        renderer
          .label('Rectangle', 200, 230)
          .css({
            fontWeight: 'bold',
            fontSize: '16px',
          })
          .add();

        // add a circle
        renderer
          .circle(500, 300, 200)
          .attr({
            'stroke-width': 2,
            stroke: 'blue',
            fill: 'pink',
          })
          .add();

        // add a label over the above circle
        renderer
          .label('Circle', 470, 290)
          .css({
            fontWeight: 'bold',
            fontSize: '16px',
          })
          .add();

        // add a line with the dash style
        renderer
          .path(['M', 50, 520, 'L', 800, 520])
          .attr({
            'stroke-width': 2,
            stroke: 'silver',
            dashstyle: 'dash',
          })
          .add();

        // add a line with the ShortDashDotDot style
        renderer
          .path(['M', 50, 550, 'L', 800, 550])
          .attr({
            'stroke-width': 2,
            stroke: 'black',
            dashstyle: 'ShortDashDotDot',
          })
          .add();
      },
    },
  },
  title: {
    text: 'Custom Shapes',
  },
  credits: {
    enabled: false,
  },
});

function App() {
  return (
    <div id="container">
      <HighchartsReact highcharts={Highcharts} options={getOptions()} />
    </div>
  );
}

export default App;

Execute yarn dev, and we see text, a rectangle, a circle, two labels, and two paths.

Image by author

Node-Based Graphs

In a previews article, we have used React Flow to build interactive node-based graphs. With Highcharts, we have all the building blocks to create our own node-based graphs.

Here is the modified src/App.js that uses labels and paths to create a node-based graph:

import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import HighchartsExporting from 'highcharts/modules/exporting';
import HighchartsAccessibility from 'highcharts/modules/accessibility';
HighchartsExporting(Highcharts);
HighchartsAccessibility(Highcharts);

const SHADOW_SIZE = 3;

// first node
const node1 = {
  text: 'Node 1',
  x: 100,
  y: 100,
  width: 100,
  height: 50,
};

// second node
const node2 = {
  text: 'Node 2',
  x: 600,
  y: 500,
  width: 100,
  height: 50,
};

// the starting point of the arrow connecting node1 to node2
const arrowStart = {
  x: node1.x + node1.width / 2 + SHADOW_SIZE,
  y: node1.y + node1.height + SHADOW_SIZE * 2,
};

// the ending point of the arrow connecting node1 to node2
const arrowEnd = {
  x: node2.x + node2.width / 2 + SHADOW_SIZE,
  y: node2.y - SHADOW_SIZE,
};

// add a label with specific text, position, dimension
const addLabel = (renderer, { text, x, y, width, height }) =>
  renderer
    .label(text, x, y) 
    .css({
      fontWeight: 'bold',
      fontSize: '16px',
      color: 'white',
    })
    .attr({
      fill: 'blue',
      r: 5, // border radius
      'text-align': 'center',
      width,
      height,
    })
    .add()
    .shadow(true); // default showdow size is 3

const getOptions = () => ({
  chart: {
    type: 'line',
    width: 800,
    height: 600,
    events: {
      load: function () {
        const chart = this;
        const renderer = chart.renderer;

        // add the first label
        addLabel(renderer, node1);

        // add the second label
        addLabel(renderer, node2);

        // add the arrow from node1 to node2
        renderer
          .path([
            // arrow line
            'M',
            arrowStart.x,
            arrowStart.y,
            'L',
            arrowEnd.x,
            arrowEnd.y,
            // arrow end
            'L',
            arrowEnd.x - 12, // can be calculated based on the line slope
            arrowEnd.y - 2, // can be calculated based on the line slope
            'M',
            arrowEnd.x,
            arrowEnd.y,
            'L',
            arrowEnd.x - 7, // can be calculated based on the line slope
            arrowEnd.y - 10, // can be calculated based on the line slope
          ])
          .attr({
            'stroke-width': 2,
            stroke: 'gray',
          })
          .add();
      },
    },
  },
  title: {
    text: 'Node-Based Graph',
  },
  credits: {
    enabled: false,
  },
});

function App() {
  return (
    <div id="container">
      <HighchartsReact highcharts={Highcharts} options={getOptions()} />
    </div>
  );
}

export default App;

Execute yarn dev, and we see the node-based graph.

Image by author

The arrow line has been dynamically calculated based on node1 and node2 positions. However, the arrow head paths are hardcoded.

Calculating the arrow head paths based on the line slope would work. Alternatively, we can draw a curve line to make the arrow predictably pointing straight up or down.

Here is how to draw the cubic Bézier curve line in SVG:

C (x1 y1 x2 y2 x y) draws a cubic Bézier curve from the current point to (x, y) using (x1, y1) as the control point 1 at the beginning of the curve and (x2, y2) as the control point 2 at the end of the curve.

Image by author

Here is the modified src/App.js:

import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import HighchartsExporting from 'highcharts/modules/exporting';
import HighchartsAccessibility from 'highcharts/modules/accessibility';
HighchartsExporting(Highcharts);
HighchartsAccessibility(Highcharts);

const SHADOW_SIZE = 3;
const ARROW_LENGTH = 10;
const ARROW_ANGLE = Math.PI / 6;
const ARROW_X = ARROW_LENGTH * Math.sin(ARROW_ANGLE);
const ARROW_Y = ARROW_LENGTH * Math.cos(ARROW_ANGLE);

const node1 = {
  text: 'Node 1',
  x: 100,
  y: 100,
  width: 100,
  height: 50,
};

const node2 = {
  text: 'Node 2',
  x: 600,
  y: 500,
  width: 100,
  height: 50,
};

// add an arrow that connects node1 to node2
const addArrow = (renderer, node1, node2, isDoubleEnded) => {
  const arrowStart = {
    x: node1.x + node1.width / 2 + SHADOW_SIZE,
    y: node1.y + node1.height + SHADOW_SIZE * 2,
  };

  const arrowEnd = {
    x: node2.x + node2.width / 2 + SHADOW_SIZE,
    y: node2.y - SHADOW_SIZE,
  };

  const initPath = isDoubleEnded
    // the beginning arrow if it is double ended
    ? [
        'M',
        arrowStart.x - ARROW_X,
        arrowStart.y + ARROW_Y,
        'L',
        arrowStart.x,
        arrowStart.y,
        'L',
        arrowStart.x + ARROW_X,
        arrowStart.y + ARROW_Y,
      ]
    : [];

  renderer
    .path([
      ...initPath,
      // arrow line
      'M',
      arrowStart.x,
      arrowStart.y,
      'C',
      arrowStart.x,
      (arrowStart.y + arrowEnd.y) / 2,
      arrowEnd.x,
      (arrowStart.y + arrowEnd.y) / 2,
      arrowEnd.x,
      arrowEnd.y,
      // the ending arrow
      'L',
      arrowEnd.x - ARROW_X,
      arrowEnd.y - ARROW_Y,
      'M',
      arrowEnd.x,
      arrowEnd.y,
      'L',
      arrowEnd.x + ARROW_X,
      arrowEnd.y - ARROW_Y,
    ])
    .attr({
      'stroke-width': 2,
      stroke: 'gray',
    })
    .add();
};

// add a label with specific text, position, dimension
const addLabel = (renderer, { text, x, y, width, height }) =>
  renderer
    .label(text, x, y)
    .css({
      fontWeight: 'bold',
      fontSize: '16px',
      color: 'white',
    })
    .attr({
      fill: 'blue',
      r: 5, // border radius
      'text-align': 'center',
      width,
      height,
    })
    .add()
    .shadow(true);

const getOptions = () => ({
  chart: {
    type: 'line',
    width: 800,
    height: 600,
    events: {
      load: function () {
        const chart = this;
        const renderer = chart.renderer;

        // add the first label
        addLabel(renderer, node1);

        // add the second label
        addLabel(renderer, node2);

        // add the double-ended arrow
        addArrow(renderer, node1, node2, true);
      },
    },
  },
  title: {
    text: 'Node-Based Graph',
  },
  credits: {
    enabled: false,
  },
});

function App() {
  return (
    <div id="container">
      <HighchartsReact highcharts={Highcharts} options={getOptions()} />
    </div>
  );
}

export default App;

Execute yarn dev, and we see the node-based graph with a curve double-ended arrow.

Image by author

We can modify App.js to draw more nodes and arrows:

import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import HighchartsExporting from 'highcharts/modules/exporting';
import HighchartsAccessibility from 'highcharts/modules/accessibility';
HighchartsExporting(Highcharts);
HighchartsAccessibility(Highcharts);

const SHADOW_SIZE = 3;
const ARROW_LENGTH = 10;
const ARROW_ANGLE = Math.PI / 6;
const ARROW_X = ARROW_LENGTH * Math.sin(ARROW_ANGLE);
const ARROW_Y = ARROW_LENGTH * Math.cos(ARROW_ANGLE);

const nodes = [
  {
    text: 'Node 1',
    x: 350,
    y: 100,
    width: 100,
    height: 50,
  },
  {
    text: 'Node 2',
    x: 225,
    y: 300,
    width: 100,
    height: 50,
  },
  {
    text: 'Node 3',
    x: 475,
    y: 300,
    width: 100,
    height: 50,
  },
  {
    text: 'Node 4',
    x: 100,
    y: 500,
    width: 100,
    height: 50,
  },
  {
    text: 'Node 5',
    x: 350,
    y: 500,
    width: 100,
    height: 50,
  },
  {
    text: 'Node 6',
    x: 600,
    y: 500,
    width: 100,
    height: 50,
  },
];

// add an arrow that connects node1 to node2
const addArrow = (renderer, node1, node2, isDoubleEnded) => {
  const arrowStart = {
    x: node1.x + node1.width / 2 + SHADOW_SIZE,
    y: node1.y + node1.height + SHADOW_SIZE * 2,
  };

  const arrowEnd = {
    x: node2.x + node2.width / 2 + SHADOW_SIZE,
    y: node2.y - SHADOW_SIZE,
  };

  const initPath = isDoubleEnded
    ? // the beginning arrow if it is double ended
      [
        'M',
        arrowStart.x - ARROW_X,
        arrowStart.y + ARROW_Y,
        'L',
        arrowStart.x,
        arrowStart.y,
        'L',
        arrowStart.x + ARROW_X,
        arrowStart.y + ARROW_Y,
      ]
    : [];

  renderer
    .path([
      ...initPath,
      // arrow line
      'M',
      arrowStart.x,
      arrowStart.y,
      'C',
      arrowStart.x,
      (arrowStart.y + arrowEnd.y) / 2,
      arrowEnd.x,
      (arrowStart.y + arrowEnd.y) / 2,
      arrowEnd.x,
      arrowEnd.y,
      // the ending arrow
      'L',
      arrowEnd.x - ARROW_X,
      arrowEnd.y - ARROW_Y,
      'M',
      arrowEnd.x,
      arrowEnd.y,
      'L',
      arrowEnd.x + ARROW_X,
      arrowEnd.y - ARROW_Y,
    ])
    .attr({
      'stroke-width': 2,
      stroke: 'gray',
    })
    .add();
};

// add a label with specific text, position, dimension
const addLabel = (renderer, { text, x, y, width, height }) =>
  renderer
    .label(text, x, y)
    .css({
      fontWeight: 'bold',
      fontSize: '16px',
      color: 'white',
    })
    .attr({
      fill: 'blue',
      r: 5, // border radius
      'text-align': 'center',
      width,
      height,
    })
    .add()
    .shadow(true);

const getOptions = () => ({
  chart: {
    type: 'line',
    width: 800,
    height: 600,
    events: {
      load: function () {
        const chart = this;
        const renderer = chart.renderer;

        // add all nodes
        nodes.map((node) => addLabel(renderer, node));

        // add arrows
        addArrow(renderer, nodes[0], nodes[1]);
        addArrow(renderer, nodes[0], nodes[2]);
        addArrow(renderer, nodes[1], nodes[3]);
        addArrow(renderer, nodes[1], nodes[4]);
        addArrow(renderer, nodes[2], nodes[4]);
        addArrow(renderer, nodes[2], nodes[5]);
      },
    },
  },
  title: {
    text: 'Node-Based Graph',
  },
  credits: {
    enabled: false,
  },
});

function App() {
  return (
    <div id="container">
      <HighchartsReact highcharts={Highcharts} options={getOptions()} />
    </div>
  );
}

export default App;

Execute yarn dev, and we see the node-based graph with many nodes and arrows.

Image by author

The final example is located at this repository.

Conclusion

Highcharts is an enterprise-grade JavaScript charting library based on SVG. It comes with excellent documentation and innumerable examples.

In this article, we have provided step-by-step guide on Highcharts custom graphs

We have also written articles on other Highcharts types:

  • In this article: line, spline, area, areaspline, column, bar, pie, scatter, scatter3d, heatmap, treemap, and gauge.
  • In this article: bubble, packedbubble, streamgraph, and cylinder.
  • In this article: sankeydiagram, arcdiagram, dependencywheel, and networkgraph.
  • In this article: parallelCoordinates.
  • In this article: timeline.

Highcharts is free for personal/non-commercial uses, but you must pay for commercial applications. If your budget allows, Highcharts is highly recommended.

Thanks for reading.

Thanks, S Sreeram and team, for working with me on Domino products.

Want to Connect? 

If you are interested, check out my directory of web development articles.

In Plain English

Thank you for being a part of our community! Before you go:

Highcharts
Graph
React
Programming
Web Development
Recommended from ReadMedium