Skip to content

Commit c448434

Browse files
committed
Less primitive tooltip
1 parent 704b5c0 commit c448434

1 file changed

Lines changed: 154 additions & 78 deletions

File tree

assets/js/dashboard/stats/graph/main-graph.tsx

Lines changed: 154 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ReactNode, useEffect, useRef, useState } from 'react'
1+
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
22
import * as d3 from 'd3'
33
import { UIMode, useTheme } from '../../theme-context'
44
import {
@@ -8,6 +8,8 @@ import {
88
import { DashboardPeriod } from '../../dashboard-time-periods'
99
import dateFormatter from './date-formatter'
1010
import classNames from 'classnames'
11+
import { ChangeArrow } from '../reports/change-arrow'
12+
import { Metric } from '../../../types/query-api'
1113

1214
const height = 368
1315
const marginTop = 16
@@ -18,11 +20,12 @@ const marginLeft = 32
1820
type ResultItem = {
1921
dimensions: [string] // one item
2022
metrics: null | [number] | [{ value: number }] // one item
21-
comparison: { metrics: [number]; change: [number]; dimensions: [string] }
2223
}
2324
type MainGraphResponse = {
2425
results: Array<ResultItem | null>
25-
comparison_results: Array<ResultItem | null>
26+
comparison_results: Array<
27+
(ResultItem & { change: [number | null] | null }) | null
28+
>
2629
meta: {
2730
time_labels: string[]
2831
time_label_result_indices: (number | null)[]
@@ -42,7 +45,7 @@ type GraphDatum = {
4245
timeLabel: string | null // null when there's no label
4346
comparisonValue?: number | null // null when comparison is not defined
4447
comparisonTimeLabel?: string | null // null when comparison is not defined
45-
change?: number // null when comparison is not defined
48+
change?: number | null // null when comparison is not defined
4649
}
4750

4851
type XPos = number
@@ -67,26 +70,22 @@ export const MainGraph = ({
6770
datum: GraphDatum | null
6871
}>({ x: 0, y: 0, datum: null })
6972

73+
const interval = data.query.dimensions[0].split('time:')[1]
74+
const metric = data.query.metrics[0] as FormattableMetric
75+
const period = data.period
76+
const { remappedData, yMax, hasMultipleYears } = useMemo(
77+
() => remapToGraphData(data),
78+
[data]
79+
)
80+
81+
const showZoomToPeriod = ['month', 'day'].includes(interval)
82+
7083
useEffect(() => {
7184
if (!svgRef.current) {
7285
return
7386
}
87+
console.log('effect running')
7488

75-
const interval = data.query.dimensions[0].split('time:')[1]
76-
const period = data.period
77-
78-
const {
79-
remappedData,
80-
yMax,
81-
resultDefinedRange,
82-
comparisonResultDefinedRange
83-
} = remapToGraphData(data)
84-
console.log({
85-
remappedData,
86-
yMax,
87-
resultDefinedRange,
88-
comparisonResultDefinedRange
89-
})
9089
const yMin = 0
9190
const yDomain = [yMin, yMax]
9291
// Declare the y (vertical position) scale.
@@ -96,14 +95,8 @@ export const MainGraph = ({
9695
// It's a simple linear axis, one unit for every time bucket
9796
// because the BE returns equal length buckets
9897
const xDomain = [0, remappedData.length - 1]
99-
console.log(xDomain)
10098
const x = d3.scaleLinear(xDomain, [marginLeft, width - marginRight])
10199

102-
const minDate = remappedData[resultDefinedRange[0]].timeLabel!
103-
const maxDate = remappedData[resultDefinedRange[1]].timeLabel!
104-
console.log(minDate, maxDate)
105-
const hasMultipleYears = minDate.split('-')[0] !== maxDate.split('-')[0]
106-
107100
const points: Point[] = remappedData.map((d, index) => [
108101
x(index),
109102
{
@@ -160,9 +153,7 @@ export const MainGraph = ({
160153
.call(
161154
d3
162155
.axisLeft(y)
163-
.tickFormat((v) =>
164-
MetricFormatterShort[data.query.metrics[0] as FormattableMetric](v)
165-
)
156+
.tickFormat((v) => MetricFormatterShort[metric](v))
166157
.ticks(yTickCount)
167158
.tickSize(0)
168159
)
@@ -338,7 +329,17 @@ export const MainGraph = ({
338329
return () => {
339330
svg.selectAll('*').remove()
340331
}
341-
}, [primaryGradient, secondaryGradient, width, data])
332+
}, [
333+
primaryGradient,
334+
secondaryGradient,
335+
width,
336+
remappedData,
337+
yMax,
338+
hasMultipleYears,
339+
period,
340+
interval,
341+
metric
342+
])
342343

343344
return (
344345
<div
@@ -351,51 +352,120 @@ export const MainGraph = ({
351352
className="w-full h-auto"
352353
/>
353354
{tooltip.datum !== null && (
354-
<GraphTooltip x={tooltip.x} y={tooltip.y} datum={tooltip.datum} />
355+
<GraphTooltip
356+
width={width}
357+
showZoomToPeriod={showZoomToPeriod}
358+
shouldShowYear={hasMultipleYears}
359+
period={period}
360+
interval={interval}
361+
metric={metric}
362+
x={tooltip.x}
363+
y={tooltip.y}
364+
datum={tooltip.datum}
365+
/>
355366
)}
356367
</div>
357368
)
358369
}
359370

360371
const GraphTooltip = ({
372+
metric,
373+
interval,
374+
period,
375+
shouldShowYear,
376+
width,
361377
x,
362378
y,
363-
datum
379+
datum,
380+
showZoomToPeriod
364381
}: {
382+
metric: FormattableMetric
383+
interval: string
384+
period: DashboardPeriod
385+
shouldShowYear: boolean
365386
x: number
366387
y: number
367388
datum: GraphDatum
389+
showZoomToPeriod?: boolean
390+
width: number
368391
}) => {
369-
console.log({ x, y, datum })
392+
const formatter = MetricFormatterShort[metric]
393+
const isLeftOfCursor = width - x < 240
370394
return (
371395
<div
372-
className={classNames(
373-
'absolute',
374-
'z-50',
375-
'p-2',
376-
'translate-x-2',
377-
'translate-y-2',
378-
'pointer-events-none',
379-
'rounded-sm',
380-
'bg-white',
381-
'dark:bg-gray-800',
382-
'shadow',
383-
'dark:border-gray-850',
384-
'dark:text-gray-200',
385-
'dark:shadow-gray-850',
386-
'shadow-gray-200'
387-
)}
388396
style={{
389397
left: x,
390398
top: y
391399
}}
400+
className={classNames(
401+
'absolute z-200 bg-gray-800 py-3 px-4 rounded-md z-[100] min-w-[180px] pointer-events-none translate-y-2 shadow shadow-gray-200 dark:shadow-gray-850',
402+
{
403+
'translate-x-2': !isLeftOfCursor,
404+
'-translate-x-full': isLeftOfCursor
405+
}
406+
)}
392407
>
393-
<div className="text-sm font-semibold">{datum.timeLabel}</div>
394-
<div className="flex items-center gap-x-1 text-sm">
395-
<strong className="dark:text-indigo-400">{datum.value}</strong>
396-
{datum.comparisonValue}
397-
{datum.comparisonTimeLabel}
398-
</div>
408+
<aside className="text-sm font-normal text-gray-100 flex flex-col gap-1.5">
409+
<div className="flex justify-between items-center rounded-sm">
410+
<span className="font-semibold mr-4 text-xs uppercase">
411+
{METRIC_LABELS[metric as keyof typeof METRIC_LABELS]}
412+
</span>
413+
{datum.comparisonTimeLabel !== null &&
414+
typeof datum.change === 'number' && (
415+
<div className="inline-flex items-center space-x-1">
416+
<ChangeArrow
417+
className=""
418+
metric={metric as Metric}
419+
change={datum.change}
420+
/>
421+
</div>
422+
)}
423+
</div>
424+
<div className="flex flex-col">
425+
{typeof datum.timeLabel === 'string' && (
426+
<div className="flex flex-row justify-between items-center">
427+
<span className="flex items-center mr-4">
428+
<div className="size-2 mr-2 rounded-full bg-indigo-400"></div>
429+
<span>
430+
{getXLabel(datum.timeLabel, {
431+
period,
432+
interval,
433+
shouldShowYear
434+
})}
435+
</span>
436+
</span>
437+
<span className="font-bold">{formatter(datum.value)}</span>
438+
</div>
439+
)}
440+
441+
{typeof datum.comparisonTimeLabel === 'string' && (
442+
<div className="flex flex-row justify-between items-center">
443+
<span className="flex items-center mr-4">
444+
<div className="size-2 mr-2 rounded-full bg-gray-500"></div>
445+
<span>
446+
{getXLabel(datum.comparisonTimeLabel, {
447+
period,
448+
interval,
449+
shouldShowYear
450+
})}
451+
</span>
452+
</span>
453+
<span className="font-bold">
454+
{formatter(datum.comparisonValue)}
455+
</span>
456+
</div>
457+
)}
458+
</div>
459+
460+
{!!showZoomToPeriod && (
461+
<>
462+
<hr className="border-gray-600 dark:border-gray-800 my-1" />
463+
<span className="text-gray-300 dark:text-gray-400 text-xs">
464+
Click to view {interval}
465+
</span>
466+
</>
467+
)}
468+
</aside>
399469
</div>
400470
)
401471
}
@@ -468,16 +538,11 @@ const remapToGraphData = (
468538
): {
469539
remappedData: GraphDatum[]
470540
yMax: number
471-
resultDefinedRange: [number, number]
472-
comparisonResultDefinedRange: null | [number, number]
541+
hasMultipleYears: boolean
473542
} => {
474543
let yMax: number = 1
475-
const resultDefinedFromBucketIndex = 0
476-
let resultDefinedToBucketIndex = 0
477-
478-
let comparisonDefinedFromBucketIndex: null | number = null
479-
let comparisonDefinedToBucketIndex: null | number = null
480-
544+
let firstTimeLabel: null | string = null
545+
let lastTimeLabel: null | string = null
481546
const remappedData: GraphDatum[] = new Array(
482547
Math.max(
483548
data.meta.comparison_time_label_result_indices?.length ?? 0,
@@ -511,7 +576,10 @@ const remapToGraphData = (
511576

512577
let value: number | null = null
513578
if (mainResultDefined) {
514-
resultDefinedToBucketIndex = index
579+
if (firstTimeLabel === null) {
580+
firstTimeLabel = timeLabel
581+
}
582+
lastTimeLabel = timeLabel
515583
if (indexOfResult !== null) {
516584
const row = data.results[indexOfResult]
517585
if (row!.metrics![0] === null) {
@@ -531,13 +599,9 @@ const remapToGraphData = (
531599
if (value !== null && value > yMax) {
532600
yMax = value
533601
}
534-
602+
let change = null
535603
let comparisonValue = null
536604
if (comparisonResultDefined) {
537-
if (comparisonDefinedFromBucketIndex === null) {
538-
comparisonDefinedFromBucketIndex = index
539-
}
540-
comparisonDefinedToBucketIndex = index
541605
if (indexOfComparisonResult !== null) {
542606
const row = data.comparison_results[indexOfComparisonResult]
543607
if (row!.metrics![0] === null) {
@@ -549,6 +613,7 @@ const remapToGraphData = (
549613
comparisonValue = row!.metrics![0].value
550614
} else if (typeof row!.metrics![0] === 'number') {
551615
comparisonValue = row!.metrics![0]
616+
change = row!.change !== null ? row!.change[0] : null
552617
}
553618
} else {
554619
comparisonValue = 0
@@ -559,21 +624,16 @@ const remapToGraphData = (
559624
yMax = comparisonValue
560625
}
561626

562-
return { value, comparisonValue, timeLabel, comparisonTimeLabel }
627+
return { value, comparisonValue, timeLabel, comparisonTimeLabel, change }
563628
})
564629

630+
const hasMultipleYears =
631+
firstTimeLabel!.split('-')[0] !== lastTimeLabel!.split('-')[0]
632+
565633
return {
566634
remappedData,
567635
yMax,
568-
resultDefinedRange: [
569-
resultDefinedFromBucketIndex,
570-
resultDefinedToBucketIndex
571-
],
572-
comparisonResultDefinedRange:
573-
comparisonDefinedFromBucketIndex !== null &&
574-
comparisonDefinedToBucketIndex !== null
575-
? [comparisonDefinedFromBucketIndex, comparisonDefinedToBucketIndex]
576-
: null
636+
hasMultipleYears
577637
}
578638
}
579639

@@ -611,3 +671,19 @@ const comparisonDotClass = 'fill-indigo-500/20 dark:fill-indigo-400/20'
611671
const sharedPathClass = 'stroke-2'
612672
const mainPathClass = 'stroke-indigo-500 dark:stroke-indigo-400 z-2'
613673
const comparisonPathClass = 'stroke-indigo-500/20 dark:stroke-indigo-400/20 z-1'
674+
675+
const METRIC_LABELS = {
676+
visitors: 'Visitors',
677+
pageviews: 'Pageviews',
678+
events: 'Total conversions',
679+
views_per_visit: 'Views per visit',
680+
visits: 'Visits',
681+
bounce_rate: 'Bounce rate',
682+
visit_duration: 'Visit duration',
683+
conversions: 'Converted visitors',
684+
conversion_rate: 'Conversion rate',
685+
average_revenue: 'Average revenue',
686+
total_revenue: 'Total revenue',
687+
scroll_depth: 'Scroll depth',
688+
time_on_page: 'Time on page'
689+
}

0 commit comments

Comments
 (0)