@automattic/jetpack-components 0.41.1 → 0.41.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/components/boost-score-graph/day-highlight-plugin.ts +82 -0
- package/components/boost-score-graph/get-date-format.ts +59 -0
- package/components/boost-score-graph/index.native.js +1 -0
- package/components/boost-score-graph/index.tsx +68 -0
- package/components/boost-score-graph/stories/index.stories.tsx +187 -0
- package/components/boost-score-graph/stories/tooltip.stories.tsx +41 -0
- package/components/boost-score-graph/style-tooltip.scss +52 -0
- package/components/boost-score-graph/style-uplot.scss +5 -0
- package/components/boost-score-graph/tooltip.tsx +87 -0
- package/components/boost-score-graph/tooltips-plugin.ts +98 -0
- package/components/boost-score-graph/uplot-line-chart.tsx +187 -0
- package/components/boost-score-graph/use-boost-score-transform.ts +33 -0
- package/components/boost-score-graph/use-resize.ts +40 -0
- package/components/product-price/index.tsx +5 -5
- package/components/product-price/style.module.scss +8 -1
- package/index.ts +1 -0
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
### This is a list detailing changes for the Jetpack RNA Components package releases.
|
|
4
4
|
|
|
5
|
+
## [0.41.2] - 2023-08-28
|
|
6
|
+
### Added
|
|
7
|
+
- Add uPlot library and boost score graph component [#32016]
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- UI: Improve discount elements for pricing section [#32545]
|
|
11
|
+
- Updated package dependencies. [#32016]
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- BoostScoreGraph: add mock module to avoid the mobile editor importing incompatible web dependencies. [#32672]
|
|
15
|
+
|
|
5
16
|
## [0.41.1] - 2023-08-09
|
|
6
17
|
### Changed
|
|
7
18
|
- Updated package dependencies. [#32166]
|
|
@@ -783,6 +794,7 @@
|
|
|
783
794
|
### Changed
|
|
784
795
|
- Update node version requirement to 14.16.1
|
|
785
796
|
|
|
797
|
+
[0.41.2]: https://github.com/Automattic/jetpack-components/compare/0.41.1...0.41.2
|
|
786
798
|
[0.41.1]: https://github.com/Automattic/jetpack-components/compare/0.41.0...0.41.1
|
|
787
799
|
[0.41.0]: https://github.com/Automattic/jetpack-components/compare/0.40.4...0.41.0
|
|
788
800
|
[0.40.4]: https://github.com/Automattic/jetpack-components/compare/0.40.3...0.40.4
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import uPlot from 'uplot';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Definition of the column highlight plugin.
|
|
5
|
+
*
|
|
6
|
+
* @returns {object} The uPlot plugin object with hooks.
|
|
7
|
+
*/
|
|
8
|
+
export function dayHighlightPlugin() {
|
|
9
|
+
let overEl, highlightEl;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize the plugin
|
|
13
|
+
*
|
|
14
|
+
* @param {uPlot} u - The uPlot instance.
|
|
15
|
+
*/
|
|
16
|
+
function init( u: uPlot ) {
|
|
17
|
+
overEl = u.over;
|
|
18
|
+
|
|
19
|
+
highlightEl = document.createElement( 'div' );
|
|
20
|
+
|
|
21
|
+
highlightEl.classList.add( 'day-highlighter' );
|
|
22
|
+
|
|
23
|
+
uPlot.assign( highlightEl.style, {
|
|
24
|
+
pointerEvents: 'none',
|
|
25
|
+
display: 'none',
|
|
26
|
+
position: 'absolute',
|
|
27
|
+
left: 0,
|
|
28
|
+
top: 0,
|
|
29
|
+
height: '100%',
|
|
30
|
+
backgroundColor: 'rgba(0,0,0,0.04)',
|
|
31
|
+
} );
|
|
32
|
+
|
|
33
|
+
overEl.appendChild( highlightEl );
|
|
34
|
+
|
|
35
|
+
// show/hide highlight on enter/exit
|
|
36
|
+
overEl.addEventListener( 'mouseenter', () => {
|
|
37
|
+
highlightEl.style.display = null;
|
|
38
|
+
} );
|
|
39
|
+
overEl.addEventListener( 'mouseleave', () => {
|
|
40
|
+
highlightEl.style.display = 'none';
|
|
41
|
+
} );
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* On update
|
|
46
|
+
*
|
|
47
|
+
* @param {uPlot} u - The uPlot instance.
|
|
48
|
+
*/
|
|
49
|
+
function update( u ) {
|
|
50
|
+
const { idx } = u.cursor;
|
|
51
|
+
|
|
52
|
+
// Timestamp of the cursor position
|
|
53
|
+
const timestamp = u.data[ 0 ][ idx ];
|
|
54
|
+
|
|
55
|
+
// Find start and end of day for the cursor position
|
|
56
|
+
const startOfDay = timestamp - ( timestamp % 86400 );
|
|
57
|
+
const endOfDay = startOfDay + 86400;
|
|
58
|
+
|
|
59
|
+
// Find the left position, and width of the box, bounded by the range of the graph
|
|
60
|
+
const boxLeft = u.valToPos( Math.max( startOfDay, u.scales.x.min ), 'x' );
|
|
61
|
+
const boxWidth = u.valToPos( Math.min( endOfDay, u.scales.x.max ), 'x' ) - boxLeft;
|
|
62
|
+
|
|
63
|
+
// Update the highlight box
|
|
64
|
+
highlightEl.style.transform = 'translateX(' + Math.round( boxLeft ) + 'px)';
|
|
65
|
+
highlightEl.style.width = Math.round( boxWidth ) + 'px';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
opts: ( u, opts ) => {
|
|
70
|
+
uPlot.assign( opts, {
|
|
71
|
+
cursor: {
|
|
72
|
+
x: false,
|
|
73
|
+
y: false,
|
|
74
|
+
},
|
|
75
|
+
} );
|
|
76
|
+
},
|
|
77
|
+
hooks: {
|
|
78
|
+
init: init,
|
|
79
|
+
setCursor: update,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// because 'en' defaults to the OS variant - replace numeric month with short month to avoid ambiguity
|
|
2
|
+
const MONTH_FORMAT = 'short';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Returns a formatted date based on the provided template and locale.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} template - The template used to format the date.
|
|
8
|
+
* @param {Date} date - The date object to be formatted.
|
|
9
|
+
* @param {string} [locale='en'] - The locale code specifying the language and region to be used for formatting. Default 'en'.
|
|
10
|
+
* @returns {string} The formatted date as a string.
|
|
11
|
+
*/
|
|
12
|
+
export default function getDateFormat( template: string, date: Date, locale = 'en' ): string {
|
|
13
|
+
let newDayMonthFormat;
|
|
14
|
+
let newYearFormat;
|
|
15
|
+
|
|
16
|
+
switch ( template ) {
|
|
17
|
+
case '{M}/{D}':
|
|
18
|
+
// only show day and month in a local format.
|
|
19
|
+
return date.toLocaleDateString( locale, { month: MONTH_FORMAT, day: 'numeric' } );
|
|
20
|
+
case '{M}/{D}\n{YYYY}':
|
|
21
|
+
// Show day, month and a year in local format.
|
|
22
|
+
newDayMonthFormat = date.toLocaleDateString( locale, {
|
|
23
|
+
month: MONTH_FORMAT,
|
|
24
|
+
day: 'numeric',
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
newYearFormat = date.toLocaleDateString( locale, {
|
|
28
|
+
year: 'numeric',
|
|
29
|
+
} );
|
|
30
|
+
|
|
31
|
+
// add new line to match the format
|
|
32
|
+
return `${ newDayMonthFormat }\n${ newYearFormat }`;
|
|
33
|
+
case '{MMM}':
|
|
34
|
+
// only month
|
|
35
|
+
return date.toLocaleDateString( locale, {
|
|
36
|
+
month: MONTH_FORMAT,
|
|
37
|
+
} );
|
|
38
|
+
case '{MMM}\n{YYYY}':
|
|
39
|
+
// month and year
|
|
40
|
+
newDayMonthFormat = date.toLocaleDateString( locale, {
|
|
41
|
+
month: MONTH_FORMAT,
|
|
42
|
+
} );
|
|
43
|
+
|
|
44
|
+
newYearFormat = date.toLocaleDateString( locale, {
|
|
45
|
+
year: 'numeric',
|
|
46
|
+
} );
|
|
47
|
+
|
|
48
|
+
// add new line to match the format
|
|
49
|
+
return `${ newDayMonthFormat }\n${ newYearFormat }`;
|
|
50
|
+
case '{h}{aa}':
|
|
51
|
+
return '';
|
|
52
|
+
default:
|
|
53
|
+
// only show day and month in a local format.
|
|
54
|
+
return date.toLocaleDateString( locale, { month: MONTH_FORMAT, day: 'numeric' } );
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// fallback that shouldn't happen.
|
|
58
|
+
return date.toLocaleDateString( locale );
|
|
59
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default () => null;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React, { type FunctionComponent } from 'react';
|
|
2
|
+
import uPlot from 'uplot';
|
|
3
|
+
import Text from '../text';
|
|
4
|
+
import UplotLineChart from './uplot-line-chart';
|
|
5
|
+
import { useBoostScoreTransform } from './use-boost-score-transform';
|
|
6
|
+
|
|
7
|
+
export interface Period {
|
|
8
|
+
timestamp: number;
|
|
9
|
+
dimensions: {
|
|
10
|
+
desktop_overall_score: number;
|
|
11
|
+
desktop_lcp: number;
|
|
12
|
+
desktop_cls: number;
|
|
13
|
+
desktop_tbt: number;
|
|
14
|
+
mobile_overall_score: number;
|
|
15
|
+
mobile_lcp: number;
|
|
16
|
+
mobile_cls: number;
|
|
17
|
+
mobile_tbt: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface BoostScoreGraphProps {
|
|
21
|
+
periods: Period[];
|
|
22
|
+
startDate: number;
|
|
23
|
+
endDate: number;
|
|
24
|
+
title?: string;
|
|
25
|
+
isLoading?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* BoostScoreGraph component composed by the chart and the legend.
|
|
30
|
+
*
|
|
31
|
+
* @param {BoostScoreGraphProps} props - The props object for the BoostScoreGraph component.
|
|
32
|
+
* @param {uPlot.AlignedData} props.data - The data used to render the uPlotLineChart.
|
|
33
|
+
* @param {string} props.title - Title for the chart.
|
|
34
|
+
* @param {boolean} [props.isLoading=false] - Whether the component is in a loading state.
|
|
35
|
+
* @returns {React.ReactElement} The JSX element representing the BoostScoreGraph component, or null if loading.
|
|
36
|
+
*/
|
|
37
|
+
export const BoostScoreGraph: FunctionComponent< BoostScoreGraphProps > = ( {
|
|
38
|
+
periods,
|
|
39
|
+
startDate,
|
|
40
|
+
endDate,
|
|
41
|
+
title,
|
|
42
|
+
isLoading = false,
|
|
43
|
+
} ) => {
|
|
44
|
+
// Sort periods by timestamp
|
|
45
|
+
periods.sort( ( a, b ) => a.timestamp - b.timestamp );
|
|
46
|
+
|
|
47
|
+
// @todo Remove this once we have a proper date range picker
|
|
48
|
+
const dayBeforeEndDate = endDate - 24 * 60 * 60 * 1000;
|
|
49
|
+
// Adjust the start date based on available data
|
|
50
|
+
if ( periods.length > 0 ) {
|
|
51
|
+
startDate = Math.min( periods[ 0 ].timestamp - 12 * 60 * 60 * 1000, dayBeforeEndDate );
|
|
52
|
+
} else {
|
|
53
|
+
startDate = dayBeforeEndDate;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = useBoostScoreTransform( periods );
|
|
57
|
+
if ( isLoading || ! data?.length ) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return (
|
|
61
|
+
<div className="jb-score-graph">
|
|
62
|
+
{ title && <Text variant="title-medium">{ title }</Text> }
|
|
63
|
+
<UplotLineChart data={ data } periods={ periods } range={ { startDate, endDate } } />
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default BoostScoreGraph;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { BoostScoreGraph } from '..';
|
|
2
|
+
import type { Meta } from '@storybook/react';
|
|
3
|
+
|
|
4
|
+
const exampleRawResponse = {
|
|
5
|
+
data: {
|
|
6
|
+
_meta: {
|
|
7
|
+
start: 1689772803000,
|
|
8
|
+
end: 1690647000000,
|
|
9
|
+
},
|
|
10
|
+
periods: [
|
|
11
|
+
{
|
|
12
|
+
timestamp: 1690636803000,
|
|
13
|
+
dimensions: {
|
|
14
|
+
desktop_overall_score: 86,
|
|
15
|
+
mobile_overall_score: 52,
|
|
16
|
+
desktop_cls: 0.088,
|
|
17
|
+
desktop_lcp: 3.2,
|
|
18
|
+
desktop_tbt: 0,
|
|
19
|
+
mobile_cls: 0.088,
|
|
20
|
+
mobile_lcp: 3.2,
|
|
21
|
+
mobile_tbt: 0,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
timestamp: 1689772803000,
|
|
26
|
+
dimensions: {
|
|
27
|
+
desktop_overall_score: 75,
|
|
28
|
+
mobile_overall_score: 52,
|
|
29
|
+
desktop_cls: 0.088,
|
|
30
|
+
desktop_lcp: 3.2,
|
|
31
|
+
desktop_tbt: 0,
|
|
32
|
+
mobile_cls: 0.088,
|
|
33
|
+
mobile_lcp: 3.2,
|
|
34
|
+
mobile_tbt: 0,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
timestamp: 1689859203000,
|
|
39
|
+
dimensions: {
|
|
40
|
+
desktop_overall_score: 72,
|
|
41
|
+
mobile_overall_score: 49,
|
|
42
|
+
desktop_cls: 0.088,
|
|
43
|
+
desktop_lcp: 3.2,
|
|
44
|
+
desktop_tbt: 0,
|
|
45
|
+
mobile_cls: 0.088,
|
|
46
|
+
mobile_lcp: 3.2,
|
|
47
|
+
mobile_tbt: 0,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
timestamp: 1689945603000,
|
|
52
|
+
dimensions: {
|
|
53
|
+
desktop_overall_score: 20,
|
|
54
|
+
mobile_overall_score: 30,
|
|
55
|
+
desktop_cls: 0.088,
|
|
56
|
+
desktop_lcp: 3.2,
|
|
57
|
+
desktop_tbt: 0,
|
|
58
|
+
mobile_cls: 0.088,
|
|
59
|
+
mobile_lcp: 3.2,
|
|
60
|
+
mobile_tbt: 0,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
timestamp: 1690032003000,
|
|
65
|
+
dimensions: {
|
|
66
|
+
desktop_overall_score: 72,
|
|
67
|
+
mobile_overall_score: 40,
|
|
68
|
+
desktop_cls: 0.088,
|
|
69
|
+
desktop_lcp: 3.2,
|
|
70
|
+
desktop_tbt: 0,
|
|
71
|
+
mobile_cls: 0.088,
|
|
72
|
+
mobile_lcp: 3.2,
|
|
73
|
+
mobile_tbt: 0,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
timestamp: 1690118403000,
|
|
78
|
+
dimensions: {
|
|
79
|
+
desktop_overall_score: 55,
|
|
80
|
+
mobile_overall_score: 45,
|
|
81
|
+
desktop_cls: 0.088,
|
|
82
|
+
desktop_lcp: 3.2,
|
|
83
|
+
desktop_tbt: 0,
|
|
84
|
+
mobile_cls: 0.088,
|
|
85
|
+
mobile_lcp: 3.2,
|
|
86
|
+
mobile_tbt: 0,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
timestamp: 1690204803000,
|
|
91
|
+
dimensions: {
|
|
92
|
+
desktop_overall_score: 75,
|
|
93
|
+
mobile_overall_score: 52,
|
|
94
|
+
desktop_cls: 0.088,
|
|
95
|
+
desktop_lcp: 3.2,
|
|
96
|
+
desktop_tbt: 0,
|
|
97
|
+
mobile_cls: 0.088,
|
|
98
|
+
mobile_lcp: 3.2,
|
|
99
|
+
mobile_tbt: 0,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
timestamp: 1690291203000,
|
|
104
|
+
dimensions: {
|
|
105
|
+
desktop_overall_score: 70,
|
|
106
|
+
mobile_overall_score: 50,
|
|
107
|
+
desktop_cls: 0.088,
|
|
108
|
+
desktop_lcp: 3.2,
|
|
109
|
+
desktop_tbt: 0,
|
|
110
|
+
mobile_cls: 0.088,
|
|
111
|
+
mobile_lcp: 3.2,
|
|
112
|
+
mobile_tbt: 0,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
timestamp: 1690377603000,
|
|
117
|
+
dimensions: {
|
|
118
|
+
desktop_overall_score: 75,
|
|
119
|
+
mobile_overall_score: 90,
|
|
120
|
+
desktop_cls: 0.088,
|
|
121
|
+
desktop_lcp: 3.2,
|
|
122
|
+
desktop_tbt: 0,
|
|
123
|
+
mobile_cls: 0.088,
|
|
124
|
+
mobile_lcp: 3.2,
|
|
125
|
+
mobile_tbt: 0,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
timestamp: 1690464003000,
|
|
130
|
+
dimensions: {
|
|
131
|
+
desktop_overall_score: 80,
|
|
132
|
+
mobile_overall_score: 60,
|
|
133
|
+
desktop_cls: 0.088,
|
|
134
|
+
desktop_lcp: 3.2,
|
|
135
|
+
desktop_tbt: 0,
|
|
136
|
+
mobile_cls: 0.088,
|
|
137
|
+
mobile_lcp: 3.2,
|
|
138
|
+
mobile_tbt: 0,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
timestamp: 1690550403000,
|
|
143
|
+
dimensions: {
|
|
144
|
+
desktop_overall_score: 85,
|
|
145
|
+
mobile_overall_score: 60,
|
|
146
|
+
desktop_cls: 0.088,
|
|
147
|
+
desktop_lcp: 3.2,
|
|
148
|
+
desktop_tbt: 0,
|
|
149
|
+
mobile_cls: 0.088,
|
|
150
|
+
mobile_lcp: 3.2,
|
|
151
|
+
mobile_tbt: 0,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const meta: Meta< typeof BoostScoreGraph > = {
|
|
159
|
+
title: 'JS Packages/Components/Boost Score Graph',
|
|
160
|
+
component: BoostScoreGraph,
|
|
161
|
+
argTypes: {
|
|
162
|
+
startDate: { control: 'date' },
|
|
163
|
+
endDate: { control: 'date' },
|
|
164
|
+
title: { control: 'string', defaultValue: 'Title' },
|
|
165
|
+
isLoading: { control: 'boolean', defaultValue: false },
|
|
166
|
+
},
|
|
167
|
+
decorators: [
|
|
168
|
+
Story => (
|
|
169
|
+
<div style={ { width: '80%', maxWidth: '1320px', margin: '200px auto', fontSize: '16px' } }>
|
|
170
|
+
<Story />
|
|
171
|
+
</div>
|
|
172
|
+
),
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const defaultValues = {
|
|
177
|
+
startDate: exampleRawResponse.data._meta.start,
|
|
178
|
+
endDate: exampleRawResponse.data._meta.end,
|
|
179
|
+
periods: exampleRawResponse.data.periods,
|
|
180
|
+
isLoading: false,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export default meta;
|
|
184
|
+
|
|
185
|
+
const Template = args => <BoostScoreGraph { ...args } />;
|
|
186
|
+
export const _default = Template.bind( {} );
|
|
187
|
+
_default.args = defaultValues;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Tooltip } from '../tooltip';
|
|
2
|
+
import type { Meta } from '@storybook/react';
|
|
3
|
+
|
|
4
|
+
const meta: Meta< typeof Tooltip > = {
|
|
5
|
+
title: 'JS Packages/Components/Boost Score Tooltip',
|
|
6
|
+
component: Tooltip,
|
|
7
|
+
argTypes: {
|
|
8
|
+
period: {
|
|
9
|
+
control: 'object',
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
decorators: [
|
|
13
|
+
Story => (
|
|
14
|
+
<div style={ { width: '300px', margin: '200px auto', fontSize: '16px' } }>
|
|
15
|
+
<Story />
|
|
16
|
+
</div>
|
|
17
|
+
),
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
|
|
23
|
+
const Template = args => {
|
|
24
|
+
return <Tooltip { ...args } />;
|
|
25
|
+
};
|
|
26
|
+
export const _default = Template.bind( {} );
|
|
27
|
+
_default.args = {
|
|
28
|
+
period: {
|
|
29
|
+
timestamp: 1689772803000,
|
|
30
|
+
dimensions: {
|
|
31
|
+
desktop_overall_score: 75,
|
|
32
|
+
mobile_overall_score: 52,
|
|
33
|
+
desktop_cls: 0.088,
|
|
34
|
+
desktop_lcp: 3.2,
|
|
35
|
+
desktop_tbt: 0,
|
|
36
|
+
mobile_cls: 0.088,
|
|
37
|
+
mobile_lcp: 3.2,
|
|
38
|
+
mobile_tbt: 0,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
$grey: #8e8e8e;
|
|
2
|
+
$black: #101517;
|
|
3
|
+
$white: #ffffff;
|
|
4
|
+
|
|
5
|
+
.jb-score-tooltip {
|
|
6
|
+
background-color: $black;
|
|
7
|
+
color: $white;
|
|
8
|
+
width: fit-content;
|
|
9
|
+
padding: 16px 24px;
|
|
10
|
+
border-radius: 4px;
|
|
11
|
+
font-size: 14px;
|
|
12
|
+
width: 20em;
|
|
13
|
+
position: relative;
|
|
14
|
+
box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05);
|
|
15
|
+
|
|
16
|
+
hr {
|
|
17
|
+
border-top: 1px solid $grey;
|
|
18
|
+
border-bottom: none;
|
|
19
|
+
border-left: none;
|
|
20
|
+
border-right: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
&__date {
|
|
24
|
+
font-size: 1em;
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
line-height: 1.6em;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&__row {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: row;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
margin: 0.9em 0;
|
|
34
|
+
|
|
35
|
+
&--secondary {
|
|
36
|
+
color: $grey;
|
|
37
|
+
margin-left: 1em;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&__pointer {
|
|
42
|
+
width: 0;
|
|
43
|
+
height: 0;
|
|
44
|
+
border-left: 8px solid transparent;
|
|
45
|
+
border-right: 8px solid transparent;
|
|
46
|
+
border-bottom: 8px solid $black;
|
|
47
|
+
position: absolute;
|
|
48
|
+
top: -7px;
|
|
49
|
+
left: 50%;
|
|
50
|
+
transform: translateX(-50%);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/*
|
|
2
|
+
This is a bit of a cheating. The content are copied from node_modules/uplot/dist/uPlot.min.css.
|
|
3
|
+
This is because if the css file imports directly from the css file sass-loader is not loading it. And if we import it from the js file, it breaks jest.
|
|
4
|
+
*/
|
|
5
|
+
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;z-index: 100;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;z-index: 100;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { getScoreLetter } from '@automattic/jetpack-boost-score-api';
|
|
2
|
+
import { dateI18n } from '@wordpress/date';
|
|
3
|
+
import { __, sprintf } from '@wordpress/i18n';
|
|
4
|
+
import { type FunctionComponent } from 'react';
|
|
5
|
+
import './style-tooltip.scss';
|
|
6
|
+
import { Period } from '.';
|
|
7
|
+
|
|
8
|
+
export const Tooltip: FunctionComponent = ( { period }: { period: Period } ) => {
|
|
9
|
+
if ( ! period || ! period.dimensions || ! period.timestamp ) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const {
|
|
13
|
+
mobile_overall_score,
|
|
14
|
+
desktop_overall_score,
|
|
15
|
+
desktop_cls,
|
|
16
|
+
desktop_lcp,
|
|
17
|
+
desktop_tbt,
|
|
18
|
+
mobile_cls,
|
|
19
|
+
mobile_lcp,
|
|
20
|
+
mobile_tbt,
|
|
21
|
+
} = period.dimensions;
|
|
22
|
+
const scoreLetter = getScoreLetter( mobile_overall_score, desktop_overall_score );
|
|
23
|
+
const date = dateI18n( 'F j, Y', new Date( period.timestamp ), false );
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="jb-score-tooltip">
|
|
27
|
+
<div className="jb-score-tooltip__date">{ date }</div>
|
|
28
|
+
<div className="jb-score-tooltip__row">
|
|
29
|
+
<div className="jb-score-tooltip__column">{ __( 'Overall score', 'jetpack' ) }</div>
|
|
30
|
+
<div className="jb-score-tooltip__column">{ scoreLetter }</div>
|
|
31
|
+
</div>
|
|
32
|
+
<hr />
|
|
33
|
+
<div className="jb-score-tooltip__row">
|
|
34
|
+
<div className="jb-score-tooltip__column">{ __( 'Desktop score', 'jetpack' ) }</div>
|
|
35
|
+
<div className="jb-score-tooltip__column">
|
|
36
|
+
{
|
|
37
|
+
/* translators: %d is the score */
|
|
38
|
+
sprintf( __( '%d / 100', 'jetpack' ), desktop_overall_score )
|
|
39
|
+
}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div className="jb-score-tooltip__row jb-score-tooltip__row--secondary">
|
|
43
|
+
<div className="jb-score-tooltip__column">
|
|
44
|
+
{ __( 'Largest Contentful Paint', 'jetpack' ) }
|
|
45
|
+
</div>
|
|
46
|
+
<div className="jb-score-tooltip__column">{ sprintf( '%0.2fs', desktop_lcp ) }</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="jb-score-tooltip__row jb-score-tooltip__row--secondary">
|
|
49
|
+
<div className="jb-score-tooltip__column">{ __( 'Total Blocking Time', 'jetpack' ) }</div>
|
|
50
|
+
<div className="jb-score-tooltip__column">{ sprintf( '%0.2fs', desktop_tbt ) }</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="jb-score-tooltip__row jb-score-tooltip__row--secondary">
|
|
53
|
+
<div className="jb-score-tooltip__column">
|
|
54
|
+
{ __( 'Cumulative Layout Shift', 'jetpack' ) }
|
|
55
|
+
</div>
|
|
56
|
+
<div className="jb-score-tooltip__column">{ sprintf( '%0.2f', desktop_cls ) }</div>
|
|
57
|
+
</div>
|
|
58
|
+
<hr />
|
|
59
|
+
<div className="jb-score-tooltip__row">
|
|
60
|
+
<div className="jb-score-tooltip__column">{ __( 'Mobile score', 'jetpack' ) }</div>
|
|
61
|
+
<div className="jb-score-tooltip__column">
|
|
62
|
+
{
|
|
63
|
+
/* translators: %d is the score */
|
|
64
|
+
sprintf( __( '%d / 100', 'jetpack' ), mobile_overall_score )
|
|
65
|
+
}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="jb-score-tooltip__row jb-score-tooltip__row--secondary">
|
|
69
|
+
<div className="jb-score-tooltip__column">
|
|
70
|
+
{ __( 'Largest Contentful Paint', 'jetpack' ) }
|
|
71
|
+
</div>
|
|
72
|
+
<div className="jb-score-tooltip__column">{ sprintf( '%0.2fs', mobile_lcp ) }</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="jb-score-tooltip__row jb-score-tooltip__row--secondary">
|
|
75
|
+
<div className="jb-score-tooltip__column">{ __( 'Total Blocking Time', 'jetpack' ) }</div>
|
|
76
|
+
<div className="jb-score-tooltip__column">{ sprintf( '%0.2fs', mobile_tbt ) }</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="jb-score-tooltip__row jb-score-tooltip__row--secondary">
|
|
79
|
+
<div className="jb-score-tooltip__column">
|
|
80
|
+
{ __( 'Cumulative Layout Shift', 'jetpack' ) }
|
|
81
|
+
</div>
|
|
82
|
+
<div className="jb-score-tooltip__column">{ sprintf( '%0.2f', mobile_cls ) }</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="jb-score-tooltip__pointer"></div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import ReactDOM from 'react-dom/client';
|
|
2
|
+
import uPlot from 'uplot';
|
|
3
|
+
import { Tooltip } from './tooltip';
|
|
4
|
+
import { Period } from '.';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Custom tooltips plugin for uPlot.
|
|
8
|
+
*
|
|
9
|
+
* @param {Period[]} periods - The periods to display in the tooltip.
|
|
10
|
+
* @returns {object} The uPlot plugin object with hooks.
|
|
11
|
+
*/
|
|
12
|
+
export function tooltipsPlugin( periods ) {
|
|
13
|
+
const reactRoot = document.createElement( 'div' );
|
|
14
|
+
let reactDom;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initializes the tooltips plugin.
|
|
18
|
+
*
|
|
19
|
+
* @param {uPlot} u - The uPlot instance.
|
|
20
|
+
* @param {object} _opts - Options for the uPlot instance.
|
|
21
|
+
*/
|
|
22
|
+
function init( u, _opts ) {
|
|
23
|
+
const over = u.over;
|
|
24
|
+
reactDom = ReactDOM.createRoot( reactRoot );
|
|
25
|
+
reactRoot.style.position = 'absolute';
|
|
26
|
+
reactRoot.style.bottom = -20 + 'px';
|
|
27
|
+
reactRoot.style.translate = '-50% calc( 100% - 20px )';
|
|
28
|
+
|
|
29
|
+
over.appendChild( reactRoot );
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Hides all tooltips.
|
|
33
|
+
*/
|
|
34
|
+
function hideTips() {
|
|
35
|
+
reactRoot.style.display = 'none';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Shows all tooltips.
|
|
40
|
+
*/
|
|
41
|
+
function showTips() {
|
|
42
|
+
reactRoot.style.display = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
over.addEventListener( 'mouseleave', () => {
|
|
46
|
+
if ( ! u.cursor._lock ) {
|
|
47
|
+
hideTips();
|
|
48
|
+
}
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
over.addEventListener( 'mouseenter', () => {
|
|
52
|
+
showTips();
|
|
53
|
+
} );
|
|
54
|
+
|
|
55
|
+
if ( u.cursor.left < 0 ) {
|
|
56
|
+
hideTips();
|
|
57
|
+
} else {
|
|
58
|
+
showTips();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sets the cursor for tooltips.
|
|
64
|
+
*
|
|
65
|
+
* @param {uPlot} u - The uPlot instance.
|
|
66
|
+
*/
|
|
67
|
+
function setCursor( u ) {
|
|
68
|
+
const { idx } = u.cursor;
|
|
69
|
+
|
|
70
|
+
const period = periods[ idx ];
|
|
71
|
+
|
|
72
|
+
// Timestamp of the cursor position
|
|
73
|
+
const timestamp = u.data[ 0 ][ idx ];
|
|
74
|
+
|
|
75
|
+
// Find start and end of day for the cursor position
|
|
76
|
+
const startOfDay = timestamp - ( timestamp % 86400 );
|
|
77
|
+
const endOfDay = startOfDay + 86400;
|
|
78
|
+
|
|
79
|
+
// Find the left position, and width of the box, bounded by the range of the graph
|
|
80
|
+
const boxLeft = u.valToPos( Math.max( startOfDay, u.scales.x.min ), 'x' );
|
|
81
|
+
const boxWidth = u.valToPos( Math.min( endOfDay, u.scales.x.max ), 'x' ) - boxLeft;
|
|
82
|
+
const boxCenter = boxLeft + boxWidth / 2;
|
|
83
|
+
|
|
84
|
+
reactRoot.style.left = boxCenter + 'px';
|
|
85
|
+
reactDom.render(
|
|
86
|
+
Tooltip( {
|
|
87
|
+
period,
|
|
88
|
+
} )
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
hooks: {
|
|
94
|
+
init,
|
|
95
|
+
setCursor,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { __ } from '@wordpress/i18n';
|
|
2
|
+
import React, { useMemo, useRef, useCallback } from 'react';
|
|
3
|
+
import uPlot from 'uplot';
|
|
4
|
+
import UplotReact from 'uplot-react';
|
|
5
|
+
import { getUserLocale } from '../../lib/locale';
|
|
6
|
+
import numberFormat from '../number-format';
|
|
7
|
+
import { dayHighlightPlugin } from './day-highlight-plugin';
|
|
8
|
+
import getDateFormat from './get-date-format';
|
|
9
|
+
import { tooltipsPlugin } from './tooltips-plugin';
|
|
10
|
+
import useResize from './use-resize';
|
|
11
|
+
import { Period } from '.';
|
|
12
|
+
|
|
13
|
+
import './style-uplot.scss';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_DIMENSIONS = {
|
|
16
|
+
height: 300,
|
|
17
|
+
width: 600,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface UplotChartProps {
|
|
21
|
+
data: uPlot.AlignedData;
|
|
22
|
+
periods: Period[];
|
|
23
|
+
options?: Partial< uPlot.Options >;
|
|
24
|
+
legendContainer?: React.RefObject< HTMLDivElement >;
|
|
25
|
+
solidFill?: boolean;
|
|
26
|
+
period?: string;
|
|
27
|
+
range?: { startDate: number; endDate: number };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a series information object for uPlot based on the label and color.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} label - The label for the series.
|
|
34
|
+
* @param {number} score - The last score for the series.
|
|
35
|
+
* @returns {object} The series information object.
|
|
36
|
+
*/
|
|
37
|
+
function createSerieInfo( label: string, score ) {
|
|
38
|
+
const { spline } = uPlot.paths;
|
|
39
|
+
return {
|
|
40
|
+
label: label,
|
|
41
|
+
stroke: getColor( score ),
|
|
42
|
+
fill: u => {
|
|
43
|
+
const gradient = u.ctx.createLinearGradient( 0, 0, 0, DEFAULT_DIMENSIONS.height );
|
|
44
|
+
gradient.addColorStop( 0, getColor( score, '44' ) );
|
|
45
|
+
gradient.addColorStop( 1, getColor( score, '11' ) );
|
|
46
|
+
|
|
47
|
+
return gradient;
|
|
48
|
+
}, // use the gradient as fill for the series
|
|
49
|
+
width: 2,
|
|
50
|
+
paths: ( u, seriesIdx, idx0, idx1 ) => {
|
|
51
|
+
return spline?.()( u, seriesIdx, idx0, idx1 ) || null;
|
|
52
|
+
},
|
|
53
|
+
points: {
|
|
54
|
+
show: true,
|
|
55
|
+
},
|
|
56
|
+
value: ( self: uPlot, rawValue: number ) => {
|
|
57
|
+
if ( ! rawValue ) {
|
|
58
|
+
return '-';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return numberFormat( rawValue );
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the color value based on the score.
|
|
68
|
+
*
|
|
69
|
+
* @param {number} score - The score to get the color for.
|
|
70
|
+
* @param {string} opacity - Whether to return a transparent color.
|
|
71
|
+
* @returns {string} The color value.
|
|
72
|
+
*/
|
|
73
|
+
function getColor( score: number, opacity = 'FF' ) {
|
|
74
|
+
let color = '#D63638'; // bad
|
|
75
|
+
|
|
76
|
+
if ( score > 70 ) {
|
|
77
|
+
color = '#069e08'; // good
|
|
78
|
+
} else if ( score > 50 ) {
|
|
79
|
+
color = '#faa754'; //mediocre
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return `${ color }${ opacity }`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* UplotLineChart component.
|
|
87
|
+
*
|
|
88
|
+
* @param {object} props - The props object for the UplotLineChart component.
|
|
89
|
+
* @param {uPlot.AlignedData} props.data - The data for the uPlot chart.
|
|
90
|
+
* @param {{ startDate: number, endDate: number }} props.range - The date range of the chart.
|
|
91
|
+
* @param {Period[]} props.periods - The periods to display in the chart.
|
|
92
|
+
* @returns {React.Element} The JSX element representing the UplotLineChart component.
|
|
93
|
+
*/
|
|
94
|
+
export default function UplotLineChart( { data, range, periods }: UplotChartProps ) {
|
|
95
|
+
const uplot = useRef< uPlot | null >( null );
|
|
96
|
+
const uplotContainer = useRef( null );
|
|
97
|
+
|
|
98
|
+
const lastDesktopScore = data[ 1 ][ data[ 1 ].length - 1 ];
|
|
99
|
+
const lastMobileScore = data[ 2 ][ data[ 2 ].length - 1 ];
|
|
100
|
+
|
|
101
|
+
const options: uPlot.Options = useMemo( () => {
|
|
102
|
+
const defaultOptions: uPlot.Options = {
|
|
103
|
+
class: 'boost-score-graph',
|
|
104
|
+
...DEFAULT_DIMENSIONS,
|
|
105
|
+
tzDate: ts => uPlot.tzDate( new Date( ts * 1e3 ), 'Etc/UTC' ),
|
|
106
|
+
fmtDate: ( chartDateStringTemplate: string ) => {
|
|
107
|
+
return date => getDateFormat( chartDateStringTemplate, date, getUserLocale() );
|
|
108
|
+
},
|
|
109
|
+
padding: [ 17, 0, 17, 0 ],
|
|
110
|
+
axes: [
|
|
111
|
+
{
|
|
112
|
+
// x-axis
|
|
113
|
+
grid: {
|
|
114
|
+
show: false,
|
|
115
|
+
},
|
|
116
|
+
ticks: {
|
|
117
|
+
stroke: '#50575E',
|
|
118
|
+
width: 1,
|
|
119
|
+
size: 3,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
// y-axis
|
|
124
|
+
side: 1,
|
|
125
|
+
gap: 8,
|
|
126
|
+
space: 100,
|
|
127
|
+
size: 30,
|
|
128
|
+
grid: {
|
|
129
|
+
stroke: 'rgba(220, 220, 222, 0.5)', // #DCDCDE with 0.5 opacity
|
|
130
|
+
width: 1,
|
|
131
|
+
},
|
|
132
|
+
ticks: {
|
|
133
|
+
show: false,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
cursor: {
|
|
138
|
+
x: false,
|
|
139
|
+
y: false,
|
|
140
|
+
},
|
|
141
|
+
series: [
|
|
142
|
+
{
|
|
143
|
+
label: __( 'Date', 'jetpack' ),
|
|
144
|
+
value: ( self: uPlot, rawValue: number ) => {
|
|
145
|
+
// outputs legend content - value available when mouse is hovering the chart
|
|
146
|
+
if ( ! rawValue ) {
|
|
147
|
+
return '-';
|
|
148
|
+
}
|
|
149
|
+
const date = new Date( rawValue );
|
|
150
|
+
return date.toLocaleDateString( getUserLocale() );
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
createSerieInfo( __( 'Desktop', 'jetpack' ), lastDesktopScore ),
|
|
154
|
+
createSerieInfo( __( 'Mobile', 'jetpack' ), lastMobileScore ),
|
|
155
|
+
],
|
|
156
|
+
scales: {
|
|
157
|
+
x: {
|
|
158
|
+
time: true,
|
|
159
|
+
auto: false,
|
|
160
|
+
range: [ range.startDate / 1000, range.endDate / 1000 ],
|
|
161
|
+
},
|
|
162
|
+
y: {
|
|
163
|
+
range: [ 0, 100 ],
|
|
164
|
+
auto: false,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
legend: {
|
|
168
|
+
show: false,
|
|
169
|
+
},
|
|
170
|
+
plugins: [ tooltipsPlugin( periods ), dayHighlightPlugin() ],
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
...defaultOptions,
|
|
174
|
+
};
|
|
175
|
+
}, [ lastDesktopScore, lastMobileScore, periods, range.endDate, range.startDate ] );
|
|
176
|
+
|
|
177
|
+
useResize( uplot, uplotContainer );
|
|
178
|
+
const onCreate = useCallback( chart => {
|
|
179
|
+
return ( uplot.current = chart );
|
|
180
|
+
}, [] );
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div ref={ uplotContainer }>
|
|
184
|
+
<UplotReact data={ data } onCreate={ onCreate } options={ options } />
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import uPlot from 'uplot';
|
|
3
|
+
import { Period } from './index';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Transforms an array of periods into an array of arrays, where the first array is the timestamps, and the rest are the values for each key
|
|
7
|
+
*
|
|
8
|
+
* @param {Period[]} periods - Array of periods to transform
|
|
9
|
+
* @param {string[]} [keysToExtract = [ 'desktop_overall_score', 'mobile_overall_score' ]] - Array of keys to extract from each period
|
|
10
|
+
* @returns {uPlot.AlignedData} - Array of arrays, where the first array is the timestamps, and the rest are the values for each key
|
|
11
|
+
*/
|
|
12
|
+
export function useBoostScoreTransform(
|
|
13
|
+
periods,
|
|
14
|
+
keysToExtract = [ 'desktop_overall_score', 'mobile_overall_score' ]
|
|
15
|
+
): uPlot.AlignedData {
|
|
16
|
+
return useMemo( () => {
|
|
17
|
+
if ( ! periods?.length || ! periods[ 0 ].dimensions ) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const timestamps = periods.map( ( { timestamp } ) => timestamp / 1000 );
|
|
21
|
+
|
|
22
|
+
const valueArray = [];
|
|
23
|
+
for ( const key of keysToExtract ) {
|
|
24
|
+
valueArray.push(
|
|
25
|
+
periods.map( ( { dimensions } ) => {
|
|
26
|
+
return dimensions[ key ];
|
|
27
|
+
} )
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [ timestamps, ...valueArray ];
|
|
32
|
+
}, [ keysToExtract, periods ] );
|
|
33
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { throttle } from '@wordpress/compose';
|
|
2
|
+
import React, { useEffect } from 'react';
|
|
3
|
+
import uPlot from 'uplot';
|
|
4
|
+
|
|
5
|
+
const THROTTLE_DURATION = 400; // in ms
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Custom hook to handle resizing of uPlot charts.
|
|
9
|
+
*
|
|
10
|
+
* @param {React.RefObject<uPlot>} uplotRef - The ref object for the uPlot instance.
|
|
11
|
+
* @param {React.RefObject<HTMLDivElement>} containerRef - The ref object for the container div.
|
|
12
|
+
*/
|
|
13
|
+
export default function useResize(
|
|
14
|
+
uplotRef: React.RefObject< uPlot >,
|
|
15
|
+
containerRef: React.RefObject< HTMLDivElement >
|
|
16
|
+
) {
|
|
17
|
+
useEffect( () => {
|
|
18
|
+
if ( ! uplotRef.current || ! containerRef.current ) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const resizeChart = throttle( () => {
|
|
23
|
+
// Repeat the check since resize can happen much later than event registration.
|
|
24
|
+
if ( ! uplotRef.current || ! containerRef.current ) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Only update width, not height.
|
|
29
|
+
uplotRef.current.setSize( {
|
|
30
|
+
height: uplotRef.current.height,
|
|
31
|
+
width: containerRef.current.clientWidth,
|
|
32
|
+
} );
|
|
33
|
+
}, THROTTLE_DURATION );
|
|
34
|
+
resizeChart();
|
|
35
|
+
window.addEventListener( 'resize', resizeChart );
|
|
36
|
+
|
|
37
|
+
// Cleanup on unmount.
|
|
38
|
+
return () => window.removeEventListener( 'resize', resizeChart );
|
|
39
|
+
}, [ uplotRef, containerRef ] );
|
|
40
|
+
}
|
|
@@ -63,6 +63,11 @@ const ProductPrice: React.FC< ProductPriceProps > = ( {
|
|
|
63
63
|
hidePriceFraction={ hidePriceFraction }
|
|
64
64
|
/>
|
|
65
65
|
) }
|
|
66
|
+
{ discountElt && (
|
|
67
|
+
<Text className={ classnames( styles[ 'promo-label' ], 'product-price_promo_label' ) }>
|
|
68
|
+
{ discountElt }
|
|
69
|
+
</Text>
|
|
70
|
+
) }
|
|
66
71
|
</div>
|
|
67
72
|
</div>
|
|
68
73
|
<div className={ styles.footer }>
|
|
@@ -76,11 +81,6 @@ const ProductPrice: React.FC< ProductPriceProps > = ( {
|
|
|
76
81
|
{ promoLabel }
|
|
77
82
|
</Text>
|
|
78
83
|
) }
|
|
79
|
-
{ discountElt && (
|
|
80
|
-
<Text className={ classnames( styles[ 'promo-label' ], 'product-price_promo_label' ) }>
|
|
81
|
-
{ discountElt }
|
|
82
|
-
</Text>
|
|
83
|
-
) }
|
|
84
84
|
</div>
|
|
85
85
|
</>
|
|
86
86
|
);
|
|
@@ -6,14 +6,21 @@
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
.price-container {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: flex-start;
|
|
9
11
|
flex-grow: 2;
|
|
10
12
|
flex-basis: 0;
|
|
13
|
+
|
|
14
|
+
.promo-label {
|
|
15
|
+
margin-left: auto;
|
|
16
|
+
}
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
.price {
|
|
14
20
|
display: inline-flex;
|
|
15
21
|
align-items: flex-start;
|
|
16
22
|
position: relative;
|
|
23
|
+
flex: 0 0 auto;
|
|
17
24
|
|
|
18
25
|
&:first-child {
|
|
19
26
|
margin-right: calc( var( --spacing-base ) * 2 );
|
|
@@ -66,4 +73,4 @@
|
|
|
66
73
|
|
|
67
74
|
.symbol {
|
|
68
75
|
font-weight: 400;
|
|
69
|
-
}
|
|
76
|
+
}
|
package/index.ts
CHANGED
|
@@ -54,6 +54,7 @@ export {
|
|
|
54
54
|
PricingTableItem,
|
|
55
55
|
} from './components/pricing-table';
|
|
56
56
|
export { default as BoostScoreBar } from './components/boost-score-bar';
|
|
57
|
+
export { default as BoostScoreGraph } from './components/boost-score-graph';
|
|
57
58
|
export { default as ProductPrice } from './components/product-price';
|
|
58
59
|
export { default as ProductOffer, IconsCard } from './components/product-offer';
|
|
59
60
|
export { default as Dialog } from './components/dialog';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@automattic/jetpack-components",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.2",
|
|
4
4
|
"description": "Jetpack Components Package",
|
|
5
5
|
"author": "Automattic",
|
|
6
6
|
"license": "GPL-2.0-or-later",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@automattic/format-currency": "1.0.1",
|
|
18
|
+
"@automattic/jetpack-boost-score-api": "^0.1.7",
|
|
18
19
|
"@babel/runtime": "^7",
|
|
19
20
|
"@wordpress/browserslist-config": "5.21.0",
|
|
20
21
|
"@wordpress/components": "25.4.0",
|
|
@@ -27,10 +28,12 @@
|
|
|
27
28
|
"classnames": "2.3.2",
|
|
28
29
|
"prop-types": "^15.7.2",
|
|
29
30
|
"qrcode.react": "3.1.0",
|
|
30
|
-
"react-slider": "2.0.5"
|
|
31
|
+
"react-slider": "2.0.5",
|
|
32
|
+
"uplot": "1.6.24",
|
|
33
|
+
"uplot-react": "1.1.4"
|
|
31
34
|
},
|
|
32
35
|
"devDependencies": {
|
|
33
|
-
"@automattic/jetpack-base-styles": "^0.6.
|
|
36
|
+
"@automattic/jetpack-base-styles": "^0.6.6",
|
|
34
37
|
"@babel/core": "7.22.9",
|
|
35
38
|
"@babel/preset-react": "7.22.5",
|
|
36
39
|
"@jest/globals": "29.4.3",
|