diff --git a/package-lock.json b/package-lock.json index e3daeb0d..8653490d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6363,15 +6363,6 @@ "@babel/types": "^7.3.0" } }, - "@types/chart.js": { - "version": "2.9.31", - "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.31.tgz", - "integrity": "sha512-hzS6phN/kx3jClk3iYqEHNnYIRSi4RZrIGJ8CDLjgatpHoftCezvC44uqB3o3OUm9ftU1m7sHG8+RLyPTlACrA==", - "dev": true, - "requires": { - "moment": "^2.10.2" - } - }, "@types/cheerio": { "version": "0.22.22", "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.22.tgz", @@ -10578,30 +10569,9 @@ "dev": true }, "chart.js": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz", - "integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==", - "requires": { - "chartjs-color": "^2.1.0", - "moment": "^2.10.2" - } - }, - "chartjs-color": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", - "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", - "requires": { - "chartjs-color-string": "^0.6.0", - "color-convert": "^1.9.3" - } - }, - "chartjs-color-string": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", - "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", - "requires": { - "color-name": "^1.0.0" - } + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.1.tgz", + "integrity": "sha512-m5kzt72I1WQ9LILwQC4syla/LD/N413RYv2Dx2nnTkRS9iv/ey1xLTt0DnPc/eWV4zI+BgEgDYBIzbQhZHc/PQ==" }, "check-types": { "version": "11.1.2", @@ -10957,6 +10927,7 @@ "version": "1.9.3", "resolved": "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha1-u3GFBpDh8TZWfeYp0tVHHe2kweg=", + "dev": true, "requires": { "color-name": "1.1.3" }, @@ -10964,14 +10935,16 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true } } }, "color-name": { "version": "1.1.4", "resolved": "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=" + "integrity": "sha1-wqCah6y95pVD3m9j+jmVyCbFNqI=", + "dev": true }, "color-string": { "version": "1.5.4", @@ -19000,11 +18973,6 @@ } } }, - "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" - }, "moo": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", @@ -24570,18 +24538,17 @@ } }, "react-chartjs-2": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.11.1.tgz", - "integrity": "sha512-G7cNq/n2Bkh/v4vcI+GKx7Q1xwZexKYhOSj2HmrFXlvNeaURWXun6KlOUpEQwi1cv9Tgs4H3kGywDWMrX2kxfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-3.0.4.tgz", + "integrity": "sha512-pcbFNpkPMTkGXXJ7k7hnukbRD0ZV01qB6JQY1ontITc2IYvhGlK6BBDy28VeydYs1Dl/c5ZpRgRVEtT5GUnxcQ==", "requires": { - "lodash": "^4.17.19", - "prop-types": "^15.7.2" + "lodash": "^4.17.19" }, "dependencies": { "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" } } }, diff --git a/package.json b/package.json index d2376a65..900cc2c5 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "bootstrap": "^4.6.0", "bottlejs": "^2.0.0", "bowser": "^2.11.0", - "chart.js": "^2.9.4", + "chart.js": "^3.5.1", "classnames": "^2.2.6", "compare-versions": "^3.6.0", "csvjson": "^5.1.0", @@ -41,7 +41,7 @@ "qs": "^6.9.6", "ramda": "^0.27.1", "react": "^17.0.1", - "react-chartjs-2": "^2.11.1", + "react-chartjs-2": "^3.0.4", "react-color": "^2.19.3", "react-copy-to-clipboard": "^5.0.2", "react-datepicker": "^3.6.0", @@ -72,7 +72,6 @@ "@stryker-mutator/jest-runner": "^5.0.0", "@stryker-mutator/typescript-checker": "^5.0.0", "@svgr/webpack": "^5.5.0", - "@types/chart.js": "^2.9.31", "@types/classnames": "^2.2.11", "@types/enzyme": "^3.10.8", "@types/jest": "^26.0.20", diff --git a/src/utils/helpers/charts.ts b/src/utils/helpers/charts.ts index e47d1b22..256fc6a9 100644 --- a/src/utils/helpers/charts.ts +++ b/src/utils/helpers/charts.ts @@ -1,30 +1,12 @@ -import { ChangeEvent, FC } from 'react'; -import { ChartData, ChartTooltipItem } from 'chart.js'; +import { ActiveElement, ChartEvent, ChartType, TooltipItem } from 'chart.js'; import { prettify } from './numbers'; -export const pointerOnHover = ({ target }: ChangeEvent, chartElement: FC[]) => { - target.style.cursor = chartElement[0] ? 'pointer' : 'default'; +export const pointerOnHover = ({ native }: ChartEvent, [ firstElement ]: ActiveElement[]) => { + if (!native?.target) { + return; + } + + (native.target as any).style.cursor = firstElement ? 'pointer' : 'default'; }; -export const renderNonDoughnutChartLabel = (labelToPick: 'yLabel' | 'xLabel') => ( - item: ChartTooltipItem, - { datasets }: ChartData, -) => { - const { datasetIndex } = item; - const value = item[labelToPick]; - const datasetLabel = datasetIndex !== undefined && datasets?.[datasetIndex]?.label || ''; - - return `${datasetLabel}: ${prettify(Number(value))}`; -}; - -export const renderDoughnutChartLabel = ( - { datasetIndex, index }: ChartTooltipItem, - { labels, datasets }: ChartData, -) => { - const datasetLabel = index !== undefined && labels?.[index] || ''; - const value = datasetIndex !== undefined && index !== undefined - && datasets?.[datasetIndex]?.data?.[index] - || ''; - - return `${datasetLabel}: ${prettify(Number(value))}`; // eslint-disable-line @typescript-eslint/no-base-to-string -}; +export const renderChartLabel = ({ dataset, label }: TooltipItem) => `${dataset.label}: ${prettify(label)}`; diff --git a/src/utils/helpers/numbers.ts b/src/utils/helpers/numbers.ts index 559184bf..dbaf6406 100644 --- a/src/utils/helpers/numbers.ts +++ b/src/utils/helpers/numbers.ts @@ -2,6 +2,6 @@ const TEN_ROUNDING_NUMBER = 10; const { ceil } = Math; const formatter = new Intl.NumberFormat('en-US'); -export const prettify = (number: number) => formatter.format(number); +export const prettify = (number: number | string) => formatter.format(Number(number)); export const roundTen = (number: number) => ceil(number / TEN_ROUNDING_NUMBER) * TEN_ROUNDING_NUMBER; diff --git a/src/visits/helpers/DefaultChart.tsx b/src/visits/helpers/DefaultChart.tsx index e68b1e4c..21f62ce9 100644 --- a/src/visits/helpers/DefaultChart.tsx +++ b/src/visits/helpers/DefaultChart.tsx @@ -1,12 +1,12 @@ -import { useState } from 'react'; -import { Doughnut, HorizontalBar } from 'react-chartjs-2'; +import { useRef } from 'react'; +import { Doughnut, Bar } from 'react-chartjs-2'; import { keys, values } from 'ramda'; import classNames from 'classnames'; -import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js'; +import { Chart, ChartData, ChartDataset, ChartOptions, LegendItem } from 'chart.js'; import { fillTheGaps } from '../../utils/helpers/visits'; import { Stats } from '../types'; import { prettify } from '../../utils/helpers/numbers'; -import { pointerOnHover, renderDoughnutChartLabel, renderNonDoughnutChartLabel } from '../../utils/helpers/charts'; +import { pointerOnHover, renderChartLabel } from '../../utils/helpers/charts'; import { HIGHLIGHTED_COLOR, HIGHLIGHTED_COLOR_ALPHA, @@ -66,7 +66,7 @@ const generateGraphData = ( borderColor: HIGHLIGHTED_COLOR, borderWidth: 2, }, - ].filter(Boolean) as ChartDataSets[], + ].filter(Boolean) as ChartDataset[], }); const dropLabelIfHidden = (label: string) => label.startsWith('hidden') ? '' : label; @@ -79,27 +79,34 @@ const determineHeight = (isBarChart: boolean, labels: string[]): number | undefi return isBarChart && labels.length > 20 ? labels.length * 8 : undefined; }; -const renderPieChartLegend = ({ config }: Chart) => { - const { labels = [], datasets = [] } = config.data ?? {}; - const { defaultColor } = config.options ?? {} as any; - const [{ backgroundColor: colors }] = datasets; +const renderPieChartLegend = ({ config }: Chart): LegendItem[] => { + const { labels = [] /* , datasets = [] */ } = config.data ?? {}; + // const { defaultColor } = config.options ?? {} as any; + // const [{ backgroundColor: colors }] = datasets; - return ( - - ); + return labels.map((label, datasetIndex) => ({ + datasetIndex, + text: label as string, + })); + + // TODO + // return ( + // + // ); }; const chartElementAtEvent = (onClick?: (label: string) => void) => ([ chart ]: [{ _index: number; _chart: Chart }]) => { + // TODO Check this function actually works with Chart.js 3 if (!onClick || !chart) { return; } @@ -115,7 +122,8 @@ const statsAreDefined = (stats: Stats | undefined): stats is Stats => !!stats && const DefaultChart = ( { title, isBarChart = false, stats, max, highlightedStats, highlightedLabel, onClick }: DefaultChartProps, ) => { - const Component = isBarChart ? HorizontalBar : Doughnut; + const Component = isBarChart ? Bar : Doughnut; + const chartRef = useRef(); const labels = keys(stats).map(dropLabelIfHidden); const data = values( !statsAreDefined(highlightedStats) ? stats : keys(highlightedStats).reduce((acc, highlightedKey) => { @@ -127,34 +135,38 @@ const DefaultChart = ( }, { ...stats }), ); const highlightedData = statsAreDefined(highlightedStats) ? fillTheGaps(highlightedStats, labels) : undefined; - const [ chartRef, setChartRef ] = useState(); const options: ChartOptions = { - legend: { display: false }, - legendCallback: !isBarChart && renderPieChartLegend as any, - scales: !isBarChart ? undefined : { - xAxes: [ - { - ticks: { - beginAtZero: true, - precision: 0, - callback: prettify, - max, - }, - stacked: true, + plugins: { + legend: { + display: false, + labels: isBarChart ? undefined : { + generateLabels: renderPieChartLegend, + }, + }, + tooltip: { + intersect: !isBarChart, + // Do not show tooltip on items with empty label when in a bar chart + filter: ({ label }) => !isBarChart || label !== '', + callbacks: { + label: renderChartLabel, }, - ], - yAxes: [{ stacked: true }], - }, - tooltips: { - intersect: !isBarChart, - // Do not show tooltip on items with empty label when in a bar chart - filter: ({ yLabel }) => !isBarChart || yLabel !== '', - callbacks: { - label: isBarChart ? renderNonDoughnutChartLabel('xLabel') : renderDoughnutChartLabel, }, }, - onHover: !isBarChart ? undefined : (pointerOnHover) as any, // TODO Types seem to be incorrectly defined in @types/chart.js + scales: !isBarChart ? undefined : { + x: { + beginAtZero: true, + stacked: true, + max, + ticks: { + precision: 0, + callback: prettify, + }, + }, + y: { stacked: true }, + }, + onHover: isBarChart ? pointerOnHover : undefined, + indexAxis: isBarChart ? 'y' : 'x', }; const graphData = generateGraphData(title, isBarChart, labels, data, highlightedData, highlightedLabel); const height = determineHeight(isBarChart, labels); @@ -164,17 +176,20 @@ const DefaultChart = (
setChartRef(element ?? undefined)} + ref={(element) => { + chartRef.current = element ?? undefined; + }} key={height} data={graphData} options={options} height={height} - getElementAtEvent={chartElementAtEvent(onClick)} + getElementAtEvent={chartElementAtEvent(onClick) as any} /* TODO */ />
{!isBarChart && (
- {chartRef?.chartInstance.generateLegend()} + No Legend in v3.0 unfortunately :( + {/* {chartRef?.chartInstance.generateLegend()} */}
)}
diff --git a/src/visits/helpers/LineChartCard.tsx b/src/visits/helpers/LineChartCard.tsx index abc5ffc2..622aa5e7 100644 --- a/src/visits/helpers/LineChartCard.tsx +++ b/src/visits/helpers/LineChartCard.tsx @@ -21,14 +21,14 @@ import { startOfISOWeek, endOfISOWeek, } from 'date-fns'; -import Chart, { ChartData, ChartDataSets, ChartOptions } from 'chart.js'; +import { Chart, ChartData, ChartDataset, ChartOptions } from 'chart.js'; import { NormalizedVisit, Stats } from '../types'; import { fillTheGaps } from '../../utils/helpers/visits'; import { useToggle } from '../../utils/helpers/hooks'; import { rangeOf } from '../../utils/utils'; import ToggleSwitch from '../../utils/ToggleSwitch'; import { prettify } from '../../utils/helpers/numbers'; -import { pointerOnHover, renderNonDoughnutChartLabel } from '../../utils/helpers/charts'; +import { pointerOnHover, renderChartLabel } from '../../utils/helpers/charts'; import { HIGHLIGHTED_COLOR, MAIN_COLOR } from '../../utils/theme'; import './LineChartCard.scss'; @@ -134,11 +134,11 @@ const generateLabelsAndGroupedVisits = ( return [ labels, fillTheGaps(groupedVisitsWithGaps, labels) ]; }; -const generateDataset = (data: number[], label: string, color: string): ChartDataSets => ({ +const generateDataset = (data: number[], label: string, color: string): ChartDataset => ({ label, data, fill: false, - lineTension: 0.2, + tension: 0.2, borderColor: color, backgroundColor: color, }); @@ -189,32 +189,28 @@ const LineChartCard = ( datasets: [ generateDataset(groupedVisits, 'Visits', MAIN_COLOR), highlightedVisits.length > 0 && generateDataset(groupedHighlighted, highlightedLabel, HIGHLIGHTED_COLOR), - ].filter(Boolean) as ChartDataSets[], + ].filter(Boolean) as ChartDataset[], }; const options: ChartOptions = { maintainAspectRatio: false, - legend: { display: false }, - scales: { - yAxes: [ - { - ticks: { - beginAtZero: true, - precision: 0, - callback: prettify, - }, - }, - ], - xAxes: [ - { - scaleLabel: { display: true, labelString: STEPS_MAP[step] }, - }, - ], + plugins: { + legend: { display: false }, + tooltip: { + intersect: false, + axis: 'x', + callbacks: { label: renderChartLabel }, + }, }, - tooltips: { - intersect: false, - axis: 'x', - callbacks: { - label: renderNonDoughnutChartLabel('yLabel'), + scales: { + y: { + beginAtZero: true, + ticks: { + precision: 0, + callback: prettify, + }, + }, + x: { + title: { display: true, text: STEPS_MAP[step] }, }, }, onHover: (pointerOnHover) as any, // TODO Types seem to be incorrectly defined in @types/chart.js @@ -248,7 +244,7 @@ const LineChartCard = (