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 (
-
- );
-});
-
-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 }) => (
-
-
- {user}
- |
- {getVote(vote)} |
+const BarChart = lazy(() => import('../BarChart/BarChart').then((value) => value.BarChart));
+
+export const ResultsPage = connectToWebSocket(({ socket }) => {
+ return (
+
+
{HEADING_RESULTS}
+
Loading...}>
+
+
+
+
+
+
+ {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",