【Recharts】グラフの軸とテーブルの列幅を合わせる

2023年11月14日 19:03

【Recharts】グラフの軸とテーブルの列幅を合わせる

上のように、列の幅に合わせて縦軸を描くグラフ&テーブルを実装していきます!

目次
ライブラリ

ライブラリ

react@18.2.0
recharts@2.8.0
@mui/material@5.14.12

サンプルデータ確認

recharts公式ドキュメントにあるサンプルデータを使う。
ついでに型を作る。

export type Data = {
  name: string
  uv: number
  pv: number
  amt: number
}

export type Column = {
  key: string | number
}

export type Row = Data & {
  key: string | number
}

const data: Data[] = [
  {
    "name": "Page A",
    "uv": 4000,
    "pv": 2400,
    "amt": 2400
  },
  {
    "name": "Page B",
    "uv": 3000,
    "pv": 1398,
    "amt": 2210
  },
  {
    "name": "Page C",
    "uv": 2000,
    "pv": 9800,
    "amt": 2290
  },
  {
    "name": "Page D",
    "uv": 2780,
    "pv": 3908,
    "amt": 2000
  },
  {
    "name": "Page E",
    "uv": 1890,
    "pv": 4800,
    "amt": 2181
  },
  {
    "name": "Page F",
    "uv": 2390,
    "pv": 3800,
    "amt": 2500
  },
  {
    "name": "Page G",
    "uv": 3490,
    "pv": 4300,
    "amt": 2100
  }
]

データの加工

テーブルの列と行に合わせてデータを加工する。
※この例ではヘッダー列を設けています。

let originalColumns: Column[] = data.map(d => ({ key: d.name }))
originalColumns.unshift({ key: 'header' })
const [ columns, setColumns ] = useState<Column[]>(originalColumns)

let rows: Row[] = [ 'uv', 'pv', 'amt' ].map(r => {
  let row = { key: r }
  columns.forEach(c => {
    const originalData = data.find(d => d.name === c.key)
    row[c.key] = originalData ? originalData[r] : ''
  })
  return row as Row
})

テーブルを用意

// ヘッダー列のスタイル
const ColumnSticky: CSSProperties = {
  position: 'sticky',
  left: 0,
  backgroundColor: '#fff',
  zIndex: 1,
}

return (
  <>
    <TableContainer>
      <Table>
        <TableHead>
          {/* ここにグラフを描画する */}
          <TableRow>
            {columns.map(column => column.key === 'header'
              ? <TableCell
                key={column.key}
                width={160}
                style={ColumnSticky}
              ></TableCell>
              : <TableCell key={column.key}>{column.key}</TableCell>
            )}
          </TableRow>
        </TableHead>
        <TableBody>
          {rows.map(row => (
            <TableRow key={row.key}>
              {columns.map(column => column.key === 'header'
                ? <TableCell
                  component="th"
                  key={`${row.key}___${column.key}`}
                  width={160}
                  style={ColumnSticky}
                >{row.key}</TableCell>
                : <TableCell key={`${row.key}___${column.key}`}>{row[column.key]}</TableCell>
              )}
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  </>
)

列の幅を計算し、ステートを更新する

副作用フックと、ResizeObserverを使う。
verticalPointsがX軸線の位置、graphData.xが棒グラフ等の位置となる。

<Table ref={table}>も忘れずに。

const table: MutableRefObject<HTMLTableElement | null> = useRef(null)
const [ verticalPoints, setVerticalPoints ] = useState<number[]>([])
const [ graphData, setGraphData ] = useState(data)

useEffect(() => {
  const observer = new ResizeObserver((entries) => {
    entries.forEach((e) => {
      let columnWidths: number[] = Array.from(e.target.querySelector('thead tr:last-child')?.children || [])
        .map((th: HTMLElement) => th.getBoundingClientRect().width)
      const verticalPoints = columnWidths.reduce((acc, cur, idx) => {
        if(idx === 0) return acc
        const accumulated = (acc[idx - 2] || 0) + cur
        acc.push(accumulated)
        return acc
      }, [] as number[])
      setVerticalPoints(verticalPoints)
      setGraphData(graphData.map((d,i) => {
        const x = verticalPoints[i] - ( columnWidths[i + 1] / 2 )
        return {
          ...d,
          x,
        }
      }))
    })
  })

  if(table.current) observer.observe(table.current)

  return () => observer.disconnect()
}, [])

return (
  <TableContainer>
    <Table ref={table}>
      {/* 略 */}
    </Table>
  </TableContainer>
)

グラフコンポーネント

TestChartsコンポーネントの作成。
type=numberのX軸XAxisを用意し、graphData.xの位置と合わせる。

export interface Props {
  data: Data[]
  verticalPoints: number[]
}

export const TestCharts:FC<Props> = ({ data, verticalPoints }) => {
  return (
    <TableRow>
      <TableCell
        padding="none"
        style={ColumnSticky}
      >
        {/* 目盛用のグラフ */}
        <ResponsiveContainer width="100%" height={200}>
          <ComposedChart
            data={data}
            margin={{right: 0, left: 0}}
          >
            <YAxis
              type="number"
              includeHidden
              domain={['auto', 'auto']}
              orientation="right"
              mirror
            />
            <Bar hide dataKey="uv" />
            <Bar hide dataKey="pv" />
            <Line hide dataKey="amt" />
          </ComposedChart>
        </ResponsiveContainer>
      </TableCell>
      <TableCell
        colSpan={verticalPoints.length}
        padding="none"
      >
        {/* 本グラフ */}
        <ResponsiveContainer width="100%" height={200}>
          <ComposedChart
            data={data}
            barGap={5}
            margin={{right: 0, left: 0}}
          >
            <CartesianGrid
              strokeDasharray="4 1 2"
              stroke="#333"
              verticalPoints={verticalPoints}
              fillOpacity={.6}
            />
            <XAxis
              hide
              dataKey="x"
              type="number"
              domain={[0, verticalPoints[verticalPoints.length - 1]]}
            />
            <Bar dataKey="uv" barSize={20} fill="#00552e" />
            <Bar dataKey="pv" barSize={20} fill="#a03e41" />
            <Line dataKey="amt" strokeWidth={2} stroke="#33ccff" />
          </ComposedChart>
        </ResponsiveContainer>
      </TableCell>
    </TableRow>
  )
}

これを先ほどのテーブルに差し込む。

<TableContainer>
  <Table ref={table}>
    <TableHead>
      <TestCharts data={graphData} verticalPoints={verticalPoints} />
      {/* 略 */}
    </TableHead>
  </Table>
</TableContainer>

テーブルの列幅を変えても崩れない、グラフ&テーブルの完成。

まとめ

Rechartsは、APIが読みやすい。

プログラミング記事の一覧に戻る