LoginSignup
1
0

More than 5 years have passed since last update.

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 を上に被せることも可能なので、挑戦してみてください。

1
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
1
0