Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6aec597
delegate parse filters straight to ApiQueryParser
RobertJoonas Feb 20, 2026
ec0c526
add missing dimensions parsing
RobertJoonas Feb 20, 2026
1b9db59
API v2: add partial_time_labels and present_index to meta
RobertJoonas Mar 4, 2026
bec63c3
API v2 comparisons
RobertJoonas Mar 16, 2026
698082e
allow fixing now in the new endpoint via conn.private
RobertJoonas Mar 9, 2026
9eabcb5
fix tests
RobertJoonas Mar 10, 2026
7178837
fix and transform main_graph_test to target the new endpoint
RobertJoonas Mar 18, 2026
3dcfad6
remove comparisons from legacy timeseries
RobertJoonas Mar 9, 2026
1ab9e59
remove main_graph from Api.StatsController
RobertJoonas Mar 10, 2026
f3f99d4
fix comparisons tests
RobertJoonas Mar 30, 2026
a0507e5
Merge remote-tracking branch 'origin/master' into main-graph-v2-backend
RobertJoonas Mar 31, 2026
44f64ad
Merge remote-tracking branch 'origin/master' into main-graph-v2-backend
RobertJoonas Apr 1, 2026
bd2b9a2
partial time labels to empty list
RobertJoonas Apr 1, 2026
c41641d
Merge remote-tracking branch 'origin/master' into main-graph-v2-backend
RobertJoonas Apr 6, 2026
dd8f2d6
fix partial time labels
RobertJoonas Apr 6, 2026
768da80
include.empty_metrics
RobertJoonas Apr 9, 2026
9b1bfa0
Merge remote-tracking branch 'origin/master' into main-graph-v2-backend
RobertJoonas Apr 9, 2026
938c9a4
make sure CE test is not trying to insert revenue goal
RobertJoonas Apr 9, 2026
37411cf
add missing tests and parsing include.empty_metrics
RobertJoonas Apr 13, 2026
591e538
add meta.comparison_partial_time_labels
RobertJoonas Apr 13, 2026
448f3e4
fix CI (ce_test, codespell, credo)
RobertJoonas Apr 13, 2026
bb79cab
Merge remote-tracking branch 'origin/master' into main-graph-v2-backend
RobertJoonas Apr 13, 2026
098bc77
Refactor main graph to d3.js (#6159)
apata Apr 15, 2026
a7c17c6
Draw points after lines, underline before line
apata Apr 15, 2026
4e534cf
Change order of drawing main and comparison series, make comparison s…
apata Apr 15, 2026
3259ca5
backend: limit time:minute to 24h periods
RobertJoonas Apr 15, 2026
7c922cd
account for DST as well with minute interval limit
RobertJoonas Apr 15, 2026
84d961b
Fix issue with x axis on Firefox (weird results measuring empty svg t…
apata Apr 15, 2026
f04027c
Fix invalid type
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
1 change: 1 addition & 0 deletions .codespellignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ referer
referers
statics
firs
IST
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' : ''}
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