1- import React , { ReactNode , useEffect , useRef , useState } from 'react'
1+ import React , { ReactNode , useEffect , useMemo , useRef , useState } from 'react'
22import * as d3 from 'd3'
33import { UIMode , useTheme } from '../../theme-context'
44import {
88import { DashboardPeriod } from '../../dashboard-time-periods'
99import dateFormatter from './date-formatter'
1010import classNames from 'classnames'
11+ import { ChangeArrow } from '../reports/change-arrow'
12+ import { Metric } from '../../../types/query-api'
1113
1214const height = 368
1315const marginTop = 16
@@ -18,11 +20,12 @@ const marginLeft = 32
1820type ResultItem = {
1921 dimensions : [ string ] // one item
2022 metrics : null | [ number ] | [ { value : number } ] // one item
21- comparison : { metrics : [ number ] ; change : [ number ] ; dimensions : [ string ] }
2223}
2324type 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
4851type 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
360371const 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'
611671const sharedPathClass = 'stroke-2'
612672const mainPathClass = 'stroke-indigo-500 dark:stroke-indigo-400 z-2'
613673const 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