4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Intimate MergerAdvent Calendar 2023

Day 20

Rechartsの軸ラベルを見切れないようにする方法

Last updated at Posted at 2023-12-19

この記事は intimatemerger Advent Calendar 2023 の 20日目の記事です。

はじめに

はじめまして。
株式会社インティメート・マージャー開発本部でエンジニアをしています、 @april418 です。

弊社では多くのデータを扱っています。
そのデータを活かすために昨今では生成AIの活用なども行っていますが、人の目で見て判断できるようにすることも大切です。
そのため、弊社の IM-DMP という製品では、グラフなどを用いてデータを可視化する機能を提供しています。

IM-DMP では React ベースのグラフ表示ライブラリ Recharts を使用しています。
レスポンシブ対応や幅広い種類のグラフに対応しているなど、非常に素晴らしいライブラリです。

しかし、日本語のラベルだと自動改行がうまくいかず、ラベルが見切れてしまう問題が発生しました。
これを解決していきたいというのが今回の内容になります。

それでは本題に行ってみましょう!

TL;DR

  • ラベルの文字数からざっくりラベルの要素サイズを算出
    • 全角と半角を考慮に入れるのがミソ
    • フォントによって実際のサイズとはズレがあるのでそのあたりは調整が必要かも
  • 計算したサイズを XAxis YAxis に設定

問題の事象

長い文字列がラベルとして入ってきた際に以下のようになってしまいます。

image.png

縦棒グラフだともっとひどいことに…。

image.png

解決法

上記の対策を施して出来たサンプルがこちらになります。
(CodePen にて実装しています)

こんな感じでフォントサイズを変更してもラベルが見切れないようになっています。

image.png

image.png

横棒グラフでも大丈夫です。

image.png

実装のポイント

ラベルのサイズを計算する

Recharts の軸ラベルは XAxis YAxis というコンテナと、 その中の Tick というラベルで構成されています。

本来ラベルのサイズを計算するには、レンダリングされた後にラベルのサイズを取得するのが最も正確です。
ですが、この方法は一度レンダリングした後にコンテナサイズを再計算して再レンダリングを行うため、パフォーマンス的な懸念があります。

そのため、今回はラベルの文字列の長さから簡易的なラベルサイズを割り出し、そのサイズをコンテナに設定することにしました。

初めは 文字列の長さ * フォントサイズ のような形で計算していたのですが、当然全角文字と半角文字でかなりの誤差が生じてしまうので、正規表現で全角半角を判定して半角文字なら半分のサイズで計算するようにしました。

/**
 * 半角文字かどうかを判定します
 * @param {string} char - 判定する文字
 * @return {boolean} 半角文字の場合はtrue、それ以外の場合はfalse
 */
const isHankaku = (char: string) => {
  return /[\x01-\x7E]|[\uFF65-\uFF9F]/.test(char);
};

/**
 * 全角文字かどうかを判定します
 * @param {string} char - 判定する文字
 * @return {boolean} 全角文字の場合はtrue、それ以外の場合はfalse
 */
const isZenkaku = (char: string) => {
  return !isHankaku(char);
};

/**
 * 文字列中の半角文字の数をカウントします
 * @param {string} str - カウントする文字列
 * @return {number} 半角文字の数
 */
const countHankaku = (str: string) => {
  return [...str].filter(isHankaku).length;
};

/**
 * 文字列中の全角文字の数をカウントします
 * @param {string} str - カウントする文字列
 * @return {number} 全角文字の数
 */
const countZenkaku = (str: string) => {
  return [...str].filter(isZenkaku).length;
};

/**
 * 文字列の長さを計算します
 * @param {string} str - 長さを計算する文字列
 * @param {number} charLen - 全角文字の長さ(デフォルトは12)
 * @return {number} 計算された文字列の長さ
 */
const calclateStringLength = (str: string, charLen = 12) => {
  return (countHankaku(str) / 2 + countZenkaku(str)) * charLen;
};

