DEV Community

Cover image for Using Apache ECharts with React and TypeScript
Maneet Goyal for Manufac Analytics Private Limited

Posted on • Edited on

Using Apache ECharts with React and TypeScript

What is Apache ECharts?

It's a cool data-visualization library like Highcharts, Chart.js, amCharts, Vega-Lite, and numerous others. A lot of companies/products including AWS are using it in production.

It supports numerous charts out-of-the-box. Here's a wide range of examples to help you out. We also found their echarts-liquidfill extension quite useful.

Different teams have varying criteria behind adopting a data visualization library. If you happen to use Apache ECharts, this feed may help you integrate it with your React + TypeScript codebase.

How to integrate with React and TypeScript?

You can implement a React functional component and reuse it in different parts of the app to avoid declaring useEffect hook and subscribing/unsubscribing to the "resize" event multiple times.

// React-ECharts.tsx

import React, { useRef, useEffect } from "react";
import { init, getInstanceByDom } from "echarts";
import type { CSSProperties } from "react";
import type { EChartsOption, ECharts, SetOptionOpts } from "echarts";

export interface ReactEChartsProps {
  option: EChartsOption;
  style?: CSSProperties;
  settings?: SetOptionOpts;
  loading?: boolean;
  theme?: "light" | "dark";
}

export function ReactECharts({
  option,
  style,
  settings,
  loading,
  theme,
}: ReactEChartsProps): JSX.Element {
  const chartRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // Initialize chart
    let chart: ECharts | undefined;
    if (chartRef.current !== null) {
      chart = init(chartRef.current, theme);
    }

    // Add chart resize listener
    // ResizeObserver is leading to a bit janky UX
    function resizeChart() {
      chart?.resize();
    }
    window.addEventListener("resize", resizeChart);

    // Return cleanup function
    return () => {
      chart?.dispose();
      window.removeEventListener("resize", resizeChart);
    };
  }, [theme]);

  useEffect(() => {
    // Update chart
    if (chartRef.current !== null) {
      const chart = getInstanceByDom(chartRef.current);
      chart.setOption(option, settings);
    }
  }, [option, settings, theme]); // Whenever theme changes we need to add option and setting due to it being deleted in cleanup function

  useEffect(() => {
    // Update chart
    if (chartRef.current !== null) {
      const chart = getInstanceByDom(chartRef.current);
      // eslint-disable-next-line @typescript-eslint/no-unused-expressions
      loading === true ? chart.showLoading() : chart.hideLoading();
    }
  }, [loading, theme]);

  return <div ref={chartRef} style={{ width: "100%", height: "100px", ...style }} />;
}

Enter fullscreen mode Exit fullscreen mode

What about echarts-for-react?

It does a similar job as the React component implemented above. But we were having trouble in making sure that the chart resizes when the window width changes. Also, at the time of writing this article, it seemed that the library may not be that actively maintained.

You can definitely try out echarts-for-react as it seems to expose more functionalities for the end user than the component implemented above.

But creating our own component eliminated the need to add an extra dependency and gave us more control into how our component should map the input props to ECharts API.

Knowing how the integration with React and TypeScript works under-the-hood, we ourselves can extend the component as and when needed instead of relying on an external library.

Clearly, there are trade-offs involved so choose whatever is more reasonable for your use cases.

How to integrate echarts-liquidfill extension?

The approach is quite similar to the component implemented above.

First, we need to specify the type definition for liquidfill charts. We are using the following:

// utils.ts

import type { DefaultLabelFormatterCallbackParams, EChartsOption } from "echarts";

/**
 * interface for LiquidFillGauge series config
 */
interface LiquidFillGaugeSeries {
  name?: string;
  type: "liquidFill";
  data: (
    | number
    | {
        name?: string;
        value: number;
        direction?: "left" | "right";
        itemStyle?: {
          color?: string;
          opacity?: number;
        };
        emphasis?: {
          itemStyle?: {
            opacity?: number;
          };
        };
      }
  )[];
  silent?: boolean;

