Implement basic time selection
This commit is contained in:
parent
6f69ecd087
commit
bf3c65bd6c
|
@ -8,6 +8,7 @@ import {
|
||||||
pointer,
|
pointer,
|
||||||
stack,
|
stack,
|
||||||
stackOrderReverse,
|
stackOrderReverse,
|
||||||
|
D3BrushEvent,
|
||||||
ScaleTime,
|
ScaleTime,
|
||||||
ScaleContinuousNumeric,
|
ScaleContinuousNumeric,
|
||||||
SeriesPoint,
|
SeriesPoint,
|
||||||
|
@ -105,7 +106,11 @@ export const SingleValueDistributionChart = (props: {
|
||||||
xScale: ScaleContinuousNumeric<number, number>
|
xScale: ScaleContinuousNumeric<number, number>
|
||||||
yScale: ScaleContinuousNumeric<number, number>
|
yScale: ScaleContinuousNumeric<number, number>
|
||||||
}) => {
|
}) => {
|
||||||
const { color, data, xScale, yScale, w, h } = props
|
const { color, data, yScale, w, h } = props
|
||||||
|
|
||||||
|
// note that we have to type this funkily in order to succesfully store
|
||||||
|
// a function inside of useState
|
||||||
|
const [xScale, setXScale] = useState(() => props.xScale)
|
||||||
const [mouseState, setMouseState] =
|
const [mouseState, setMouseState] =
|
||||||
useState<PositionValue<DistributionPoint>>()
|
useState<PositionValue<DistributionPoint>>()
|
||||||
|
|
||||||
|
@ -122,6 +127,19 @@ export const SingleValueDistributionChart = (props: {
|
||||||
return { fmtX, fmtY, xAxis, yAxis }
|
return { fmtX, fmtY, xAxis, yAxis }
|
||||||
}, [xScale, yScale])
|
}, [xScale, yScale])
|
||||||
|
|
||||||
|
const onSelect = useEvent((ev: D3BrushEvent<DistributionPoint>) => {
|
||||||
|
if (ev.selection) {
|
||||||
|
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||||
|
setXScale(() =>
|
||||||
|
xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
||||||
|
)
|
||||||
|
setMouseState(undefined)
|
||||||
|
} else {
|
||||||
|
setXScale(() => props.xScale)
|
||||||
|
setMouseState(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
||||||
const [mouseX, mouseY] = pointer(event)
|
const [mouseX, mouseY] = pointer(event)
|
||||||
const queryX = xScale.invert(mouseX)
|
const queryX = xScale.invert(mouseX)
|
||||||
|
@ -145,6 +163,7 @@ export const SingleValueDistributionChart = (props: {
|
||||||
h={h}
|
h={h}
|
||||||
xAxis={xAxis}
|
xAxis={xAxis}
|
||||||
yAxis={yAxis}
|
yAxis={yAxis}
|
||||||
|
onSelect={onSelect}
|
||||||
onMouseOver={onMouseOver}
|
onMouseOver={onMouseOver}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
|
@ -171,7 +190,11 @@ export const MultiValueHistoryChart = (props: {
|
||||||
yScale: ScaleContinuousNumeric<number, number>
|
yScale: ScaleContinuousNumeric<number, number>
|
||||||
pct?: boolean
|
pct?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const { colors, data, xScale, yScale, labels, w, h, pct } = props
|
const { colors, data, yScale, labels, w, h, pct } = props
|
||||||
|
|
||||||
|
// note that we have to type this funkily in order to succesfully store
|
||||||
|
// a function inside of useState
|
||||||
|
const [xScale, setXScale] = useState(() => props.xScale)
|
||||||
const [mouseState, setMouseState] = useState<PositionValue<MultiPoint>>()
|
const [mouseState, setMouseState] = useState<PositionValue<MultiPoint>>()
|
||||||
|
|
||||||
type SP = SeriesPoint<MultiPoint>
|
type SP = SeriesPoint<MultiPoint>
|
||||||
|
@ -187,7 +210,7 @@ export const MultiValueHistoryChart = (props: {
|
||||||
|
|
||||||
const [min, max] = yScale.domain()
|
const [min, max] = yScale.domain()
|
||||||
const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
||||||
const xAxis = axisBottom<Date>(xScale).tickFormat(fmtX)
|
const xAxis = axisBottom<Date>(xScale)
|
||||||
const yAxis = axisLeft<number>(yScale)
|
const yAxis = axisLeft<number>(yScale)
|
||||||
.tickValues(tickValues)
|
.tickValues(tickValues)
|
||||||
.tickFormat(fmtY)
|
.tickFormat(fmtY)
|
||||||
|
@ -200,6 +223,19 @@ export const MultiValueHistoryChart = (props: {
|
||||||
return { fmtX, fmtY, xAxis, yAxis, series }
|
return { fmtX, fmtY, xAxis, yAxis, series }
|
||||||
}, [h, pct, xScale, yScale, data, labels.length])
|
}, [h, pct, xScale, yScale, data, labels.length])
|
||||||
|
|
||||||
|
const onSelect = useEvent((ev: D3BrushEvent<MultiPoint>) => {
|
||||||
|
if (ev.selection) {
|
||||||
|
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||||
|
setXScale(() =>
|
||||||
|
xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
||||||
|
)
|
||||||
|
setMouseState(undefined)
|
||||||
|
} else {
|
||||||
|
setXScale(() => props.xScale)
|
||||||
|
setMouseState(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
||||||
const [mouseX, mouseY] = pointer(event)
|
const [mouseX, mouseY] = pointer(event)
|
||||||
const queryX = xScale.invert(mouseX)
|
const queryX = xScale.invert(mouseX)
|
||||||
|
@ -231,6 +267,7 @@ export const MultiValueHistoryChart = (props: {
|
||||||
h={h}
|
h={h}
|
||||||
xAxis={xAxis}
|
xAxis={xAxis}
|
||||||
yAxis={yAxis}
|
yAxis={yAxis}
|
||||||
|
onSelect={onSelect}
|
||||||
onMouseOver={onMouseOver}
|
onMouseOver={onMouseOver}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
|
@ -259,12 +296,17 @@ export const SingleValueHistoryChart = (props: {
|
||||||
yScale: d3.ScaleContinuousNumeric<number, number>
|
yScale: d3.ScaleContinuousNumeric<number, number>
|
||||||
pct?: boolean
|
pct?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const { color, data, xScale, yScale, pct, w, h } = props
|
const { color, data, pct, yScale, w, h } = props
|
||||||
|
|
||||||
|
// note that we have to type this funkily in order to succesfully store
|
||||||
|
// a function inside of useState
|
||||||
|
const [xScale, setXScale] = useState(() => props.xScale)
|
||||||
const [mouseState, setMouseState] = useState<PositionValue<HistoryPoint>>()
|
const [mouseState, setMouseState] = useState<PositionValue<HistoryPoint>>()
|
||||||
|
|
||||||
const px = useCallback((p: HistoryPoint) => xScale(p[0]), [xScale])
|
const px = useCallback((p: HistoryPoint) => xScale(p[0]), [xScale])
|
||||||
const py0 = yScale(0)
|
const py0 = yScale(0)
|
||||||
const py1 = useCallback((p: HistoryPoint) => yScale(p[1]), [yScale])
|
const py1 = useCallback((p: HistoryPoint) => yScale(p[1]), [yScale])
|
||||||
|
const xBisector = bisector((p: HistoryPoint) => p[0])
|
||||||
|
|
||||||
const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => {
|
const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => {
|
||||||
const [start, end] = xScale.domain()
|
const [start, end] = xScale.domain()
|
||||||
|
@ -273,16 +315,28 @@ export const SingleValueHistoryChart = (props: {
|
||||||
|
|
||||||
const [min, max] = yScale.domain()
|
const [min, max] = yScale.domain()
|
||||||
const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
const tickValues = getTickValues(min, max, h < 200 ? 3 : 5)
|
||||||
const xAxis = axisBottom<Date>(xScale).tickFormat(fmtX)
|
const xAxis = axisBottom<Date>(xScale)
|
||||||
const yAxis = axisLeft<number>(yScale)
|
const yAxis = axisLeft<number>(yScale)
|
||||||
.tickValues(tickValues)
|
.tickValues(tickValues)
|
||||||
.tickFormat(fmtY)
|
.tickFormat(fmtY)
|
||||||
return { fmtX, fmtY, xAxis, yAxis }
|
return { fmtX, fmtY, xAxis, yAxis }
|
||||||
}, [h, pct, xScale, yScale])
|
}, [h, pct, xScale, yScale])
|
||||||
|
|
||||||
const xBisector = bisector((p: HistoryPoint) => p[0])
|
const onSelect = useEvent((ev: D3BrushEvent<HistoryPoint>) => {
|
||||||
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
if (ev.selection) {
|
||||||
const [mouseX, mouseY] = pointer(event)
|
const [mouseX0, mouseX1] = ev.selection as [number, number]
|
||||||
|
setXScale(() =>
|
||||||
|
xScale.copy().domain([xScale.invert(mouseX0), xScale.invert(mouseX1)])
|
||||||
|
)
|
||||||
|
setMouseState(undefined)
|
||||||
|
} else {
|
||||||
|
setXScale(() => props.xScale)
|
||||||
|
setMouseState(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const onMouseOver = useEvent((ev: React.PointerEvent) => {
|
||||||
|
const [mouseX, mouseY] = pointer(ev)
|
||||||
const queryX = xScale.invert(mouseX)
|
const queryX = xScale.invert(mouseX)
|
||||||
const [_x, y] = data[xBisector.center(data, queryX)]
|
const [_x, y] = data[xBisector.center(data, queryX)]
|
||||||
setMouseState({ top: mouseY - 10, left: mouseX + 60, p: [queryX, y] })
|
setMouseState({ top: mouseY - 10, left: mouseX + 60, p: [queryX, y] })
|
||||||
|
@ -304,6 +358,7 @@ export const SingleValueHistoryChart = (props: {
|
||||||
h={h}
|
h={h}
|
||||||
xAxis={xAxis}
|
xAxis={xAxis}
|
||||||
yAxis={yAxis}
|
yAxis={yAxis}
|
||||||
|
onSelect={onSelect}
|
||||||
onMouseOver={onMouseOver}
|
onMouseOver={onMouseOver}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
import { ReactNode, SVGProps, memo, useRef, useEffect } from 'react'
|
import { ReactNode, SVGProps, memo, useRef, useEffect, useMemo } from 'react'
|
||||||
import { Axis, CurveFactory, area, curveStepAfter, line, select } from 'd3'
|
import {
|
||||||
|
Axis,
|
||||||
|
CurveFactory,
|
||||||
|
D3BrushEvent,
|
||||||
|
area,
|
||||||
|
brushX,
|
||||||
|
curveStepAfter,
|
||||||
|
line,
|
||||||
|
select,
|
||||||
|
} from 'd3'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
import { Contract } from 'common/contract'
|
import { Contract } from 'common/contract'
|
||||||
|
|
||||||
|
@ -14,8 +24,11 @@ export const XAxis = <X,>(props: { w: number; h: number; axis: Axis<X> }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (axisRef.current != null) {
|
if (axisRef.current != null) {
|
||||||
select(axisRef.current)
|
select(axisRef.current)
|
||||||
|
.transition()
|
||||||
|
.duration(250)
|
||||||
.call(axis)
|
.call(axis)
|
||||||
.call((g) => g.select('.domain').remove())
|
.select('.domain')
|
||||||
|
.attr('stroke-width', 0)
|
||||||
}
|
}
|
||||||
}, [h, axis])
|
}, [h, axis])
|
||||||
return <g ref={axisRef} transform={`translate(0, ${h})`} />
|
return <g ref={axisRef} transform={`translate(0, ${h})`} />
|
||||||
|
@ -27,11 +40,14 @@ export const YAxis = <Y,>(props: { w: number; h: number; axis: Axis<Y> }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (axisRef.current != null) {
|
if (axisRef.current != null) {
|
||||||
select(axisRef.current)
|
select(axisRef.current)
|
||||||
|
.transition()
|
||||||
|
.duration(250)
|
||||||
.call(axis)
|
.call(axis)
|
||||||
.call((g) => g.select('.domain').remove())
|
|
||||||
.call((g) =>
|
.call((g) =>
|
||||||
g.selectAll('.tick line').attr('x2', w).attr('stroke-opacity', 0.1)
|
g.selectAll('.tick line').attr('x2', w).attr('stroke-opacity', 0.1)
|
||||||
)
|
)
|
||||||
|
.select('.domain')
|
||||||
|
.attr('stroke-width', 0)
|
||||||
}
|
}
|
||||||
}, [w, h, axis])
|
}, [w, h, axis])
|
||||||
return <g ref={axisRef} />
|
return <g ref={axisRef} />
|
||||||
|
@ -99,20 +115,53 @@ export const SVGChart = <X, Y>(props: {
|
||||||
h: number
|
h: number
|
||||||
xAxis: Axis<X>
|
xAxis: Axis<X>
|
||||||
yAxis: Axis<Y>
|
yAxis: Axis<Y>
|
||||||
|
onSelect?: (ev: D3BrushEvent<any>) => void
|
||||||
onMouseOver?: (ev: React.PointerEvent) => void
|
onMouseOver?: (ev: React.PointerEvent) => void
|
||||||
onMouseLeave?: (ev: React.PointerEvent) => void
|
onMouseLeave?: (ev: React.PointerEvent) => void
|
||||||
pct?: boolean
|
pct?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const { children, w, h, xAxis, yAxis, onMouseOver, onMouseLeave } = props
|
const { children, w, h, xAxis, yAxis, onMouseOver, onMouseLeave, onSelect } =
|
||||||
|
props
|
||||||
|
const overlayRef = useRef<SVGGElement>(null)
|
||||||
const innerW = w - MARGIN_X
|
const innerW = w - MARGIN_X
|
||||||
const innerH = h - MARGIN_Y
|
const innerH = h - MARGIN_Y
|
||||||
|
const clipPathId = useMemo(() => nanoid(), [])
|
||||||
|
|
||||||
|
const justSelected = useRef(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (onSelect != null && overlayRef.current) {
|
||||||
|
const brush = brushX().extent([
|
||||||
|
[0, 0],
|
||||||
|
[innerW, innerH],
|
||||||
|
])
|
||||||
|
brush.on('end', (ev) => {
|
||||||
|
// when we clear the brush after a selection, that would normally cause
|
||||||
|
// another 'end' event, so we have to suppress it with this flag
|
||||||
|
if (!justSelected.current) {
|
||||||
|
justSelected.current = true
|
||||||
|
onSelect(ev)
|
||||||
|
if (overlayRef.current) {
|
||||||
|
select(overlayRef.current).call(brush.clear)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
justSelected.current = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
select(overlayRef.current).call(brush)
|
||||||
|
}
|
||||||
|
}, [innerW, innerH, onSelect])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg className="w-full" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
<svg className="w-full" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
|
||||||
|
<clipPath id={clipPathId}>
|
||||||
|
<rect x={0} y={0} width={innerW} height={innerH} />
|
||||||
|
</clipPath>
|
||||||
<g transform={`translate(${MARGIN.left}, ${MARGIN.top})`}>
|
<g transform={`translate(${MARGIN.left}, ${MARGIN.top})`}>
|
||||||
<XAxis axis={xAxis} w={innerW} h={innerH} />
|
<XAxis axis={xAxis} w={innerW} h={innerH} />
|
||||||
<YAxis axis={yAxis} w={innerW} h={innerH} />
|
<YAxis axis={yAxis} w={innerW} h={innerH} />
|
||||||
{children}
|
<g clipPath={`url(#${clipPathId})`}>{children}</g>
|
||||||
<rect
|
<g
|
||||||
|
ref={overlayRef}
|
||||||
x="0"
|
x="0"
|
||||||
y="0"
|
y="0"
|
||||||
width={innerW}
|
width={innerW}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user