diff --git a/package-lock.json b/package-lock.json index 61a23c948..3f5e8103b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "assert": "^2.1.0", "buffer": "^6.0.3", "chart.js": "^4.5.1", + "chartjs-plugin-zoom": "^2.2.0", "crypto-browserify": "^3.12.1", "dayjs": "^1.11.20", "express": "^5.2.1", @@ -4479,6 +4480,12 @@ "integrity": "sha512-nDKoaKJYbnn1MZxUY0cA1bPmmgZbg0cTq7Rh13d0KWYNOiKbqoR+2d89SnRPszGh7ROzSwZ/GOjZ4jPbmmZ6Eg==", "dev": true }, + "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/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -6763,6 +6770,19 @@ "pnpm": ">=8" } }, + "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.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -9385,6 +9405,15 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "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", @@ -20110,6 +20139,11 @@ "integrity": "sha512-nDKoaKJYbnn1MZxUY0cA1bPmmgZbg0cTq7Rh13d0KWYNOiKbqoR+2d89SnRPszGh7ROzSwZ/GOjZ4jPbmmZ6Eg==", "dev": true }, + "@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==" + }, "@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -21716,6 +21750,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.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -23554,6 +23597,11 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "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 a31c0e837..7ce58922a 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "assert": "^2.1.0", "buffer": "^6.0.3", "chart.js": "^4.5.1", + "chartjs-plugin-zoom": "^2.2.0", "crypto-browserify": "^3.12.1", "dayjs": "^1.11.20", "express": "^5.2.1", diff --git a/src/components/CompareResults/CommonGraph.tsx b/src/components/CompareResults/CommonGraph.tsx index a43215c8c..f0ff1e51c 100644 --- a/src/components/CompareResults/CommonGraph.tsx +++ b/src/components/CompareResults/CommonGraph.tsx @@ -9,12 +9,13 @@ import { type TooltipModel, } 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 { Colors } from '../../styles/Colors'; -ChartJS.register(LinearScale, LineElement); +ChartJS.register(LinearScale, LineElement, ZoomPlugin); // This computes the min, max and the KDE bandwidth from a list of numbers. function computeStatisticsForRuns(data: number[]) { @@ -88,9 +89,29 @@ function CommonGraph({ baseValues, newValues, unit }: CommonGraphProps) { const min = computeMin(statsForBase?.min, statsForNew?.min) * 0.95; const max = computeMax(statsForBase?.max, statsForNew?.max) * 1.05; + //////////////////// START FAST KDE //////////////////////// + // So that the 2 KDE graphs are visually comparable, it's important to use the + // same bandwidth for both. + const bandwidth = computeMin(statsForBase?.bandwidth, statsForNew?.bandwidth); + + 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 //////////////////////// + // The KDE line chart and categorical bubble chart share an x-axis but use // entirely different y-scales, making the composition flexible but // non-trivial. + const options = { // Make the chart responsive to container size responsive: true, @@ -186,6 +207,31 @@ function CommonGraph({ baseValues, newValues, unit }: CommonGraphProps) { padding: 10, boxPadding: 4, }, + 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 }, + }, + }, }, scales: { x: { @@ -276,25 +322,6 @@ function CommonGraph({ baseValues, newValues, unit }: CommonGraphProps) { const allValuesData = [...baseValuesData, ...newValuesData]; - //////////////////// START FAST KDE //////////////////////// - // So that the 2 KDE graphs are visually comparable, it's important to use the - // same bandwidth for both. - const bandwidth = computeMin(statsForBase?.bandwidth, statsForNew?.bandwidth); - - 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 data = { datasets: [ {