  color?: string[];
  center?: string[];
  radius?: string;
  amplitude?: number;
  waveLength?: string | number;
  phase?: number | "auto";
  period?: number | "auto" | ((value: number, index: number) => number);
  direction?: "right" | "left";
  shape?: "circle" | "rect" | "roundRect" | "triangle" | "diamond" | "pin" | "arrow" | string;

  waveAnimation?: boolean;
  animationEasing?: string;
  animationEasingUpdate?: string;
  animationDuration?: number;
  animationDurationUpdate?: number;

  outline?: {
    show?: boolean;
    borderDistance?: number;
    itemStyle?: {
      color?: string;
      borderColor?: string;
      borderWidth?: number;
      shadowBlur?: number;
      shadowColor?: string;
    };
  };

  backgroundStyle?: {
    color?: string;
    borderWidth?: string;
    borderColor?: string;
    itemStyle?: {
      shadowBlur?: number;
      shadowColor?: string;
      opacity?: number;
    };
  };

  itemStyle?: {
    opacity?: number;
    shadowBlur?: number;
    shadowColor?: string;
  };

  label?: {
    show?: true;
    color?: string;
    insideColor?: string;
    fontSize?: number;
    fontWeight?: string;
    formatter?: string | ((params: DefaultLabelFormatterCallbackParams) => string);

    align?: "left" | "center" | "right";
    baseline?: "top" | "middle" | "bottom";
    position?: "inside" | "left" | "right" | "top" | "bottom" | string[];
  };

  emphasis?: {
    itemStyle?: {
      opacity?: number;
    };
  };
}

export interface LiquidFillGaugeOption extends Omit<EChartsOption, "series"> {
  series: LiquidFillGaugeSeries;
}
Enter fullscreen mode Exit fullscreen mode

Then, update the ReactEChartsProps:

export interface ReactEChartsProps {
  option: EChartsOption | LiquidFillGaugeOption;
  style?: CSSProperties;
  settings?: SetOptionOpts;
  loading?: boolean;
  theme?: "light" | "dark";
}
Enter fullscreen mode Exit fullscreen mode

Finally, reuse the ReactECharts component to create LiquidFillGauge component:

// LiquidFillGauge.tsx

import React from "react";
import "echarts-liquidfill";
import type { CSSProperties } from "react";
import { ReactECharts } from "../React-ECharts";
import type { LiquidFillGaugeOption } from "../utils";

export interface LiquidFillGaugeProps {
  option: LiquidFillGaugeOption;
  style?: CSSProperties;
}

