diff --git a/package-lock.json b/package-lock.json index 9e82ac232..1818f79a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "assert": "^2.1.0", "buffer": "^6.0.3", "chart.js": "^4.3.0", + "chartjs-plugin-zoom": "^2.2.0", "crypto-browserify": "^3.12.0", "dayjs": "^1.11.7", "eslint-plugin-jest-formatting": "^3.1.0", @@ -4583,6 +4584,12 @@ "@types/node": "*" } }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT" + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -6417,6 +6424,19 @@ "pnpm": ">=7" } }, + "node_modules/chartjs-plugin-zoom": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz", + "integrity": "sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==", + "license": "MIT", + "dependencies": { + "@types/hammerjs": "^2.0.45", + "hammerjs": "^2.0.8" + }, + "peerDependencies": { + "chart.js": ">=3.2.0" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -9157,6 +9177,15 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "node_modules/hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -20933,6 +20962,11 @@ "@types/node": "*" } }, + "@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==" + }, "@types/hoist-non-react-statics": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", @@ -22372,6 +22406,15 @@ "@kurkle/color": "^0.3.0" } }, + "chartjs-plugin-zoom": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-zoom/-/chartjs-plugin-zoom-2.2.0.tgz", + "integrity": "sha512-in6kcdiTlP6npIVLMd4zXZ08PDUXC52gZ4FAy5oyjk1zX3gKarXMAof7B9eFiisf9WOC3bh2saHg+J5WtLXZeA==", + "requires": { + "@types/hammerjs": "^2.0.45", + "hammerjs": "^2.0.8" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -24413,6 +24456,11 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, + "hammerjs": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", + "integrity": "sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==" + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", diff --git a/package.json b/package.json index a8b00d6e1..46c373692 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "assert": "^2.1.0", "buffer": "^6.0.3", "chart.js": "^4.3.0", + "chartjs-plugin-zoom": "^2.2.0", "crypto-browserify": "^3.12.0", "dayjs": "^1.11.7", "eslint-plugin-jest-formatting": "^3.1.0", diff --git a/src/components/CompareResults/CommonGraph.tsx b/src/components/CompareResults/CommonGraph.tsx index 71d8c28f5..abe0fd0ed 100644 --- a/src/components/CompareResults/CommonGraph.tsx +++ b/src/components/CompareResults/CommonGraph.tsx @@ -1,59 +1,112 @@ -import { Chart as ChartJS, LineElement, LinearScale } from 'chart.js'; +import { + Chart as ChartJS, + LineElement, + LinearScale, + ScriptableContext, +} from 'chart.js'; import 'chart.js/auto'; +import ZoomPlugin from 'chartjs-plugin-zoom'; import * as kde from 'fast-kde'; import { Line } from 'react-chartjs-2'; -import { style } from 'typestyle'; -import { Spacing } from '../../styles'; -import { MeasurementUnit } from '../../types/types'; - -ChartJS.register(LinearScale, LineElement); - -const styles = { - container: style({ - display: 'flex', - marginBottom: Spacing.Medium, - width: '100%', - }), -}; +ChartJS.register(LinearScale, LineElement, ZoomPlugin); function CommonGraph({ - baseRevisionRuns, - newRevisionRuns, + baseValues, + newValues, min, max, + unit, }: CommonGraphProps) { - const scaleUnit = - baseRevisionRuns.measurementUnit || newRevisionRuns.measurementUnit; + ///////////////// START SHOW VALUES //////////////////////// + const baseValuesData = baseValues.map((v) => { + return { x: v, y: 'Base' }; + }); + const newValuesData = newValues.map((v) => { + return { x: v, y: 'New' }; + }); + + const allValuesData = [...baseValuesData, ...newValuesData]; + + //////////////////// START FAST KDE //////////////////////// + // Arbitrary value that seems to work OK. + // In the future we'll want to compute a better value, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1901248 for some ideas. + const bandwidth = (max - min) / 15; + const baseRunsDensity = Array.from( + kde.density1d(baseValues, { + bandwidth, + extent: [min, max], + }), + ); + const newRunsDensity = Array.from( + kde.density1d(newValues, { + bandwidth, + extent: [min, max], + }), + ); + //////////////////// END FAST KDE //////////////////////// const options = { + responsive: true, + maintainAspectRatio: false, plugins: { legend: { - align: 'start' as const, - position: 'bottom' as const, + display: false, }, title: { align: 'start' as const, display: true, text: 'Runs Density Distribution', }, + zoom: { + zoom: { + mode: 'x', + drag: { + enabled: true, + borderWidth: 1, + backgroundColor: 'rgba(225,225,225,0.5)', + }, + wheel: { + enabled: true, + speed: 0.2, + }, + pinch: { + enabled: true, + }, + }, + pan: { + enabled: true, + mode: 'x', + modifierKey: 'ctrl', + }, + limits: { + x: { min: 'original', max: 'original', minRange: bandwidth }, + }, + }, }, - responsive: true, scales: { x: { + type: 'linear' as const, + suggestedMin: min, + suggestedMax: max, grid: { display: false, offset: false, }, - type: 'linear' as const, title: { align: 'end' as const, display: true, - text: scaleUnit || '', + text: `${unit} →`, }, }, - y: { + yKde: { + type: 'linear', // Linear scale + stack: 'y', // yKde and yValues are part of the same stack + stackWeight: 3, // Higher stack weight means it takes more space + weight: 2, // Higher weight means it's on top in the stack beginAtZero: true, + grace: '3%', // Make sure there's some more space at the top grid: { drawBorder: false, display: false, @@ -63,10 +116,16 @@ function CommonGraph({ beginAtZero: true, display: true, }, - title: { - align: 'end' as const, - display: true, - text: 'Run Density' as const, + }, + yValues: { + type: 'category', + stack: 'y', + stackWeight: 1, // Lower stack weight means it takes less space + weight: 1, // Lower weight means it's lower in the stack + labels: ['Base', 'New'], + offset: true, // This gives some space around the graph + ticks: { + autoSkip: false, }, }, }, @@ -88,74 +147,46 @@ function CommonGraph({ }, }; - //////////////////// START FAST KDE //////////////////////// - // Arbitrary value that seems to work OK. - // In the future we'll want to compute a better value, see - // https://bugzilla.mozilla.org/show_bug.cgi?id=1901248 for some ideas. - if (max === min) { - min = max - 15; - max = max + 15; - } - const bandwidth = (max - min) / 15; - const baseRunsDensity = Array.from( - kde.density1d(baseRevisionRuns.values, { - bandwidth, - extent: [min - bandwidth, max + bandwidth], - }), - ); - const newRunsDensity = Array.from( - kde.density1d(newRevisionRuns.values, { - bandwidth, - extent: [min - bandwidth, max + bandwidth], - }), - ); - - //////////////////// END FAST KDE //////////////////////// - const data = { datasets: [ { + yAxisID: 'yKde', label: 'Base', data: baseRunsDensity, fill: false, borderColor: 'rgba(144, 89, 255, 1)', }, { + yAxisID: 'yKde', label: 'New', data: newRunsDensity, fill: false, borderColor: 'rgba(0, 135, 135, 1)', }, + { + yAxisID: 'yValues', + type: 'bubble', + pointStyle: 'triangle', + pointRadius: 7, + data: allValuesData, + backgroundColor: (context: ScriptableContext<'bubble'>) => + (context.raw as { y: 'Base' | 'New' }).y === 'Base' + ? 'rgba(144, 89, 255, 0.6)' + : 'rgba(0, 135, 135, 0.6)', + }, ], }; - return ( -
- {/* @ts-expect-error the types for chart.js do not seem great and do not support all options. */} - -
- ); + /* @ts-expect-error the types for chart.js do not seem great and do not support all options. */ + return ; } interface CommonGraphProps { - baseRevisionRuns: { - name: string; - median: number; - values: number[]; - stddev: number; - stddevPercent: number; - measurementUnit: MeasurementUnit; - }; - newRevisionRuns: { - name: string; - median: number; - values: number[]; - stddev: number; - stddevPercent: number; - measurementUnit: MeasurementUnit; - }; + baseValues: number[]; + newValues: number[]; min: number; max: number; + unit: string | null; } export default CommonGraph; diff --git a/src/components/CompareResults/Distribution.tsx b/src/components/CompareResults/Distribution.tsx index b57eb9bf4..e2fa6a1af 100644 --- a/src/components/CompareResults/Distribution.tsx +++ b/src/components/CompareResults/Distribution.tsx @@ -1,17 +1,9 @@ -import { style } from 'typestyle'; +import Grid from '@mui/material/Grid'; import CommonGraph from './CommonGraph'; import RunValues from './RunValues'; -import { Spacing } from '../../styles'; import type { CompareResultsItem } from '../../types/state'; -const styles = { - container: style({ - display: 'flex', - marginBottom: Spacing.Medium, - }), -}; - function computeMinMax( baseRuns: number[], newRuns: number[], @@ -26,6 +18,10 @@ function computeMinMax( min = Math.min(min, value); max = Math.max(max, value); } + + // Add some grace value of 5% + min = min * 0.95; + max = max * 1.05; return [min, max]; } @@ -50,54 +46,64 @@ function Distribution(props: DistributionProps) { new_measurement_unit: newUnit, } = result; + const baseValues = + baseRunsReplicates && baseRunsReplicates.length + ? baseRunsReplicates + : baseRuns; + const baseRevisionRuns = { name: 'Base', avg: baseAvg, median: baseMedian, - values: - baseRunsReplicates && baseRunsReplicates.length - ? baseRunsReplicates - : baseRuns, + values: baseValues, application: baseApplication, stddev: baseStddev, stddevPercent: baseStddevPercent, measurementUnit: baseUnit, }; + const newValues = + newRunsReplicates && newRunsReplicates.length ? newRunsReplicates : newRuns; + const newRevisionRuns = { name: 'New', avg: newAvg, median: newMedian, - values: - newRunsReplicates && newRunsReplicates.length - ? newRunsReplicates - : newRuns, + values: newValues, application: newApplication, stddev: newStddev, stddevPercent: newStddevPercent, measurementUnit: newUnit, }; - const [minValue, maxValue] = computeMinMax( - baseRevisionRuns.values, - newRevisionRuns.values, - ); - + const [minValue, maxValue] = computeMinMax(baseValues, newValues); + const scaleUnit = baseUnit || newUnit; return ( -
- - - -
+ + + + + + + + + + + ); } diff --git a/src/components/CompareResults/GraphDistribution.tsx b/src/components/CompareResults/GraphDistribution.tsx index cffcb7a17..7fd7cd9f1 100644 --- a/src/components/CompareResults/GraphDistribution.tsx +++ b/src/components/CompareResults/GraphDistribution.tsx @@ -4,12 +4,12 @@ import { PointElement, LineElement, Tooltip, - Legend, TooltipItem, + ScriptableContext, } from 'chart.js'; import { Bubble } from 'react-chartjs-2'; -ChartJS.register(LinearScale, PointElement, LineElement, Tooltip, Legend); +ChartJS.register(LinearScale, PointElement, LineElement, Tooltip); interface GraphContextRaw { x: number; @@ -18,43 +18,50 @@ interface GraphContextRaw { } function GraphDistribution(props: GraphDistributionProps) { - const { name, values, min, max } = props; + const { baseValues, newValues, min, max, unit } = props; - const graphData = values.map((v) => { - return { x: v, y: 0, r: 10 }; + const baseData = baseValues.map((v) => { + return { x: v, y: 'Base', r: 10 }; }); + const newData = newValues.map((v) => { + return { x: v, y: 'New', r: 10 }; + }); + + const graphData = [...baseData, ...newData]; const options = { + maintainAspectRatio: false, plugins: { legend: { - align: 'start' as const, - position: 'top' as const, + display: false, }, tooltip: { callbacks: { label: (context: TooltipItem<'bubble'>) => { - return `${(context.raw as GraphContextRaw).x} ms`; + return `${(context.raw as GraphContextRaw).x}${unit ? ' ' + unit : ''}`; }, }, }, }, scales: { x: { - grid: { - display: false, - }, + type: 'linear', suggestedMin: min, suggestedMax: max, - }, - y: { - ticks: { - display: false, - beginAtZero: true, - }, grid: { - drawBorder: false, display: false, }, + title: { + align: 'end' as const, + display: true, + text: unit, + }, + }, + y: { + type: 'category', + labels: ['Base', 'New'], + offset: true, + display: false, }, }, elements: { @@ -67,24 +74,25 @@ function GraphDistribution(props: GraphDistributionProps) { const data = { datasets: [ { - label: name, data: graphData, - backgroundColor: - name.toLowerCase() === 'base' + backgroundColor: (context: ScriptableContext<'bubble'>) => + (context.raw as { y: 'Base' | 'New' }).y === 'Base' ? 'rgba(144, 89, 255, 0.6)' : 'rgba(0, 135, 135, 0.6)', }, ], }; + /* @ts-expect-error the types for chart.js do not seem great and do not support all options. */ return ; } interface GraphDistributionProps { - name: string; - values: number[]; + baseValues: number[]; + newValues: number[]; min: number; max: number; + unit: string | null; } export default GraphDistribution; diff --git a/src/components/CompareResults/RunValues.tsx b/src/components/CompareResults/RunValues.tsx index facf0aa63..1dde2579d 100644 --- a/src/components/CompareResults/RunValues.tsx +++ b/src/components/CompareResults/RunValues.tsx @@ -3,25 +3,18 @@ import { useState } from 'react'; import { Button } from '@mui/material'; import { style } from 'typestyle'; -import GraphDistribution from './GraphDistribution'; import { Spacing } from '../../styles'; import { MeasurementUnit } from '../../types/types'; import { formatNumber } from './../../utils/format'; const styles = { - container: style({ - width: '300px', - marginRight: Spacing.xLarge, - }), values: style({ display: 'flex', flexWrap: 'wrap', alignItems: 'center', marginBottom: Spacing.Small, width: '300px', - }), - value: style({ - marginRight: Spacing.xSmall, + gap: Spacing.xSmall, }), deviation: style({ textTransform: 'uppercase', @@ -51,7 +44,7 @@ function RunValues(props: RunValuesProps) { }; return ( -
+ <> {application ? (
{name}: {formatNumber(avg)} {measurementUnit} ({application}) @@ -61,26 +54,15 @@ function RunValues(props: RunValuesProps) { {name}: {formatNumber(avg)} {measurementUnit}
)} -
- -
+
{firstValues.map((value, index) => ( -
- {formatNumber(value)} -
+
{formatNumber(value)}
))} {expanded ? lastValues.map((value, index) => ( -
- {value} -
+
{value}
)) : null} {lastValues.length ? ( @@ -101,7 +83,7 @@ function RunValues(props: RunValuesProps) { {stddev} {unit} = {stddevPercent}% standard deviation
-
+ ); } interface RunValuesProps {