Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5a82ec4
Add generic graph component and use it for main graph
apata Apr 1, 2026
e05fdaa
Remove superfluous mobile related code
apata Apr 5, 2026
a312c3c
Add isTouchDevice state and touch-device specific zoom instructions
apata Apr 5, 2026
3faf67b
Move useMainGraphWidth
apata Apr 5, 2026
31745c4
Center tooltip
apata Apr 5, 2026
edb2faa
Adjust tooltip position
apata Apr 5, 2026
93e9a9f
Clarify pointerup
apata Apr 5, 2026
d3ca930
Extract fetching main graph and remapping main graph data
apata Apr 6, 2026
d694cf5
Unify BE and FE change calculations
apata Apr 6, 2026
ae2272a
Stop wrapping change arrow, unify with ComparisonTooltipContent
apata Apr 6, 2026
dd75d4a
Exports
apata Apr 6, 2026
ffc7127
Attempt partial logic
apata Apr 7, 2026
16452d7
Draw dashed partial periods at the start and end of main series
apata Apr 7, 2026
ea8f640
Simplify
apata Apr 7, 2026
1fa6945
Clarify series
apata Apr 7, 2026
578ef76
Better types
apata Apr 13, 2026
7e97655
Make remapAndFillData take accessors
apata Apr 13, 2026
6d79540
Get metric label using utility
apata Apr 13, 2026
83708f2
Refactor value and comparisonValue to numericValue and comparisonNume…
apata Apr 13, 2026
67b83ba
Refactor outerValue and comparisoOuterValue to value and comparisonValue
apata Apr 13, 2026
3fcda54
Fix format
apata Apr 13, 2026
fbbaf4d
Fix type error in test
apata Apr 13, 2026
c631892
No need to assert isPartial
apata Apr 13, 2026
6a3120a
Fix issue with full crash when switching between periods while tooltip
apata Apr 13, 2026
0c4d397
Add necessary types
apata Apr 13, 2026
4f27cde
Handle comparison_partial_time_labels
apata Apr 13, 2026
59f92e9
Handle [null] style result items
apata Apr 13, 2026
f058ae2
Hint expected tick count to y axis nicing
apata Apr 14, 2026
6add2b9
Remove old line graph
apata Apr 14, 2026
66d2ceb
Clarify graph inputs
apata Apr 14, 2026
09ad9ed
Add MetricValue type
apata Apr 15, 2026
a46219a
Extract logic to read first and last time labels
apata Apr 15, 2026
3392577
Clarify remapAndFillData
apata Apr 15, 2026
8a9f4f2
Stop requesting for present_index as it is unused
apata Apr 15, 2026
a11812e
Clarify getLineSegments comment and refactor getNumericValue arg
apata Apr 15, 2026
54cd2bd
Remove unnecessary guard and unnecessary return value
apata Apr 15, 2026
5b2484f
Refactor x tick values function and the way it is passed around
apata Apr 15, 2026
1fe8eff
Add getXDomain function
apata Apr 15, 2026
a3ccc6e
Add tests for some graph.ts utils
apata Apr 15, 2026
3837947
Add constants to top of graph.tsx, move utiltiy types to bottom
apata Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions assets/js/dashboard/components/graph-tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React, { ReactNode, useLayoutEffect, useRef, useState } from 'react'
import { Transition } from '@headlessui/react'