export function LiquidFillGauge({ option, style }: LiquidFillGaugeProps): JSX.Element {
  return (
    <ReactECharts
      option={option}
      style={style}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

How do you call this component in an app?

Create an option object, say:

const option: ReactEChartsProps["option"] = {
    dataset: {
      source: [
        ["Commodity", "Owned", "Financed"],
        ["Commodity 1", 4, 1],
        ["Commodity 2", 2, 4],
        ["Commodity 3", 3, 6],
        ["Commodity 4", 5, 3],
      ],
    },
    tooltip: {
      trigger: "axis",
      axisPointer: {
        type: "shadow",
      },
    },
    legend: {
      data: ["Owned", "Financed"],
    },
    grid: {
      left: "10%",
      right: "0%",
      top: "20%",
      bottom: "20%",
    },
    xAxis: {
      type: "value",
    },
    yAxis: {
      type: "category",
    },
    series: [
      {
        type: "bar",
        stack: "total",
        label: {
          show: true,
        },
      },
      {
        type: "bar",
        stack: "total",
        label: {
          show: true,
        },
      },
    ],
  }
Enter fullscreen mode Exit fullscreen mode

Now, simply use it as a prop as you would in any other component:

<div>
  <ReactECharts option={option} />
</div>
Enter fullscreen mode Exit fullscreen mode

Do consider Apache Echarts if you are looking for a data visualization library for your projects.


Feel free to take a look at Using Apache ECharts with React and TypeScript: Optimizing Bundle Size in case you're trying to reduce your bundle size as well.

Top comments (14)

Collapse
 
raymolla7 profile image
raymolla7 • Edited

This is great, thank you for writing this.

I am wondering how do you get tree shaking working with the above? (Minimal Option Type in TypeScript)

I found this documentation here: echarts.apache.org/en/tutorial.htm...

but I am having trouble incorporating it into your React-ECharts.tsx

Collapse
 
maneetgoyal profile image
Maneet Goyal • Edited

Didn't try it yet @raymolla7 . But thanks for pointing out that link. Seems like tree shaking can be useful in our case too.

Collapse
 
raymolla7 profile image
raymolla7

Ok let me know if you can figure out a way to incorporate it. I tried for a bit but I had no luck :(

The bundle size of ECharts is quite large and can be drastically reduced if we use the "Minimal Option Type in TypeScript" listed in the bottom of the link

Thread Thread
 
maneetgoyal profile image
Maneet Goyal

@raymolla7 If you were still looking for some info, I hope this helps: dev.to/maneetgoyal/using-apache-ec...

Thread Thread
 
raymolla7 profile image
raymolla7

this is incredibly helpful! Thank you so much for this well written article Maneet!

Collapse
 
captaint33mo profile image
Vibhor Sharma

This is great article, thanks.

I was wondering if you could also add the onclick and other events functionality in this component as well which is given in this example: echarts.apache.org/examples/en/edi....

I want to make use of this onclick even when I click on any of the bar elements in the bar graph or any other graph.

Collapse
 
maneetgoyal profile image
Maneet Goyal

You should be able to add any click events on the chart too:

const chart = getInstanceByDom(chartRef.current);
chart.on("click", ...);
Enter fullscreen mode Exit fullscreen mode

All the methods and properties mentioned here should be accessible via this approach.

Collapse
 
huydhoang profile image
Huy

Nice work! Underrated article. Perhaps it would be easier to follow if you incorporate the example dataset in the comment into the main article :-)

Collapse
 
maneetgoyal profile image
Maneet Goyal • Edited

Thanks Huy. Have made some updates. Hope it hasn't become too complicated now. The source code changed multiple times in our private repo since the article was first written, so updated the source here too.

Collapse
 
huydhoang profile image
Huy

+1 unicorn for the edit! This has been immensely helpful in my learning of echarts. The above article was much more comprehensive and easy to follow now.

Collapse
 
dcantu96 profile image
David Cantu • Edited

Hi there. First of all great article! I'm having some trouble myself with the width of the charts not setting correctly on first load. After I adjust the browser width, it is adjusted correctly. Is anyone else having trouble with this?

Collapse
 
dcantu96 profile image
David Cantu

I also noticed that the chart.resize() returns undefined every time the resize event listener is triggered

Collapse
 
edusidsan profile image
edusidsan

How do you call this component in app? Iยดm having problmes in type declaration.

Collapse
 
maneetgoyal profile image
Maneet Goyal • Edited

Have updated the component code in the first snippet since the article was first published. Please take a look.

Here's how I am using it inside an app:

<Grid item>
  <ReactECharts option={{.......whatever options object we want to add........}} />
</Grid>
Enter fullscreen mode Exit fullscreen mode

An valid option prop can be:

 {
    dataset: {
      source: [
        ["Commodity", "Owned", "Financed"],
        ["Commodity 1", 4, 1],
        ["Commodity 2", 2, 4],
        ["Commodity 3", 3, 6],
        ["Commodity 4", 5, 3],
      ],
    },
    tooltip: {
      trigger: "axis",
      axisPointer: {
        type: "shadow",
      },
    },
    legend: {
      data: ["Owned", "Financed"],
    },
    grid: {
      left: "10%",
      right: "0%",
      top: "20%",
      bottom: "20%",
    },
    xAxis: {
      type: "value",
    },
    yAxis: {
      type: "category",
    },
    series: [
      {
        type: "bar",
        stack: "total",
        label: {
          show: true,
        },
      },
      {
        type: "bar",
        stack: "total",
        label: {
          show: true,
        },
      },
    ],
  }
Enter fullscreen mode Exit fullscreen mode