Enabling Apache ECharts in React for Data Visualization

Making dashboards with charts and graphs is a pretty common part of the front-end developer experience, as well as deciding which JavaScript library to use for data visualization. As you explore all the different options - Recharts, Visx, D3 - you'll see they all do something you want, and lack something you need.

For a while I was using Nivo, a really impressive React component library, but it didn't have some features I needed like drilling down from one graph type into another, so I started looking into Apache ECharts.

This library is open-source, awesome, and incredibly easy to use. However, as of writing, the React component wrapper (echarts-for-react) that someone made is pretty out of date - last updated more than three years ago, and relies on an old version of ECharts that didn't have all the features I was looking for, so I made a tiny little wrapper that can just be imported as a component in a single file..

Demo

ECharts works by creating an option object that holds all the configuration about the graph. That makes it incredibly easy to work with, because it doesn't require separate components for each graph type.

App.js
Copy
import { EChart } from './EChart'

export const App = () => {
  const option = {
    // ... chart configuration goes here
  }

  return <EChart option={option} />
}

Here's a little demo importing the default Line, Bar and Pie type charts.

Installation

Just install the echarts dependency and you're good to go.

Copy
npm i echarts

Using ECharts

With just a few methods from the API, you can get this up and running.

ECharts

First you'll need to create a div and use that ref to inject the ECharts instance, then you can initialize the echartInstance with init(). For as long as it exists, you can access the instanced with getInstanceByDom(). When you're done, you can disconnect() from the instance.

Function Description
init Creates an ECharts instance
getInstanceByDom Returns chart instance of dom container
disconnect Disconnects interaction of multiple chart series.

EChart Instance

Once you have an echartsInstance in the DOM, you can use setOption to update the configuration. We'll be able to update this with useEffect in React. When you're done, you can destroy the instance with dispose()

Function Description
setOption Configuration item, data, universal interface, all parameters and data can all be modified through setOption
dispose Destroys chart instance, after which the instance cannot be used any more

<EChart> Component

To set up the basic Echart component, you can create a ref on an element and initialize the chart on mount using init(). All the configuration of echarts is set in setOption, so whatever option you pass into the component will initialize the chart.

Everything you need to get set up can be handled in these two useEffects.

EChart.js
Copy
import React, { useMemo, useRef, useEffect } from 'react'
import { init, getInstanceByDom } from 'echarts'

export const EChart = ({
  option,
  chartSettings,
  optionSettings,
  style = { width: '100%', height: '350px' },
  ...props
}) => {
  const chartRef = useRef(null)

  useEffect(() => {
    // Initialize chart
    const chart = init(chartRef.current, null, chartSettings)

    return () => {
      chart?.dispose()
    }
  }, [])

  useEffect(() => {
    // Re-render chart when option changes
    const chart = getInstanceByDom(chartRef.current)

    chart.setOption(option, optionSettings)
  }, [option])

  return <div ref={chartRef} style={style} {...props} />
}

Now you'll have an element that contains the chart and re-renders if the options change.

Events

The component is pretty basic right now, and there's a few things you'll probably want to add. You can create an event handler to pass a click event into the chart like so:

Copy
<EChart
  option={option}
  events={{
    click: () => {
      console.log('Click handler!')
    },
  }}
/>

This enables any type of event and handler you want to add.

Copy
import React, { useMemo, useRef, useEffect } from 'react'
import { init, getInstanceByDom } from 'echarts'

export const EChart = ({
  option,
  chartSettings
  optionSettings
  style = { width: '100%', height: '350px' },
  events = {},  ...props
}) => {
  const chartRef = useRef(null)

  useEffect(() => {
    // Initialize chart
    const chart = init(chartRef.current, null, chartSettings)

    // Set up event listeners    for (const [key, handler] of Object.entries(events)) {      chart.on(key, (param) => {        handler(param)      })    }
    // Return cleanup function
    return () => {
      chart?.dispose()
    }
  }, [])

  useEffect(() => {
    // Re-render chart when option changes
    const chart = getInstanceByDom(chartRef.current)

    chart.setOption(option, optionSettings)
  }, [option])

  return <div ref={chartRef} style={style} {...props} />
}

Resizing

You also might want the chart to resize if the screen changes, or if the layout changes, such as opening/closing a sidebar.

I created a debounced event with Lodash debounce() and useMemo(), which will be debounced 50 milliseconds. This will help performance so the chart doesn't constant run resize() during a resize event, but attempts to do it only once.

Copy
const resizeChart = useMemo(
  () =>
    debounce(() => {
      if (chartRef.current) {
        const chart = getInstanceByDom(chartRef.current)
        chart.resize()
      }
    }, 50),
  []
)

I used the ResizeObserver() WebAPI to determine when to resize in the useEffect.

Copy
const resizeObserver = new ResizeObserver(() => {
  resizeChart()
})

resizeObserver.observe(chartRef.current)

Now the EChart component is complete - it will resize on window or layout change, pass events through, and update the graph based on the option configuration passed in.

EChart.js
Copy
import React, { useMemo, useRef, useEffect } from 'react'
import { init, getInstanceByDom } from 'echarts'
import { debounce } from 'lodash'
export const EChart = ({
  option,
  chartSettings,
  optionSettings,
  style = { width: '100%', height: '350px' },
  events = {},
  ...props
}) => {
  const chartRef = useRef(null)

  // Debounce resize event so it only fires periodically instead of constantly  const resizeChart = useMemo(    () =>      debounce(() => {        if (chartRef.current) {          const chart = getInstanceByDom(chartRef.current)          chart.resize()        }      }, 50),    []  )
  useEffect(() => {
    // Initialize chart
    const chart = init(chartRef.current, null, chartSettings)

    // Set up event listeners
    for (const [key, handler] of Object.entries(events)) {
      chart.on(key, (param) => {
        handler(param)
      })
    }

    // Resize event listener    const resizeObserver = new ResizeObserver(() => {      resizeChart()    })    resizeObserver.observe(chartRef.current)
    // Return cleanup function
    return () => {
      chart?.dispose()

      if (chartRef.current) {        resizeObserver.unobserve(chartRef.current)      }      resizeObserver.disconnect()    }
  }, [])

  useEffect(() => {
    // Re-render chart when option changes
    const chart = getInstanceByDom(chartRef.current)

    chart.setOption(option, optionSettings)
  }, [option])

  return <div ref={chartRef} style={style} {...props} />
}

Conclusion

Setting up the EChart component was a good exercise in creating my own simple wrapper for a JavaScript library without relying on the third-party React implementation. Supposedly, other frameworks like Svelte make it a lot easier to implement third party JavaScript libraries without making them "React-specific, but I work primarily in React, so it's a useful skill to have.

You can view the Demo and source code on CodeSandbox, which uses the completed EChart component to display a line, bar, and pie chart.

Comments