export const GraphTooltipWrapper = ({
x,
y,
maxX,
minWidth,
children,
className,
onClick,
isTouchDevice
}: {
x: number
y: number
maxX: number
minWidth: number
children: ReactNode
className?: string
onClick?: () => void
isTouchDevice?: boolean
}) => {
const ref = useRef<HTMLDivElement>(null)
// bigger on mobile to have room between thumb and tooltip
const xOffsetFromCursor = isTouchDevice ? 24 : 12
const yOffsetFromCursor = isTouchDevice ? 48 : 24
const [measuredWidth, setMeasuredWidth] = useState(minWidth)
// center tooltip above the cursor, clamped to prevent left/right overflow
const rawLeft = x + xOffsetFromCursor
const tooltipLeft = Math.max(0, Math.min(rawLeft, maxX - measuredWidth))

useLayoutEffect(() => {
if (!ref.current) {
return
}
setMeasuredWidth(ref.current.offsetWidth)
}, [children, className, minWidth])

return (
<Transition
as={React.Fragment}
appear
show
// enter delay on mobile is needed to prevent the tooltip from entering when the user starts to y-pan
// but the y-pan is not yet certain
enter={isTouchDevice ? 'transition-opacity duration-0 delay-150' : ''}
Comment thread
RobertJoonas marked this conversation as resolved.
enterFrom={isTouchDevice ? 'opacity-0' : ''}
enterTo={isTouchDevice ? 'opacity-100' : ''}
>
<div
ref={ref}
className={className}
onClick={onClick}
style={{
minWidth,
left: tooltipLeft,
top: y,
transform: `translateY(-100%) translateY(-${yOffsetFromCursor}px)`
}}
>
{children}
</div>
</Transition>
)
}
106 changes: 106 additions & 0 deletions assets/js/dashboard/components/graph.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { getSuggestedXTickValues, getXDomain } from './graph'
import * as d3 from 'd3'

describe(`${getXDomain.name}`, () => {
it('returns [0, 1] for a single bucket to avoid a zero-width domain', () => {
expect(getXDomain(1)).toEqual([0, 1])
})
it('returns [0, bucketCount - 1] for multiple buckets', () => {
expect(getXDomain(5)).toEqual([0, 4])
})
})

const anyRange = [0, 100]
describe(`${getSuggestedXTickValues.name}`, () => {
it('handles 1 bucket', () => {
const data = new Array(1).fill(0)
expect(
getSuggestedXTickValues(
d3.scaleLinear(getXDomain(data.length), anyRange),
data.length
)
).toEqual([[0, 1]])
})

it('handles 2 buckets', () => {
const data = new Array(2).fill(0)
expect(
getSuggestedXTickValues(
d3.scaleLinear(getXDomain(data.length), anyRange),
data.length
)
).toEqual([[0, 1]])
})

it('handles 7 buckets', () => {
const data = new Array(7).fill(0)
expect(
getSuggestedXTickValues(
d3.scaleLinear(getXDomain(data.length), anyRange),
data.length
)
).toEqual([
[0, 1, 2, 3, 4, 5, 6],
[0, 2, 4, 6],
[0, 5]
])
})

it('handles 24 buckets (day by hours)', () => {
const data = new Array(24).fill(0)
expect(
getSuggestedXTickValues(
d3.scaleLinear(getXDomain(data.length), anyRange),
data.length
)
).toEqual([
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22],
[0, 5, 10, 15, 20],
[0, 10, 20],
[0, 20]
])
})

it('handles 28 buckets', () => {
const data = new Array(28).fill(0)
expect(
getSuggestedXTickValues(
d3.scaleLinear(getXDomain(data.length), anyRange),
data.length
)
).toEqual([
[0, 5, 10, 15, 20, 25],
[0, 10, 20],
[0, 20]
])
})

it('handles 91 buckets', () => {
const data = new Array(91).fill(0)
expect(
getSuggestedXTickValues(
d3.scaleLinear(getXDomain(data.length), anyRange),
data.length
)
).toEqual([
[0, 10, 20, 30, 40, 50, 60, 70, 80, 90],
[0, 20, 40, 60, 80],
[0, 50],
[0]
])
})

it('handles 700 buckets', () => {
const data = new Array(700).fill(0)
expect(
getSuggestedXTickValues(
d3.scaleLinear(getXDomain(data.length), anyRange),
data.length
)
).toEqual([
[0, 100, 200, 300, 400, 500, 600],
[0, 200, 400, 600],
[0, 500]
])
})
})
Loading
Loading