グラフのレイアウトに応じて計算したラベルのサイズを設定する

カテゴリが縦に並ぶレイアウト ( layout="vertical" ) の場合はコンテナ要素である YAxiswidth に先ほど計算した文字列の長さを設定するだけです。

しかし、カテゴリが横に並ぶレイアウト ( layout="horizontal" ) の場合はラベルが長すぎて他のラベルと被ってしまいます。
そのため、 XAxis の tick にカスタムコンポーネントを設定してラベルを縦書きに変更し、コンテナ要素の XAxisheight に計算した文字列の長さを設定しています。

( 見た目を整えるのに Material UI を併せて使用しています )

/**
 * 棒グラフの軸ラベルのプロパティ
 */
type CustomBarChartTickProps = {
  x?: number;
  y?: number;
  stroke?: string;
  fontSize?: number;
  writingMode?: "horizontal-tb" | "vertical-rl" | "vertical-lr";
  payload?: any;
};

/**
 * 棒グラフの軸ラベル
 * 縦書きにしたいので writingMode を設定できるようにしています
 */
const CustomBarChartTick: React.FC<CustomBarChartTickProps> = (props) => {
  const { x, y, stroke, fontSize, writingMode, payload } = props;
  const { value } = payload;

  return (
    <text
      x={x}
      y={y}
      fill={stroke}
      fontSize={fontSize}
      writingMode={writingMode}
    >
      <tspan>{value}</tspan>
    </text>
  );
};

/**
 * 棒グラフのデータ型
 */
type CustomBarChartData = {
  name: string;
  uv: number;
  pv: number;
  amt: number;
};

/**
 * 棒グラフのプロパティ
 */
type CustomBarChartProps = {
  layout: LayoutType;
  height: number;
  data: CustomBarChartData[];
  axisFontSize?: number;
};

/**
 * フォントサイズに応じてラベルのコンテナサイズを調整できる棒グラフ
 */
const CustomBarChart: React.FC<CustomBarChartProps> = (props) => {
  const theme = useTheme();
  const { layout, height, data, axisFontSize } = props;
  const axisSize = Math.max(
    ...data.map((d) => calclateStringLength(d.name, axisFontSize))
  );

  return (
    <ResponsiveContainer width="100%" height={height}>
      <BarChart
        layout={layout}
        data={data}
        margin={{
          top: 5,
          right: 30,
          left: 20,
          bottom: 5
        }}
      >
        <CartesianGrid stroke={theme.palette.grey[300]} strokeDasharray="3 3" />
        {layout === "horizontal" ? (
          <>
            <XAxis
              type="category"
              dataKey="name"
              interval={0}
              height={axisSize}
              tick={
                <CustomBarChartTick
                  stroke={theme.palette.text.primary}
                  fontSize={axisFontSize}
                  writingMode="vertical-rl"
                />
              }
            />
            <YAxis
              type="number"
              tick={{
                fontSize: axisFontSize,
                fill: theme.palette.text.primary
              }}
            />
          </>
        ) : (
          <>
            <XAxis
              type="number"
              tick={{
                fontSize: axisFontSize,
                fill: theme.palette.text.primary
              }}
            />
            <YAxis
              type="category"
              dataKey="name"
              interval={0}
              width={axisSize}
              tick={{
                fontSize: axisFontSize,
                fill: theme.palette.text.primary
              }}
            />
          </>
        )}
        <Tooltip />
        <Legend />
        <Bar dataKey="pv" fill={indigo[700]} />
        <Bar dataKey="uv" fill={amber[700]} />
      </BarChart>
    </ResponsiveContainer>
  );
};

おわりに

読んでいただきありがとうございました。
あまりキレイな実装方法ではないですが、皆さんの参考になりましたら幸いです。

次回は @Jumons さんの「GCPの権限昇格の話」です! お楽しみに!

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?