@aptpod/data-viz-create-visual-parts-react
Version:
template of npm project with typescript
250 lines (222 loc) • 7.41 kB
text/typescript
import {
ViewGrid,
ViewBox,
PlaybackTimeSelectedRange,
DataSpecification,
Value,
} from '@aptpod/data-viz-visual-parts-sdk'
import { ComponentProps, useMemo, useCallback, useEffect } from 'react'
import { BIND_DATA_COUNT_MAX } from './constant'
import { LineGraph } from './component'
import { Extension } from './extension'
import {
shouldContentFit,
buildColorWithOpacity,
arrayIncludes,
arraySearchIndexLe,
arraySearchIndexGe,
} from './utils'
import { buildEmptyArray, selectNonNullables } from 'src/utils/array'
import { memoize } from 'src/utils/memoize'
/**
* LineGraph ComponentのPropsの型
*/
type LineGraphProps = ComponentProps<typeof LineGraph>
type LineGraphPropsValue = LineGraphProps['values'][0]
/**
* DataSpecificationで使用するフィールドの型のみで再定義
*/
type UsedDataSpecification = Pick<
DataSpecification,
| 'id'
| 'rangeMin'
| 'rangeMax'
| 'dataName'
| 'decimalDigits'
| 'colorCode'
| 'colorOpacity'
| 'colorFill'
>
/**
* Valueで使用するフィールドの型のみで再定義
*/
type UsedValue = Pick<Value, 'data'>
/**
* ValueのData型からUNIX Timestampのフィールドのみで型を再定義
*/
type UsedValuePickedTime = Pick<UsedValue['data'][0], 't'>
/**
* LineGraph ComponentのPropsに変換します。
* メモ化のため、useMemo、memoize を使用しています。
* memoizeは、可変引数、loop内で使用するfunctionのメモ化で使用しています。
*/
export const useSelectProps = (params: {
viewGrid: ViewGrid
viewBox: ViewBox
playbackTimeSelectedRange: PlaybackTimeSelectedRange
extension: Extension
dataSpecifications: UsedDataSpecification[]
values: UsedValue[]
onExtensionPatch: (patch: Partial<Extension>) => void
}): LineGraphProps => {
const {
viewGrid,
viewBox,
playbackTimeSelectedRange,
extension,
dataSpecifications,
values,
onExtensionPatch,
} = params
// メモリのラベルを表示せずにビジュアルパーツのパネルにフィットするか
// viewGridを使用して判定します。
// メモ化のため、useMemoを使用します。
const gotContentFit = useMemo(() => {
return shouldContentFit(viewGrid.colSpan, viewGrid.rowSpan)
}, [viewGrid.colSpan, viewGrid.rowSpan])
// 表示するピクセルサイズをviewBoxを使用して作成します。
// メモ化のため、useMemoを使用します。
const gotSize = useMemo(() => {
return {
width: viewBox.width,
height: viewBox.height,
}
}, [viewBox.width, viewBox.height])
// 表示する時間範囲をplaybackTimeSelectedRangeを使用して作成します。
// メモ化のため、useMemoを使用します。
const gotTime = useMemo(() => {
return {
start: playbackTimeSelectedRange.start,
end: playbackTimeSelectedRange.end,
}
}, [playbackTimeSelectedRange.end, playbackTimeSelectedRange.start])
// selectNonNullablesのメモ化
const memoizedSelectNonNullables = useMemo(
() => memoize(selectNonNullables),
[],
)
// PropsValueを生成するためのメモ化済みfunctionの初期化
const memoizedPropsValues = useMemo(() => {
return buildEmptyArray(BIND_DATA_COUNT_MAX).map(() => ({
buildColorWithOpacity: memoize(buildColorWithOpacity),
hiddenIDsIncludes: memoize(arrayIncludes),
exactValueDatas: memoize(exactValueDatas),
buildPropsValue: memoize(buildPropsValue),
}))
}, [])
// Props Values を取得します。
// メモ化したfunctionを使用します。
const gotValues = memoizedSelectNonNullables(
...buildEmptyArray(BIND_DATA_COUNT_MAX).map((_, i) =>
selectPropsValue(
playbackTimeSelectedRange.start,
playbackTimeSelectedRange.end,
dataSpecifications[i],
values[i],
extension.hiddenIDs,
memoizedPropsValues[i].buildColorWithOpacity,
memoizedPropsValues[i].hiddenIDsIncludes,
memoizedPropsValues[i].exactValueDatas,
memoizedPropsValues[i].buildPropsValue,
),
),
)
// 凡例をクリックしたときのイベントハンドラを定義します。
// メモ化のため useCallback を使用します。
const onLegendHiddenChange: LineGraphProps['onLegendHiddenChange'] = useCallback(
({ valueID, hidden }) => {
const newHiddenIDs = !hidden
? extension.hiddenIDs.concat([valueID])
: extension.hiddenIDs.filter((id) => id !== valueID)
onExtensionPatch({ hiddenIDs: newHiddenIDs })
},
[extension.hiddenIDs, onExtensionPatch],
)
// Data Visualizerのバインドデータ削除によりhiddenIDsにゴミが残らないようにケアします。
useEffect(() => {
const dataSpecIDs = dataSpecifications.map(({ id }) => id)
const newHiddenIDs = extension.hiddenIDs.filter((hiddenID) =>
dataSpecIDs.includes(hiddenID),
)
if (extension.hiddenIDs.length !== newHiddenIDs.length) {
onExtensionPatch({ hiddenIDs: newHiddenIDs })
}
}, [dataSpecifications, extension.hiddenIDs, onExtensionPatch])
return {
contentFit: gotContentFit,
size: gotSize,
time: gotTime,
values: gotValues,
onLegendHiddenChange,
}
}
/**
* LineGraph Component Propsに含むValueの型に変換します。
* dataSpecification, value のいずれかが未定義の場合はnullを返します。
*/
const selectPropsValue = (
startTime: number,
endTime: number,
dataSpecification: UsedDataSpecification | undefined,
value: UsedValue | undefined,
hiddenIDs: string[],
funcBuildColorWithOpacity: typeof buildColorWithOpacity,
funcHiddenIDInclues: typeof arrayIncludes,
funcExactValueDatas: typeof exactValueDatas,
funcBuildPropsValue: typeof buildPropsValue,
): LineGraphPropsValue | null => {
if (!dataSpecification || !value) {
return null
}
const { id, dataName, colorCode, colorOpacity, colorFill } = dataSpecification
const { data } = value
// RGB, またはRGBAのカラーコードを作成します。作成できないときはwhiteとみなします。
const color = funcBuildColorWithOpacity(colorCode, colorOpacity) ?? 'white'
// 非表示状態を判定します。
const hidden = funcHiddenIDInclues(hiddenIDs, id)
// 時間範囲指定のデータポイント配列を抽出します。
const gotDatas = funcExactValueDatas(data, startTime, endTime)
return funcBuildPropsValue(id, dataName, color, colorFill, hidden, gotDatas)
}
/**
* 時間範囲を指定してデータポイント配列を取得します。
*/
export const exactValueDatas = (
datas: UsedValue['data'],
startTime: number,
endTime: number,
): UsedValue['data'] => {
const compiler = (a: UsedValuePickedTime, b: UsedValuePickedTime) => a.t - b.t
const startIndex = arraySearchIndexGe<UsedValuePickedTime>(
datas,
{ t: startTime },
compiler,
)
const endIndex = arraySearchIndexLe<UsedValuePickedTime>(
datas,
{ t: endTime },
compiler,
)
if (startIndex < 0 || endIndex < 0) {
return []
}
return datas.slice(startIndex, endIndex + 1)
}
/**
* PropsValueのObjectを作成します。
*/
const buildPropsValue = (
id: string,
name: string,
color: string,
fill: boolean,
hidden: boolean,
datas: UsedValue['data'],
): LineGraphPropsValue => ({
id,
name,
color,
fill,
hidden,
datas,
})