Implement basic time selection
This commit is contained in:
parent
6f69ecd087
commit
bf3c65bd6c
|
@ -8,6 +8,7 @@ import {
|
|||
pointer,
|
||||
stack,
|
||||
stackOrderReverse,
|
||||
D3BrushEvent,
|
||||
ScaleTime,
|
||||
ScaleContinuousNumeric,
|
||||
SeriesPoint,
|
||||
|
@ -105,7 +106,11 @@ export const SingleValueDistributionChart = (props: {
|
|||
xScale: 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] =
|
||||
useState<PositionValue<DistributionPoint>>()
|
||||
|
||||
|
@ -122,6 +127,19 @@ export const SingleValueDistributionChart = (props: {
|
|||
return { fmtX, fmtY, xAxis, yAxis }
|
||||
}, [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 [mouseX, mouseY] = pointer(event)
|
||||
const queryX = xScale.invert(mouseX)
|
||||
|
@ -145,6 +163,7 @@ export const SingleValueDistributionChart = (props: {
|
|||
h={h}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
|
@ -171,7 +190,11 @@ export const MultiValueHistoryChart = (props: {
|
|||
yScale: ScaleContinuousNumeric<number, number>
|
||||
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>>()
|
||||
|
||||
type SP = SeriesPoint<MultiPoint>
|
||||
|
@ -187,7 +210,7 @@ export const MultiValueHistoryChart = (props: {
|
|||
|
||||
const [min, max] = yScale.domain()
|
||||
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)
|
||||
.tickValues(tickValues)
|
||||
.tickFormat(fmtY)
|
||||
|
@ -200,6 +223,19 @@ export const MultiValueHistoryChart = (props: {
|
|||
return { fmtX, fmtY, xAxis, yAxis, series }
|
||||
}, [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 [mouseX, mouseY] = pointer(event)
|
||||
const queryX = xScale.invert(mouseX)
|
||||
|
@ -231,6 +267,7 @@ export const MultiValueHistoryChart = (props: {
|
|||
h={h}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
|
@ -259,12 +296,17 @@ export const SingleValueHistoryChart = (props: {
|
|||
yScale: d3.ScaleContinuousNumeric<number, number>
|
||||
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 px = useCallback((p: HistoryPoint) => xScale(p[0]), [xScale])
|
||||
const py0 = yScale(0)
|
||||
const py1 = useCallback((p: HistoryPoint) => yScale(p[1]), [yScale])
|
||||
const xBisector = bisector((p: HistoryPoint) => p[0])
|
||||
|
||||
const { fmtX, fmtY, xAxis, yAxis } = useMemo(() => {
|
||||
const [start, end] = xScale.domain()
|
||||
|
@ -273,16 +315,28 @@ export const SingleValueHistoryChart = (props: {
|
|||
|
||||
const [min, max] = yScale.domain()
|
||||
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)
|
||||
.tickValues(tickValues)
|
||||
.tickFormat(fmtY)
|
||||
return { fmtX, fmtY, xAxis, yAxis }
|
||||
}, [h, pct, xScale, yScale])
|
||||
|
||||
const xBisector = bisector((p: HistoryPoint) => p[0])
|
||||
const onMouseOver = useEvent((event: React.PointerEvent) => {
|
||||
const [mouseX, mouseY] = pointer(event)
|
||||
const onSelect = useEvent((ev: D3BrushEvent<HistoryPoint>) => {
|
||||
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((ev: React.PointerEvent) => {
|
||||
const [mouseX, mouseY] = pointer(ev)
|
||||
const queryX = xScale.invert(mouseX)
|
||||
const [_x, y] = data[xBisector.center(data, queryX)]
|
||||
setMouseState({ top: mouseY - 10, left: mouseX + 60, p: [queryX, y] })
|
||||
|
@ -304,6 +358,7 @@ export const SingleValueHistoryChart = (props: {
|
|||
h={h}
|
||||
xAxis={xAxis}
|
||||
yAxis={yAxis}
|
||||
onSelect={onSelect}
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
import { ReactNode, SVGProps, memo, useRef, useEffect } from 'react'
|
||||
import { Axis, CurveFactory, area, curveStepAfter, line, select } from 'd3'
|
||||
import { ReactNode, SVGProps, memo, useRef, useEffect, useMemo } from 'react'
|
||||
import {
|
||||
Axis,
|
||||
CurveFactory,
|
||||
D3BrushEvent,
|
||||
area,
|
||||
brushX,
|
||||
curveStepAfter,
|
||||
line,
|
||||
select,
|
||||
} from 'd3'
|
||||
import dayjs from 'dayjs'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
import { Contract } from 'common/contract'
|
||||
|
||||
|
@ -14,8 +24,11 @@ export const XAxis = <X,>(props: { w: number; h: number; axis: Axis<X> }) => {
|
|||
useEffect(() => {
|
||||
if (axisRef.current != null) {
|
||||
select(axisRef.current)
|
||||
.transition()
|
||||
.duration(250)
|
||||
.call(axis)
|
||||
.call((g) => g.select('.domain').remove())
|
||||
.select('.domain')
|
||||
.attr('stroke-width', 0)
|
||||
}
|
||||
}, [h, axis])
|
||||
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(() => {
|
||||
if (axisRef.current != null) {
|
||||
select(axisRef.current)
|
||||
.transition()
|
||||
.duration(250)
|
||||
.call(axis)
|
||||
.call((g) => g.select('.domain').remove())
|
||||
.call((g) =>
|
||||
g.selectAll('.tick line').attr('x2', w).attr('stroke-opacity', 0.1)
|
||||
)
|
||||
.select('.domain')
|
||||
.attr('stroke-width', 0)
|
||||
}
|
||||
}, [w, h, axis])
|
||||
return <g ref={axisRef} />
|
||||
|
@ -99,20 +115,53 @@ export const SVGChart = <X, Y>(props: {
|
|||
h: number
|
||||
xAxis: Axis<X>
|
||||
yAxis: Axis<Y>
|
||||
onSelect?: (ev: D3BrushEvent<any>) => void
|
||||
onMouseOver?: (ev: React.PointerEvent) => void
|
||||
onMouseLeave?: (ev: React.PointerEvent) => void
|
||||
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 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 (
|
||||
<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})`}>
|
||||
<XAxis axis={xAxis} w={innerW} h={innerH} />
|
||||
<YAxis axis={yAxis} w={innerW} h={innerH} />
|
||||
{children}
|
||||
<rect
|
||||
<g clipPath={`url(#${clipPathId})`}>{children}</g>
|
||||
<g
|
||||
ref={overlayRef}
|
||||
x="0"
|
||||
y="0"
|
||||
width={innerW}
|
||||
|
|
Loading…
Reference in New Issue
Block a user