svg chart 最終日は Bar Chart です。昨日の実装に似ていますが、マルチレコード対応しています。Chart にマウスオーバーすると、Chart下部に該当レコードの複数値が表示されます。
code: github / $ yarn 1218
カラム幅に収まる様に、項目数から単レコード幅(Barの幅)を算出します。
Record
まずは Record 定義です。サンプルでは mock 生成関数を利用しています。マルチレコードのため、コードではRecord[][]
を利用します。
type Record = {
dateLabel: string
amount: number
}
点座標算出関数
Records と描画領域を突合し、点座標を算出します。daily record を表す chart なので、日付け毎の点座標・情報を確保。列幅は統一なので、別で確保します。
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
はマウスオーバーによって状態更新されます。
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 で利用します。
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 を作ります。
import { createContext } from 'react'
import { useBarChart } from './useBarChart'
export const BarChartContext = createContext(
{} as ReturnType<typeof useBarChart>
)
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 表示のトリガーになります。
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 単位で配置する処理です。日毎に与えらた幅で、表示位置を表示します。
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]
)
}
日付単位で表示する仕様なので、横一列にヒットエリアを配置します。
export default () => {
const {
hitareaBounds,
chartBound,
handleEnterHitarea
} = useContext(BarChartContext)
return useMemo(
() => (
<StyledView
hitareaBounds={hitareaBounds}
chartBound={chartBound}
onMouseEnter={handleEnterHitarea}
/>
),
[hitareaBounds, chartBound]
)
}
要件微調整が可能な点が、手書き Chart のアドバンテージですね。曲線による Chart を上に被せることも可能なので、挑戦してみてください。