DEV Community

Cover image for Using Apache ECharts with ReactJS and TypeScript: Server Side Rendering (SSR)

Using Apache ECharts with ReactJS and TypeScript: Server Side Rendering (SSR)

What is SSR?

BairesDev explains this well: Server Side Rendering (SSR) is a paradigm under which we render web pages on the server before sending them to the client.

It has both pros and cons, but some use cases may well justify its adoption. For instance, better search engine indexability could be a huge selling point for SSR for some people.

Broadly speaking, whatever aligns well with your end objectives (CSR, SSR, or a hybrid approach) is fine. However, in this article, we mainly explore how to render charts using Apache ECharts via SSR.

How does Apache ECharts help us in building SSR apps?

As a charting library, it offers built-in support for both CSR and SSR modes.

The visualization instances produced by Apache ECharts can easily be converted to PNG data URLs on the server side. Further, it exposes an isomorphic API to allow converting these instances to SVG strings as well.

Subsequently, these image strings can be rendered in the browser using the <img> element, allowing for server-side rendering (SSR) capability.

The following discussion focuses on building ReactJS SSR applications using NextJS but this approach should work as a blueprint for other frameworks too.


Before we dive into the SSR way of using ECharts, here's a quick refresher on the CSR approach.

CSR Snippets

If you pick the Canvas Renderer, here's how a CSR Scatter Plot component may look like. If the following syntax feels alien, you may want to go through two other articles in this series first: this and this.

// Import necessary modules and types from "echarts"
import { ... } from "echarts/...";
import { CanvasRenderer } from "echarts/renderers";
import { useRef, useEffect } from "react";

use([
  ...,
  ...,
  CanvasRenderer,
]);

export interface Props {
  theme?: "light" | "dark";
  data: Record<string, number>[];
}

