Using Apache ECharts with React and TypeScript: Regression Transform

maneetgoyal

Maneet Goyal

Posted on February 27, 2022

Using Apache ECharts with React and TypeScript: Regression Transform

In one of our previous articles, Using Apache ECharts with React and TypeScript: Using Aggregate Transform, we talked about a few data transformation functionalities available in the ECharts ecosystem.

echarts-stat happens to be a very popular ECharts plugin useful in performing a wide range of data transformations like regression, histogram binning, k-means clustering, etc. However, it doesn't seem to be on an active development cycle and is currently suffering from at least 1 TS compatibility issue.

In essence, this plugin is written in JS and has a .d.ts file to expose the type information. Unfortunately, the type definition file doesn't export any of the transforms. Consequently, our TSC starts complaining when we try to import any of the transforms.

Turns out that module augmentation is a neat solution to this missing types problem. Here's what we ended up doing to fix the missing types/modules error:

// some-name.d.ts

import type { ExternalDataTransform } from "@manufac/echarts-simple-transform";

/**
 * Needed because of: https://github.com/ecomfe/echarts-stat/issues/35
 * Module augmentation: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
 */
declare module "echarts-stat" {
  let transform: {
    regression: ExternalDataTransform;
    histogram: ExternalDataTransform;
    clustering: ExternalDataTransform;
  };
}
Enter fullscreen mode Exit fullscreen mode

You may obtain more information on @manufac/echarts-simple-transform in Using Apache ECharts with React and TypeScript: Using Aggregate Transform.


Okay, now how to use echarts-stat with TypeScript and React?

Since the TSC errors are now resolved by the addition of the declaration file (as described above) in our project, we can safely import "transform" from "echarts-stat". Here's the full recipe:

import { transform } from "echarts-stat";
import { ScatterChart, LineChart } from "echarts/charts";
import { TransformComponent } from "echarts/components";
import { init, getInstanceByDom, use, registerTransform } from "echarts/core";
import { useRef, useEffect } from "react";
import type { ScatterSeriesOption, LineSeriesOption } from "echarts/charts";
import type { ECharts, ComposeOption } from "echarts/core";

// Register the required components
use([
  TitleComponent,
  TooltipComponent,
  GridComponent,
  DatasetComponent,
  LegendComponent,
  ToolboxComponent,
  CanvasRenderer,
  ScatterChart,
  LineChart, // In order to plot regression lines
  TransformComponent, // Built-in transform (filter, sort)
]);

registerTransform(transform.regression); // No missing module error due to module augmentation as done above

// As per docs: https://echarts.apache.org/handbook/en/basics/import/#minimal-option-type-in-typescript
export interface ScatterPlotChartProps extends BaseEChartsProps {
  option: ComposeOption<TitleComponentOption | TooltipComponentOption | GridComponentOption | DatasetComponentOption | ScatterSeriesOption | LineSeriesOption>;
}

export function ScatterPlotChart({
  option,
  style,
  settings,
  loading,
  theme,
}: ScatterPlotChartProps): 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]);

  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

And, how does an option object look like?

The entire props object is shown below which can be passed to the ScatterPlotChart component as <ScatterPlotChart {...props} />.

const props = {
  option: {
    dataset: [
      {
        source: [[1, 2], [4, 7], [2, 6], [3, 8], ...]
      },
      {
        transform: {
          type: "ecStat:regression",
          config: { method: "polynomial", order: 3 },
        },
      },
    ],
    grid: {
      containLabel: true,
    },
    title: {
      text: "Scatter-Regression Chart",
      left: "center",
    },
    legend: {
      bottom: 5,
    },
    tooltip: {
      trigger: "item",
    },
    xAxis: {
      name: "Year of Experience",
    },
    yAxis: {
      name: "Salary",
    },
    series: [
      {
        name: "Experience-Salary",
        type: "scatter",
      },
      {
        name: "Cubic Polynomial",
        type: "line",
        datasetIndex: 1,
        symbol: "none",
        // to show regression formule
        endLabel: {
          show: true,
          distance: -200, // distance of endLabel from the grid
          formatter: (params) => {
            const datum = params.data as (string | number)[];
            return datum[2].toString();
          },
        },
      },
    ],
  },
  style: {
    height: "300px",
  },
}
Enter fullscreen mode Exit fullscreen mode

Parting Notes

  • If the use of use(...) function and ComposeOption<...> generic type seems weird, you should go through our older article, Using Apache ECharts with React and TypeScript: Optimizing Bundle Size, to learn more about it. TLDR: they are a way to optimize the bundle size.

  • This plugin doesn't export regression coefficient value (R^2) which seems to be an important missing feature. Nevertheless, the plugin looks pretty comprehensive in that it supports so many kinds of data transformations. For instance, even within just the regression transform, it supports linear, logarithmic, exponential, and polynomial (nth order) flavors.

  • It would have been way nicer if this plugin (echarts-stat) was more actively maintained though.


Thanks for reading. Feel free to share your views and suggestions. 📚 📖

💖 💪 🙅 🚩
maneetgoyal
Maneet Goyal

Posted on February 27, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related