React で Highcharts で作成したグラフを PDF のプレビュー上に表示させる

Highcharts で生成される SVG の画像を React 上で PDF プレビュー上に表示させてみました。画像を表示させるだけであれば react-pdf を使って簡単に PDF を作成することができます。問題になるのは SVG をどう PDF 上に持っていくかになります。
2021.09.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Highcharts というグラフ用のライブラリを使って、グラフを生成し PDF で 上に表示させてみます。

前提としては React を使用しており、ビルドツールには Vite を使用しています。

デモ

準備

上記のテンプレートを使用します。

Highcharts

Highcharts を追加します。今回は React を使っているので highcharts-react-official も追加しています。

yarn add highcharts highcharts-react-official

Highcharts は 最終的なグラフを SVG として出力してくれます。綺麗でかつデモやドキュメントも豊富で使いやすいライブラリなのですが、商用の場合には有料なので採用を検討する場合には注意が必要です。

代替のライブラリとしては React Google ChartsRecharts などがあります。

react-pdf

react-pdf を追加します。今回は PDF を作成したいので使用するのは @react-pdf/renderer になります。

yarn add @react-pdf/renderer

ただし Vite でこれを import すると下記のようなエラーが出ます。

Uncaught ReferenceError: global is not defined

そのため vite-plugin-shim-react-pdf も使用します。

// vite.config.ts
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import shimReactPdf from "vite-plugin-shim-react-pdf";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react(), shimReactPdf()],
});

以上で準備は完了です。

Highcharts のグラフを PDF で表示させる

react-pdf には SVG をそのまま表示させることが可能です。

可能ではあるのですが、Highcharts の SVG を JSX に変換しても表示が崩れてしまいます。

そのため、Highcharts で生成される SVG を <canvas> に表示させ toDataURL() を使って PNG に変換したものを PDF 上で表示させることにします。

type Props = Readonly<
  {
    scale?: number;
  } & PropsWithoutRef<HighchartsReact.Props>
>;

export const Chart = (props: Props): JSX.Element => {
  const { scale = 3 } = props;
  const { width, height } = props.options.chart;

  const chartRef = useRef<HighchartsReact.Props["ref"]>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  return (
    <>
      <HighchartsReact ref={chartRef} {...props} />
      <canvas
        ref={canvasRef}
        width={width * scale}
        height={height * scale}
      ></canvas>
    </>
  );
};

canvas のサイズを大きくし出力する PNG のサイズを大きくすると、PDF 上で綺麗に表示されるため、ここではデフォルトを 3 倍として拡大したものを canvas 上に描画させます。

scale=1 scale=3

グラフはちょっとわかりにくいですが、文字だとscale=1のときにちょっとぼやけているのがわかるでしょうか。

あとはこのコンポーネントの useEffect 内で、SVG を PNG に変換する処理を記述するだけです。

useEffect(() => {
  const canvas = canvasRef.current;
  if (!canvas) {
    return;
  }

  const context = canvas.getContext("2d");
  if (!context) {
    return;
  }

  const svg = chartRef.current.container.current.querySelector("svg");

  const image = new Image();
  const svgData = new XMLSerializer().serializeToString(svg);
  const base64 = btoa(unescape(encodeURIComponent(svgData)));
  image.src = `data:image/svg+xml;charset=utf-8;base64,${base64}`;

  const handleLoad = () => {
    context.drawImage(image, 0, 0, width * scale, height * scale);
    setImg(canvas.toDataURL());
  };

  image.addEventListener("load", handleLoad);

  return () => {
    image.removeEventListener("load", handleLoad);
  };
}, [width, height, setImg, scale]);

以上で Highcharts のグラフを PDF 上に表示できました。