export function CanvasRendererScatterPlot({
  theme,
  data,
}: Props): JSX.Element {
  const chartRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const chart = init(chartRef.current, theme);

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

  useEffect(() => {
    if (chartRef.current !== null) {
      const chart = getInstanceByDom(chartRef.current);
      const option = {...};
      chart?.setOption(option, true);
    }
  }, [data, theme]);

  return (
    <div
      ref={chartRef}
      style={...}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

But if you want to pick the SVG Renderer instead, the code is mostly the same except that instead of using CanvasRenderer, we will need to use SVGRenderer.

Now, moving onto the SSR approach:

SSR Snippets

A. Canvas

import { ... } from "echarts/...";
import { CanvasRenderer } from "echarts/renderers";
import { Canvas, createCanvas } from "canvas";

use([
  ...,
  ...,
  CanvasRenderer,
]);

export function getCanvasScatterPlotServerSideProps(points: number): Canvas {
  const canvas = createCanvas(400, 500);
  const chart = init(canvas as unknown as HTMLCanvasElement);
  const option = {
    ...,
    series: {
      type: "scatter",
      encode: {
        x: "x",
        y: "y",
      },
      progressiveThreshold: points + 1,
    },
    ...
  };
  chart.setOption(option);

  return canvas;
}

Enter fullscreen mode Exit fullscreen mode

The dataURL can be generated from the Canvas instance via canvas.toDataURL(). This data URL can be sent to the client which can then render the chart image as follows:

import Image from "next/image";

export function SSRCanvasRendererScatterPlot({ url }: { url: string }): JSX.Element {
  return (
    <div style={...}>
      <Image src={url} width={400} height={500} alt="SSR canvas scatter plot" />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

B. SVG

import { ... } from "echarts/...";
import { SVGRenderer } from "echarts/renderers";

use([
  ...,
  ...,
  SVGRenderer,
]);


export function getSVGScatterPlotServerSideProps(points: number): string {
  const chart = init(null as unknown as HTMLElement, undefined, {
    renderer: "svg",
    ssr: true,
    width: 400,
    height: 500,
  });

  const option = {
    ...,
    animation: false,
    series: {
      type: "scatter",
      encode: {
        x: "x",
        y: "y",
      },
      progressiveThreshold: points + 1,
    },
    ...
  };

  chart.setOption(option);
  const chartData = chart.renderToSVGString();
  chart.dispose();
  return chartData;
}
Enter fullscreen mode Exit fullscreen mode

Similar to Canvas, the SVG string can be sent to the client where it can be rendered as follows:

import Image from "next/image";

export function SSRSVGRendererScatterPlot({
  svgDataString,
}: {
  svgDataString: string;
}): JSX.Element {
  return (
    <div style={{ ... }}>
      <Image
        src={`data:image/svg+xml;utf8,${encodeURIComponent(svgDataString)}`}
        width={400}
        height={500}
        alt="SSR SVG scatter plot"
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now that we know how to generate charts on server-side, we simply need a mechanism to fetch that data on the client-side for final rendering. The API Routes approach is showcased below. However, the Server Components approach should work fine as well.

// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { getCanvasScatterPlotServerSideProps } from "@/components/SSRCanvasRendererScatterPlot";
import { getSVGScatterPlotServerSideProps } from "@/components/SSRSVGRendererScatterplot";
import { SSRScatterPlotData } from "@/components/utils";
import type { NextApiRequest, NextApiResponse } from "next";

export default function handler(req: NextApiRequest, res: NextApiResponse<SSRScatterPlotData>) {
  const { points } = req.query;
  let ssrData: SSRScatterPlotData = { svgDataString: "", canvasDataURL: "" };
  ...
  ...
  const canvas = getCanvasScatterPlotServerSideProps(...);
  const svgDataString = getSVGScatterPlotServerSideProps(...);
  ssrData = { svgDataString, canvasDataURL: canvas.toDataURL() };
  res.status(200).json(ssrData);
}
Enter fullscreen mode Exit fullscreen mode

And then, we can consume those image strings as shown below:


export const getServerSideData = async (...) => {
  ...
  const rawRes = await fetch(`/api/...`, {
    method: "GET",
  });
  const { canvasDataURL, svgDataString } = (await rawRes.json());
  return {
    canvasDataURL,
    svgDataString,
  };
};

...
...

const [ssrData, setSSRData] = useState();

useEffect(() => {
  ...
  getServerSideData(...)
    .then((res) => {
      setSSRData(res);
    })
    .catch(console.log);
  setData(scatterPlotData);
}, [value]);

...
...

<Grid>
  <Grid.Col span={6} h="50vh">
    {ssrData?.canvasDataURL === undefined ? null : (
      <SSRCanvasRendererScatterPlot url={ssrData.canvasDataURL} />
    )}
  </Grid.Col>
  <Grid.Col span={6} h="50vh">
    {ssrData?.svgDataString === undefined ? null : (
      <SSRSVGRendererScatterPlot svgDataString={ssrData.svgDataString} />
    )}
  </Grid.Col>
</Grid>
Enter fullscreen mode Exit fullscreen mode

Parting Notes

  1. Generating charts on server-side via ECharts is achievable, thanks to the in-built support that ECharts provide.
  2. The approach involves creating a PNG or SVG string on the server side and then rendering that string on client side using the <img> element's src attribute.
  3. Although ECharts provide support for animation in server-side rendered charts by embedding CSS animations in the output SVG string, those animations can still feel limited as compared to JS animations.
  4. While rendering a scatter plot on server-side via SVG renderer, we observed that all the points were getting rendered in 1 corner instead of their expected places. Disabling the animation fixed the issue.
  5. Another thing that made us scratched our heads for a bit was the progressive rendering feature that ECharts provides.

Since ECharts 4, "progressive rendering" is supported in its workflow, which processes and renders data chunk by chunk alone with each frame, avoiding to block the UI thread of the browser.

Although it can be desirable in CSR cases, for SSR it understandably led to partial rendering of the chart whenever the data exceeded the progressiveThreshold. We resolved this issue by always setting the progressiveThreshold to value greater than the size of the data so that progressive rendering doesn't trigger.

Top comments (0)