diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 87444671..08566f83 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - name: Install dependencies run: npm ci - name: Run tests @@ -34,7 +34,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 - name: Install dependencies run: npm ci - name: Install Playwright Browsers diff --git a/frontend/index.html b/frontend/index.html index b489666b..ff602ea1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,6 +6,7 @@ + Next Generation Scrum Poker diff --git a/frontend/src/Components/BarChart/BarChart.css b/frontend/src/Components/BarChart/BarChart.css new file mode 100644 index 00000000..f0ba9864 --- /dev/null +++ b/frontend/src/Components/BarChart/BarChart.css @@ -0,0 +1,18 @@ +.ct-label { + font-size: 16px; + color: var(--text-primary); +} + +.ct-grid { + stroke: var(--text-primary); + opacity: 50%; +} + +.ct-horizontal { + display: none; +} + +.ct-bar { + stroke: var(--primary) !important; + stroke-width: 48px; +} diff --git a/frontend/src/Components/BarChart/BarChart.module.css b/frontend/src/Components/BarChart/BarChart.module.css new file mode 100644 index 00000000..4e2b799e --- /dev/null +++ b/frontend/src/Components/BarChart/BarChart.module.css @@ -0,0 +1,5 @@ +.chart { + height: 320px; + width: 640px; + max-width: 95vw; +} diff --git a/frontend/src/Components/BarChart/BarChart.tsx b/frontend/src/Components/BarChart/BarChart.tsx new file mode 100644 index 00000000..45ec0f52 --- /dev/null +++ b/frontend/src/Components/BarChart/BarChart.tsx @@ -0,0 +1,26 @@ +import { ChartistBarChart } from 'preact-chartist'; +import { connectToWebSocket } from '../WebSocket/WebSocket'; +import chartStyles from './BarChart.css?inline'; +import classes from './BarChart.module.css'; +import { useDeepCompareMemoize } from '../../helpers/helpers'; +import { getChartVoteData } from './getChartVoteData'; + +export const BarChart = connectToWebSocket(({ socket }) => { + const { data, high } = useDeepCompareMemoize(getChartVoteData(socket.state)); + + return ( + <> + + + + ); +}); diff --git a/frontend/src/Components/BarChart/getChartVoteData.test.ts b/frontend/src/Components/BarChart/getChartVoteData.test.ts new file mode 100644 index 00000000..45cba4da --- /dev/null +++ b/frontend/src/Components/BarChart/getChartVoteData.test.ts @@ -0,0 +1,48 @@ +import { CardValue, VOTE_COFFEE, VOTE_NOTE_VOTED, VOTE_OBSERVER } from '../../../../shared/cards'; +import { getChartVoteData } from './getChartVoteData'; + +describe('The getChartVoteData function', () => { + it('accumulates votes correctly', () => { + const { data, high } = getChartVoteData({ + votes: { + user1: '2', + user2: '2', + user3: '1', + user4: '2', + user5: '1', + user6: '0.5', + user7: '4', + }, + scale: ['0', '0.5', '1', '2', '3', '4', '5'], + }); + expect(data.series).toEqual([[1, 2, 3, 0, 1]]); + expect(data.labels).toEqual(['0.5', '1', '2', '3', '4']); + expect(high).toBe(3); + }); + + it.each([VOTE_COFFEE, VOTE_OBSERVER, VOTE_NOTE_VOTED])('ignores %j votes', (vote) => { + const { data, high } = getChartVoteData({ + votes: { + user1: '1', + user2: vote, + user3: '?', + }, + scale: ['0', '0.5', '1', '2', '3'], + }); + expect(data.series).toEqual([[1, 1]]); + expect(data.labels).toEqual(['1', '?']); + expect(high).toBe(1); + }); + + it('returns full scale when no valid vote was given', () => { + const { data, high } = getChartVoteData({ + votes: { + user1: VOTE_COFFEE, + }, + scale: ['0', '0.5', '1', '2', '3', '4', '5'], + }); + expect(data.series).toEqual([[]]); + expect(data.labels).toEqual(['0', '0.5', '1', '2', '3', '4', '5']); + expect(high).toBe(1); + }); +}); diff --git a/frontend/src/Components/BarChart/getChartVoteData.ts b/frontend/src/Components/BarChart/getChartVoteData.ts new file mode 100644 index 00000000..7fc04215 --- /dev/null +++ b/frontend/src/Components/BarChart/getChartVoteData.ts @@ -0,0 +1,47 @@ +import { CardValue, VOTE_COFFEE, VOTE_NOTE_VOTED, VOTE_OBSERVER } from '../../../../shared/cards'; +import { WebSocketState } from '../../../../shared/serverMessages'; + +export const getChartVoteData = ({ + votes, + scale, +}: Pick): { + data: { labels: CardValue[]; series: [number[]] }; + high: number; +} => { + const labels = scale.filter( + (value) => ![VOTE_OBSERVER, VOTE_NOTE_VOTED, VOTE_COFFEE, '?'].includes(value), + ); + const votesByValue = new Map(labels.map((value) => [value, 0])); + const otherVotesByValue = new Map(); + + for (const value of Object.values(votes)) { + if (labels.includes(value)) { + votesByValue.set(value, (votesByValue.get(value) ?? 0) + 1); + } + + if (['?'].includes(value)) { + otherVotesByValue.set(value, (otherVotesByValue.get(value) ?? 0) + 1); + } + } + + const accumulatedVotes = [...votesByValue.entries()]; + const firstVoteIndex = accumulatedVotes.findIndex(([, votes]) => votes !== 0); + const lastVoteIndex = accumulatedVotes.findLastIndex(([, votes]) => votes !== 0); + const slicedAccumulatedVotes = [ + ...accumulatedVotes.slice(firstVoteIndex, lastVoteIndex + 1), + ...otherVotesByValue.entries(), + ]; + const slicedLabels = slicedAccumulatedVotes.length + ? [...labels.slice(firstVoteIndex, lastVoteIndex + 1), ...otherVotesByValue.keys()] + : [...labels, ...otherVotesByValue.keys()]; + + const data = slicedAccumulatedVotes.map(([, numberOfVotes]) => numberOfVotes); + + return { + data: { + labels: slicedLabels, + series: [data], + }, + high: Math.max(1, ...data), + }; +}; diff --git a/frontend/src/Components/PieChart/PieChart.tsx b/frontend/src/Components/PieChart/PieChart.tsx deleted file mode 100644 index 9452cd56..00000000 --- a/frontend/src/Components/PieChart/PieChart.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { ArcElement, Chart, ChartOptions, Legend, PieController, Tooltip } from 'chart.js'; -import { useContext, useEffect, useRef } from 'preact/hooks'; -import { useDeepCompareMemoize } from '../../helpers/helpers'; -import { ColorMode } from '../ColorModeProvider/ColorModeProvider'; -import { connectToWebSocket } from '../WebSocket/WebSocket'; -import { getPieChartVoteData } from './getPieChartVoteData'; - -Chart.register(PieController, ArcElement, Legend, Tooltip); - -export const PieChart = connectToWebSocket(({ socket }) => { - const canvasRef = useRef(null); - const chartRef = useRef | null>(null); - const { isDark } = useContext(ColorMode); - const { labels, datasets } = useDeepCompareMemoize(getPieChartVoteData(socket.state.votes)); - - useEffect(() => { - if (chartRef.current) { - chartRef.current.config.data.datasets = datasets; - chartRef.current.config.data.labels = labels; - chartRef.current.config.options = getChartOptions(); - chartRef.current.update('none'); - } - }, [datasets, labels, isDark]); - - useEffect(() => { - if (!canvasRef.current) return; - - chartRef.current = new Chart(canvasRef.current, { - type: 'pie', - data: { - labels, - datasets, - }, - options: getChartOptions(), - }); - - return () => { - if (chartRef.current) { - chartRef.current.destroy(); - chartRef.current = null; - } - }; - }, []); - - if (!datasets.length) { - return null; - } - - return ( - - Your browser does not support the canvas element. - - ); -}); - -const getChartOptions = (): ChartOptions<'pie'> => ({ - layout: { - padding: 16, - }, - plugins: { - legend: { - labels: { color: getComputedStyle(document.body).getPropertyValue('--text-primary') }, - }, - }, -}); diff --git a/frontend/src/Components/PieChart/getPieChartVoteData.test.ts b/frontend/src/Components/PieChart/getPieChartVoteData.test.ts deleted file mode 100644 index fbfc4317..00000000 --- a/frontend/src/Components/PieChart/getPieChartVoteData.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CardValue, VOTE_COFFEE, VOTE_NOTE_VOTED, VOTE_OBSERVER } from '../../../../shared/cards'; -import { getPieChartVoteData } from './getPieChartVoteData'; - -describe('The getPieChartVoteData function', () => { - it('accumulates votes correctly', () => { - const { datasets, labels } = getPieChartVoteData({ - user1: '2', - user2: '2', - user3: '1', - user4: '2', - user5: '1', - user6: '0.5', - }); - expect(datasets[0].data).toEqual([1, 2, 3]); - expect(labels).toEqual(['0.5', '1', '2']); - }); - - it.each([VOTE_COFFEE, VOTE_OBSERVER, VOTE_NOTE_VOTED])('ignores %j votes', (vote) => { - const { datasets, labels } = getPieChartVoteData({ - user1: '1', - user2: vote, - }); - expect(datasets[0].data).toEqual([1]); - expect(labels).toEqual(['1']); - }); -}); diff --git a/frontend/src/Components/PieChart/getPieChartVoteData.ts b/frontend/src/Components/PieChart/getPieChartVoteData.ts deleted file mode 100644 index 59bd992d..00000000 --- a/frontend/src/Components/PieChart/getPieChartVoteData.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ChartDataset } from 'chart.js'; -import { CardValue, VOTE_COFFEE, VOTE_NOTE_VOTED, VOTE_OBSERVER } from '../../../../shared/cards'; -import { Votes } from '../../../../shared/serverMessages'; -import { compareCardValues } from '../../helpers/compareVotes'; - -export const getPieChartVoteData = ( - votes: Votes, -): { labels: CardValue[]; datasets: ChartDataset<'pie', number[]>[] } => { - const votesByValue: Partial> = {}; - for (const value of Object.values(votes)) { - if (![VOTE_OBSERVER, VOTE_NOTE_VOTED, VOTE_COFFEE].includes(value)) { - votesByValue[value] = (votesByValue[value] ?? 0) + 1; - } - } - - const accumulatedVotes = Object.entries(votesByValue) as [CardValue, number][]; - - accumulatedVotes.sort(([value1], [value2]) => compareCardValues(value1, value2)); - - const datasets: ChartDataset<'pie', number[]>[] = [ - { - data: accumulatedVotes.map(([, numberOfVotes]) => numberOfVotes), - backgroundColor: [ - // Colors taken from https://colorbrewer2.org/ - '#a6cee3', - '#1f78b4', - '#b2df8a', - '#33a02c', - '#fb9a99', - '#e31a1c', - '#fdbf6f', - '#ff7f00', - '#cab2d6', - '#6a3d9a', - '#ffff99', - '#b15928', - ], - hoverOffset: 4, - borderColor: getComputedStyle(document.body).getPropertyValue('--background'), - }, - ]; - - return { - labels: accumulatedVotes.map(([label]) => label), - datasets, - }; -}; diff --git a/frontend/src/Components/ResultsPage/ResultsPage.module.css b/frontend/src/Components/ResultsPage/ResultsPage.module.css index 2ebdad63..4b5460e9 100644 --- a/frontend/src/Components/ResultsPage/ResultsPage.module.css +++ b/frontend/src/Components/ResultsPage/ResultsPage.module.css @@ -20,3 +20,7 @@ color: var(--text-tertiary); fill: var(--text-tertiary); } + +.loading { + line-height: 320px; +} diff --git a/frontend/src/Components/ResultsPage/ResultsPage.tsx b/frontend/src/Components/ResultsPage/ResultsPage.tsx index 12ec77a6..857345d6 100644 --- a/frontend/src/Components/ResultsPage/ResultsPage.tsx +++ b/frontend/src/Components/ResultsPage/ResultsPage.tsx @@ -1,3 +1,4 @@ +import { Suspense, lazy } from 'preact/compat'; import { CardValue, VOTE_COFFEE, VOTE_NOTE_VOTED, VOTE_OBSERVER } from '../../../../shared/cards'; import { COLUMN_NAME, @@ -5,16 +6,15 @@ import { HEADING_RESULTS, TOOLTIP_PENDING_CONNECTION, } from '../../constants'; +import { compareVotes } from '../../helpers/compareVotes'; +import { UserState, getVotingState } from '../../helpers/getVotingState'; import sharedClasses from '../../styles.module.css'; import { WebSocketApi } from '../../types/WebSocket'; import { IconCoffee } from '../IconCoffee/IconCoffee'; import { IconNotVoted } from '../IconNotVoted/IconNotVoted'; import { IconObserver } from '../IconObserver/IconObserver'; -import { PieChart } from '../PieChart/PieChart'; import { ResetButton } from '../ResetButton/ResetButton'; import { connectToWebSocket } from '../WebSocket/WebSocket'; -import { compareVotes } from '../../helpers/compareVotes'; -import { getVotingState, UserState } from '../../helpers/getVotingState'; import classes from './ResultsPage.module.css'; const getSortedResultsArray = (socket: WebSocketApi): UserState[] => { @@ -37,35 +37,39 @@ const getVote = (vote: CardValue) => { const getClassName = (vote: CardValue) => vote === VOTE_NOTE_VOTED || vote === VOTE_OBSERVER ? classes.notVotedEntry : classes.votedEntry; -export const ResultsPage = connectToWebSocket(({ socket }) => ( -
-

{HEADING_RESULTS}

-
- -
-
- - - - - - - - - {getSortedResultsArray(socket).map(({ user, vote, pendingConnection }) => ( - - - +const BarChart = lazy(() => import('../BarChart/BarChart').then((value) => value.BarChart)); + +export const ResultsPage = connectToWebSocket(({ socket }) => { + return ( +
+

{HEADING_RESULTS}

+ Loading...

}> + +
+
+
{COLUMN_NAME}{COLUMN_VOTE}
- {user} - {getVote(vote)}
+ + + + - ))} - -
{COLUMN_NAME}{COLUMN_VOTE}
+ + + {getSortedResultsArray(socket).map(({ user, vote, pendingConnection }) => ( + + + {user} + + {getVote(vote)} + + ))} + + +
+
- - -)); + ); +}); diff --git a/package-lock.json b/package-lock.json index 293e411c..279f1e6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@types/ws": "^8.5.5", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", - "chart.js": "^4.4.0", + "chartist": "^1.3.0", "classnames": "^2.3.2", "dequal": "^2.0.3", "eslint": "^8.49.0", @@ -35,6 +35,7 @@ "nodemon": "^3.0.1", "npm-run-all": "^4.1.5", "preact": "^10.17.1", + "preact-chartist": "^0.15.3", "preact-render-to-string": "^6.2.1", "prettier": "^3.0.3", "rimraf": "^5.0.1", @@ -1310,11 +1311,6 @@ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, - "node_modules/@kurkle/color": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", - "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3117,15 +3113,12 @@ "node": ">=4" } }, - "node_modules/chart.js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz", - "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==", - "dependencies": { - "@kurkle/color": "^0.3.0" - }, + "node_modules/chartist": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/chartist/-/chartist-1.3.0.tgz", + "integrity": "sha512-M3ckI3ua7EHt08WLZvdi3QXn5g+in27qU6TxjI5bpriS6QwIZlWtisyUhFbpGclW546SlT3SL9oq0vFFDiAo6g==", "engines": { - "pnpm": ">=7" + "node": ">=14" } }, "node_modules/check-error": { @@ -4678,6 +4671,13 @@ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" }, + "node_modules/immutable": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "optional": true, + "peer": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -6241,6 +6241,15 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/preact-chartist": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/preact-chartist/-/preact-chartist-0.15.3.tgz", + "integrity": "sha512-LCxuSWkHnyZV9JWLhXQzrhKiDTvqd5PAykknDMSaUW6EU9RCg/v5eiooZJBjKRVBaKSQS6vgk8hLm+1+A4WNvA==", + "peerDependencies": { + "chartist": ">=1", + "preact": ">=8" + } + }, "node_modules/preact-render-to-string": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.2.1.tgz", @@ -6690,6 +6699,24 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sass": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz", + "integrity": "sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==", + "optional": true, + "peer": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -9013,11 +9040,6 @@ } } }, - "@kurkle/color": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", - "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" - }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -10299,13 +10321,10 @@ "supports-color": "^5.3.0" } }, - "chart.js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.0.tgz", - "integrity": "sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==", - "requires": { - "@kurkle/color": "^0.3.0" - } + "chartist": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/chartist/-/chartist-1.3.0.tgz", + "integrity": "sha512-M3ckI3ua7EHt08WLZvdi3QXn5g+in27qU6TxjI5bpriS6QwIZlWtisyUhFbpGclW546SlT3SL9oq0vFFDiAo6g==" }, "check-error": { "version": "1.0.2", @@ -11411,6 +11430,13 @@ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==" }, + "immutable": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", + "optional": true, + "peer": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -12480,6 +12506,12 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.17.1.tgz", "integrity": "sha512-X9BODrvQ4Ekwv9GURm9AKAGaomqXmip7NQTZgY7gcNmr7XE83adOMJvd3N42id1tMFU7ojiynRsYnY6/BRFxLA==" }, + "preact-chartist": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/preact-chartist/-/preact-chartist-0.15.3.tgz", + "integrity": "sha512-LCxuSWkHnyZV9JWLhXQzrhKiDTvqd5PAykknDMSaUW6EU9RCg/v5eiooZJBjKRVBaKSQS6vgk8hLm+1+A4WNvA==", + "requires": {} + }, "preact-render-to-string": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.2.1.tgz", @@ -12777,6 +12809,18 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sass": { + "version": "1.67.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.67.0.tgz", + "integrity": "sha512-SVrO9ZeX/QQyEGtuZYCVxoeAL5vGlYjJ9p4i4HFuekWl8y/LtJ7tJc10Z+ck1c8xOuoBm2MYzcLfTAffD0pl/A==", + "optional": true, + "peer": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, "saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", diff --git a/package.json b/package.json index 0c69a6d1..fee36bd8 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@types/ws": "^8.5.5", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", - "chart.js": "^4.4.0", + "chartist": "^1.3.0", "classnames": "^2.3.2", "dequal": "^2.0.3", "eslint": "^8.49.0", @@ -57,6 +57,7 @@ "nodemon": "^3.0.1", "npm-run-all": "^4.1.5", "preact": "^10.17.1", + "preact-chartist": "^0.15.3", "preact-render-to-string": "^6.2.1", "prettier": "^3.0.3", "rimraf": "^5.0.1",