LoginSignup
0

More than 3 years have passed since last update.

posted at

updated at

Organization

Bar Chart

svg chart 最終日は Bar Chart です。昨日の実装に似ていますが、マルチレコード対応しています。Chart にマウスオーバーすると、Chart下部に該当レコードの複数値が表示されます。
code: github / $ yarn 1218

1218.jpg

カラム幅に収まる様に、項目数から単レコード幅(Barの幅)を算出します。

Record

まずは Record 定義です。サンプルでは mock 生成関数を利用しています。マルチレコードのため、コードではRecord[][] を利用します。

components/records.ts
type Record = {
  dateLabel: string
  amount: number
}

点座標算出関数

Records と描画領域を突合し、点座標を算出します。daily record を表す chart なので、日付け毎の点座標・情報を確保。列幅は統一なので、別で確保します。

components/chartSrc.ts
const colors = [styles.blue, styles.tarcoize, styles.green]

const getChartSrc = (
  records: Record[],
  bound: Bound,
  index: number,
  total: number
) => {
  const count = records.length
  const max = Math.max.apply(
    Math,
    records.map(record => record.amount)
  )
  const startY = bound.height + bound.y
  const chartPoints = records.map((record, _index) => {
    const i = _index + 1
    const x = (bound.width / count) * i + bound.x
    const y = startY - (bound.height * record.amount) / max
    return {
      x,
      y,
      amount: record.amount,
      dateLabel: record.dateLabel,
      color: colors[index]
    }
  })
  const columnWidth = bound.width / count
  const chartColumnWidth = columnWidth / (total + 1)
  return {
    chartPoints,
    chartColumnWidth
  }
}

const getMultipleChartSrc = (
  multipleRecords: Record[][],
  bound: Bound
) => {
  const total = multipleRecords.length
  return multipleRecords.map((records, index) => {
    return getChartSrc(records, bound, index, total)
  })
}

useBarChart

今回の Custom Hooks です。currentColumn currentData はマウスオーバーによって状態更新されます。

components/useBarChart.ts
const useBarChart = (props: Props) => {
  const padding = useMemo(...)
  const svgRect = useMemo(...)
  const chartBound = useMemo(...)
  const multipleChartSrc = useMemo(...)
  const chartPoints = useMemo(...)
  const chartColumnWidth = useMemo(...)
  const bgRowLinesPoints = useMemo(...)
  const hitareaBounds = useMemo(...)
  const [currentColumn, updateCurrentColumn] = useState<null | number>(null)
  const [currentData, updateCurrentData] = useState<(Point | null)[]>([null])
  const handleEnterHitarea = useCallback(...)
  const handleLeaveHitarea = useCallback(...)
  useEffect(...)
  return {
    multipleRecords: props.multipleRecords,
    padding,
    svgRect,
    chartBound,
    chartPoints,
    chartColumnWidth,
    bgRowLinesPoints,
    hitareaBounds,
    currentColumn,
    currentData,
    handleEnterHitarea,
    handleLeaveHitarea
  }
}

先に用意した純関数を、useMemo で利用します。

components/useBarChart.ts
const multipleChartSrc = useMemo(
  () => {
    return getMultipleChartSrc(
      props.multipleRecords,
      chartBound
    )
  },
  [props.multipleRecords, chartBound]
)
const chartPoints = useMemo(
  () => multipleChartSrc.map(src => src.chartPoints),
  [multipleChartSrc]
)
const chartColumnWidth = useMemo(
  () =>
    multipleChartSrc.map(src => src.chartColumnWidth)[0],
  [multipleChartSrc]
)

Context Hooks

先に定義した Custom Hooks をもって Provider を作ります。

components/contexts.ts
import { createContext } from 'react'
import { useBarChart } from './useBarChart'

export const BarChartContext = createContext(
  {} as ReturnType<typeof useBarChart>
)
components/provider.tsx
export default (props: Props) => {
  const value = useBarChart({
    rowCount: props.rowCount,
    columnCount: props.columnCount,
    max: props.max,
    width: props.width,
    height: props.height,
    multipleRecords: props.multipleRecords
  })
  return (
    <BarChartContext.Provider value={value}>
      {props.children}
    </BarChartContext.Provider>
  )
}

要素への分配

Chart コンポーネントが svg タグを含む wrapper です。RecordsHitArea は透明な矩形を横一列に配置しています。これがマウスイベントを listen し、該当日の MultiRecord 表示のトリガーになります。

components/index.tsx
const View = (props: Props) => {
  const count = 2
  const columnCount = 28
  const rowCount = 20
  const max = 1000
  const width = 640
  const height = 256
  const multipleRecords = mockMultipleRecords(
    count,
    columnCount,
    max
  )
  return (
    <Provider
      rowCount={rowCount}
      columnCount={columnCount}
      max={max}
      width={width}
      height={height}
      multipleRecords={multipleRecords}
    >
      <div className={props.className}>
        <Chart>
          <BgColumnRect />
          <BgColumnLines />
          <BgRowLines />
          <RecordsBars />
          <HitAreas />
          <BaseLines />
        </Chart>
        <Info />
      </div>
    </Provider>
  )
}

複数 Bar を Column 単位で配置する処理です。日毎に与えらた幅で、表示位置を表示します。

components/chart/recordsBars.tsx
export default () => {
  const {
    padding,
    chartPoints,
    chartBound,
    chartColumnWidth
  } = useContext(BarChartContext)
  return useMemo(
    () => (
      <>
        {chartPoints
          .map((points, index) => (
            <StyledView
              key={index}
              chartPoints={points}
              chartBound={chartBound}
              chartColumnWidth={chartColumnWidth}
              offsetX={ // here
                chartColumnWidth * -chartPoints.length -
                chartColumnWidth * 0.5 +
                index * chartColumnWidth
              }
              offsetY={padding.top}
            />
          ))
          .reverse()}
      </>
    ),
    [padding, chartPoints, chartBound, chartColumnWidth]
  )
}

日付単位で表示する仕様なので、横一列にヒットエリアを配置します。

components/chart/hitAreas.tsx
export default () => {
  const {
    hitareaBounds,
    chartBound,
    handleEnterHitarea
  } = useContext(BarChartContext)
  return useMemo(
    () => (
      <StyledView
        hitareaBounds={hitareaBounds}
        chartBound={chartBound}
        onMouseEnter={handleEnterHitarea}
      />
    ),
    [hitareaBounds, chartBound]
  )
}

要件微調整が可能な点が、手書き Chart のアドバンテージですね。曲線による Chart を上に被せることも可能なので、挑戦してみてください。

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
What you can do with signing up
0