Skip to content

Commit 098bc77

Browse files
authored
Refactor main graph to d3.js (#6159)
* Add generic graph component and use it for main graph * Remove superfluous mobile related code * Add isTouchDevice state and touch-device specific zoom instructions * Move useMainGraphWidth * Center tooltip * Adjust tooltip position * Clarify pointerup * Extract fetching main graph and remapping main graph data * Unify BE and FE change calculations * Stop wrapping change arrow, unify with ComparisonTooltipContent * Exports * Attempt partial logic * Draw dashed partial periods at the start and end of main series * Simplify * Clarify series * Better types * Make remapAndFillData take accessors * Get metric label using utility * Refactor value and comparisonValue to numericValue and comparisonNumericValue * Refactor outerValue and comparisoOuterValue to value and comparisonValue * Fix format * Fix type error in test * No need to assert isPartial * Fix issue with full crash when switching between periods while tooltip * Add necessary types * Handle comparison_partial_time_labels * Handle [null] style result items * Hint expected tick count to y axis nicing * Remove old line graph * Clarify graph inputs * Add MetricValue type * Extract logic to read first and last time labels * Clarify remapAndFillData * Stop requesting for present_index as it is unused * Clarify getLineSegments comment and refactor getNumericValue arg * Remove unnecessary guard and unnecessary return value * Refactor x tick values function and the way it is passed around * Add getXDomain function * Add tests for some graph.ts utils * Add constants to top of graph.tsx, move utiltiy types to bottom
1 parent bb79cab commit 098bc77

17 files changed

+2089
-787
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React, { ReactNode, useLayoutEffect, useRef, useState } from 'react'
2+
import { Transition } from '@headlessui/react'
3+
4+
export const GraphTooltipWrapper = ({
5+
x,
6+
y,
7+
maxX,
8+
minWidth,
9+
children,
10+
className,
11+
onClick,
12+
isTouchDevice
13+
}: {
14+
x: number
15+
y: number
16+
maxX: number
17+
minWidth: number
18+
children: ReactNode
19+
className?: string
20+
onClick?: () => void
21+
isTouchDevice?: boolean
22+
}) => {
23+
const ref = useRef<HTMLDivElement>(null)
24+
// bigger on mobile to have room between thumb and tooltip
25+
const xOffsetFromCursor = isTouchDevice ? 24 : 12
26+
const yOffsetFromCursor = isTouchDevice ? 48 : 24
27+
const [measuredWidth, setMeasuredWidth] = useState(minWidth)
28+
// center tooltip above the cursor, clamped to prevent left/right overflow
29+
const rawLeft = x + xOffsetFromCursor
30+
const tooltipLeft = Math.max(0, Math.min(rawLeft, maxX - measuredWidth))
31+
32+
useLayoutEffect(() => {
33+
if (!ref.current) {
34+
return
35+
}
36+
setMeasuredWidth(ref.current.offsetWidth)
37+
}, [children, className, minWidth])
38+
39+
return (
40+
<Transition
41+
as={React.Fragment}
42+
appear
43+
show
44+
// enter delay on mobile is needed to prevent the tooltip from entering when the user starts to y-pan
45+
// but the y-pan is not yet certain
46+
enter={isTouchDevice ? 'transition-opacity duration-0 delay-150' : ''}
47+
enterFrom={isTouchDevice ? 'opacity-0' : ''}
48+
enterTo={isTouchDevice ? 'opacity-100' : ''}
49+
>
50+
<div
51+
ref={ref}
52+
className={className}
53+
onClick={onClick}
54+
style={{
55+
minWidth,
56+
left: tooltipLeft,
57+
top: y,
58+
transform: `translateY(-100%) translateY(-${yOffsetFromCursor}px)`
59+
}}
60+
>
61+
{children}
62+
</div>
63+
</Transition>
64+
)
65+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { getSuggestedXTickValues, getXDomain } from './graph'
2+
import * as d3 from 'd3'
3+
4+
describe(`${getXDomain.name}`, () => {
5+
it('returns [0, 1] for a single bucket to avoid a zero-width domain', () => {
6+
expect(getXDomain(1)).toEqual([0, 1])
7+
})
8+
it('returns [0, bucketCount - 1] for multiple buckets', () => {
9+
expect(getXDomain(5)).toEqual([0, 4])
10+
})
11+
})
12+
13+
const anyRange = [0, 100]
14+
describe(`${getSuggestedXTickValues.name}`, () => {
15+
it('handles 1 bucket', () => {
16+
const data = new Array(1).fill(0)
17+
expect(
18+
getSuggestedXTickValues(
19+
d3.scaleLinear(getXDomain(data.length), anyRange),
20+
data.length
21+
)
22+
).toEqual([[0, 1]])
23+
})
24+
25+
it('handles 2 buckets', () => {
26+
const data = new Array(2).fill(0)
27+
expect(
28+
getSuggestedXTickValues(
29+
d3.scaleLinear(getXDomain(data.length), anyRange),
30+
data.length
31+
)
32+
).toEqual([[0, 1]])
33+
})
34+
35+
it('handles 7 buckets', () => {
36+
const data = new Array(7).fill(0)
37+
expect(
38+
getSuggestedXTickValues(
39+
d3.scaleLinear(getXDomain(data.length), anyRange),
40+
data.length
41+
)
42+
).toEqual([
43+
[0, 1, 2, 3, 4, 5, 6],
44+
[0, 2, 4, 6],
45+
[0, 5]
46+
])
47+
})
48+
49+
it('handles 24 buckets (day by hours)', () => {
50+
const data = new Array(24).fill(0)
51+
expect(
52+
getSuggestedXTickValues(
53+
d3.scaleLinear(getXDomain(data.length), anyRange),
54+
data.length
55+
)
56+
).toEqual([
57+
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22],
58+
[0, 5, 10, 15, 20],
59+
[0, 10, 20],
60+
[0, 20]
61+
])
62+
})
63+
64+
it('handles 28 buckets', () => {
65+
const data = new Array(28).fill(0)
66+
expect(
67+
getSuggestedXTickValues(
68+
d3.scaleLinear(getXDomain(data.length), anyRange),
69+
data.length
70+
)
71+
).toEqual([
72+
[0, 5, 10, 15, 20, 25],
73+
[0, 10, 20],
74+
[0, 20]
75+
])
76+
})
77+
78+
it('handles 91 buckets', () => {
79+
const data = new Array(91).fill(0)
80+
expect(
81+
getSuggestedXTickValues(
82+
d3.scaleLinear(getXDomain(data.length), anyRange),
83+
data.length
84+
)
85+
).toEqual([
86+
[0, 10, 20, 30, 40, 50, 60, 70, 80, 90],
87+
[0, 20, 40, 60, 80],
88+
[0, 50],
89+
[0]
90+
])
91+
})
92+
93+
it('handles 700 buckets', () => {
94+
const data = new Array(700).fill(0)
95+
expect(
96+
getSuggestedXTickValues(
97+
d3.scaleLinear(getXDomain(data.length), anyRange),
98+
data.length
99+
)
100+
).toEqual([
101+
[0, 100, 200, 300, 400, 500, 600],
102+
[0, 200, 400, 600],
103+
[0, 500]
104+
])
105+
})
106+
})

0 commit comments

Comments
 (0)