From daa3603d634ff9bc5d4e22933dc33ae9849091a6 Mon Sep 17 00:00:00 2001 From: Thilo Aschebrock Date: Wed, 31 May 2023 08:46:46 +1000 Subject: [PATCH 1/5] Replace PieChart with BarChart Signed-off-by: Thilo Aschebrock --- frontend/src/Components/BarChart/BarChart.tsx | 102 ++++++++++++++++++ .../BarChart/getChartVoteData.test.ts | 32 ++++++ .../Components/BarChart/getChartVoteData.ts | 42 ++++++++ frontend/src/Components/PieChart/PieChart.tsx | 65 ----------- .../PieChart/getPieChartVoteData.test.ts | 26 ----- .../PieChart/getPieChartVoteData.ts | 47 -------- .../Components/ResultsPage/ResultsPage.tsx | 6 +- 7 files changed, 178 insertions(+), 142 deletions(-) create mode 100644 frontend/src/Components/BarChart/BarChart.tsx create mode 100644 frontend/src/Components/BarChart/getChartVoteData.test.ts create mode 100644 frontend/src/Components/BarChart/getChartVoteData.ts delete mode 100644 frontend/src/Components/PieChart/PieChart.tsx delete mode 100644 frontend/src/Components/PieChart/getPieChartVoteData.test.ts delete mode 100644 frontend/src/Components/PieChart/getPieChartVoteData.ts diff --git a/frontend/src/Components/BarChart/BarChart.tsx b/frontend/src/Components/BarChart/BarChart.tsx new file mode 100644 index 00000000..be3b7de3 --- /dev/null +++ b/frontend/src/Components/BarChart/BarChart.tsx @@ -0,0 +1,102 @@ +import { + Chart, + ChartOptions, + Legend, + BarController, + Tooltip, + CategoryScale, + LinearScale, + BarElement, +} 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 { getChartVoteData } from './getChartVoteData'; + +Chart.register(BarController, CategoryScale, LinearScale, BarElement, Legend, Tooltip); + +export const BarChart = connectToWebSocket(({ socket }) => { + const canvasRef = useRef(null); + const chartRef = useRef | null>(null); + const { isDark } = useContext(ColorMode); + const { labels, datasets } = useDeepCompareMemoize(getChartVoteData(socket.state)); + + 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: 'bar', + 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<'bar'> => ({ + layout: { + padding: 32, + }, + scales: { + x: { + grid: { color: getComputedStyle(document.body).getPropertyValue('--text-tertiary') }, + ticks: { + color: getComputedStyle(document.body).getPropertyValue('--text-primary'), + }, + }, + y: { + min: 0, + ticks: { + stepSize: 1, + color: getComputedStyle(document.body).getPropertyValue('--text-primary'), + }, + grid: { color: getComputedStyle(document.body).getPropertyValue('--text-tertiary') }, + title: { + display: true, + text: 'Votes', + color: getComputedStyle(document.body).getPropertyValue('--text-primary'), + }, + }, + }, + plugins: { + legend: { + display: false, + labels: { color: getComputedStyle(document.body).getPropertyValue('--text-primary') }, + }, + }, +}); diff --git a/frontend/src/Components/BarChart/getChartVoteData.test.ts b/frontend/src/Components/BarChart/getChartVoteData.test.ts new file mode 100644 index 00000000..0fa5426b --- /dev/null +++ b/frontend/src/Components/BarChart/getChartVoteData.test.ts @@ -0,0 +1,32 @@ +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 { datasets, labels } = getChartVoteData({ + votes: { + user1: '2', + user2: '2', + user3: '1', + user4: '2', + user5: '1', + user6: '0.5', + }, + scale: ['0', '0.5', '1', '2', '3'], + }); + expect(datasets[0].data).toEqual([0, 1, 2, 3, 0]); + expect(labels).toEqual(['0', '0.5', '1', '2', '3']); + }); + + it.each([VOTE_COFFEE, VOTE_OBSERVER, VOTE_NOTE_VOTED])('ignores %j votes', (vote) => { + const { datasets, labels } = getChartVoteData({ + votes: { + user1: '1', + user2: vote, + }, + scale: ['0', '0.5', '1', '2', '3'], + }); + expect(datasets[0].data).toEqual([0, 0, 1, 0, 0]); + expect(labels).toEqual(['0', '0.5', '1', '2', '3']); + }); +}); diff --git a/frontend/src/Components/BarChart/getChartVoteData.ts b/frontend/src/Components/BarChart/getChartVoteData.ts new file mode 100644 index 00000000..cd79525d --- /dev/null +++ b/frontend/src/Components/BarChart/getChartVoteData.ts @@ -0,0 +1,42 @@ +import { ChartDataset } from 'chart.js'; +import { CardValue, VOTE_COFFEE, VOTE_NOTE_VOTED, VOTE_OBSERVER } from '../../../../shared/cards'; +import { WebSocketState } from '../../../../shared/serverMessages'; +import { compareCardValues } from '../../helpers/compareVotes'; + +export const getChartVoteData = ({ + votes, + scale, +}: Pick): { + labels: CardValue[]; + datasets: ChartDataset<'bar', number[]>[]; +} => { + const labels = scale.filter( + (value) => ![VOTE_OBSERVER, VOTE_NOTE_VOTED, VOTE_COFFEE].includes(value), + ); + const votesByValue: Partial> = Object.fromEntries( + labels.map((value) => [value, 0]), + ); + + for (const value of Object.values(votes)) { + if (labels.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<'bar', number[]>[] = [ + { + data: accumulatedVotes.map(([, numberOfVotes]) => numberOfVotes), + backgroundColor: [getComputedStyle(document.body).getPropertyValue('--primary')], + borderColor: getComputedStyle(document.body).getPropertyValue('--background'), + }, + ]; + + return { + labels, + datasets, + }; +}; 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.tsx b/frontend/src/Components/ResultsPage/ResultsPage.tsx index 12ec77a6..54ea39a5 100644 --- a/frontend/src/Components/ResultsPage/ResultsPage.tsx +++ b/frontend/src/Components/ResultsPage/ResultsPage.tsx @@ -10,7 +10,7 @@ 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 { BarChart } from '../BarChart/BarChart'; import { ResetButton } from '../ResetButton/ResetButton'; import { connectToWebSocket } from '../WebSocket/WebSocket'; import { compareVotes } from '../../helpers/compareVotes'; @@ -40,9 +40,7 @@ const getClassName = (vote: CardValue) => export const ResultsPage = connectToWebSocket(({ socket }) => (

{HEADING_RESULTS}

-
- -
+
From 9bc36ee1fe63d4c70061d8a57cccc0be0163dad2 Mon Sep 17 00:00:00 2001 From: Thilo Aschebrock Date: Fri, 15 Sep 2023 16:24:48 +1000 Subject: [PATCH 2/5] Improve bar chart range Signed-off-by: Thilo Aschebrock --- .../BarChart/getChartVoteData.test.ts | 23 ++++++++++--- .../Components/BarChart/getChartVoteData.ts | 32 ++++++++++++------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/frontend/src/Components/BarChart/getChartVoteData.test.ts b/frontend/src/Components/BarChart/getChartVoteData.test.ts index 0fa5426b..99f9d5a8 100644 --- a/frontend/src/Components/BarChart/getChartVoteData.test.ts +++ b/frontend/src/Components/BarChart/getChartVoteData.test.ts @@ -11,11 +11,12 @@ describe('The getChartVoteData function', () => { user4: '2', user5: '1', user6: '0.5', + user7: '4', }, - scale: ['0', '0.5', '1', '2', '3'], + scale: ['0', '0.5', '1', '2', '3', '4', '5'], }); - expect(datasets[0].data).toEqual([0, 1, 2, 3, 0]); - expect(labels).toEqual(['0', '0.5', '1', '2', '3']); + expect(datasets[0].data).toEqual([1, 2, 3, 0, 1]); + expect(labels).toEqual(['0.5', '1', '2', '3', '4']); }); it.each([VOTE_COFFEE, VOTE_OBSERVER, VOTE_NOTE_VOTED])('ignores %j votes', (vote) => { @@ -23,10 +24,22 @@ describe('The getChartVoteData function', () => { votes: { user1: '1', user2: vote, + user3: '?', }, scale: ['0', '0.5', '1', '2', '3'], }); - expect(datasets[0].data).toEqual([0, 0, 1, 0, 0]); - expect(labels).toEqual(['0', '0.5', '1', '2', '3']); + expect(datasets[0].data).toEqual([1, 1]); + expect(labels).toEqual(['1', '?']); + }); + + it('returns full scale when no valid vote was given', () => { + const { datasets, labels } = getChartVoteData({ + votes: { + user1: VOTE_COFFEE, + }, + scale: ['0', '0.5', '1', '2', '3', '4', '5'], + }); + expect(datasets[0].data).toEqual([]); + expect(labels).toEqual(['0', '0.5', '1', '2', '3', '4', '5']); }); }); diff --git a/frontend/src/Components/BarChart/getChartVoteData.ts b/frontend/src/Components/BarChart/getChartVoteData.ts index cd79525d..842c9d7a 100644 --- a/frontend/src/Components/BarChart/getChartVoteData.ts +++ b/frontend/src/Components/BarChart/getChartVoteData.ts @@ -1,7 +1,6 @@ import { ChartDataset } from 'chart.js'; import { CardValue, VOTE_COFFEE, VOTE_NOTE_VOTED, VOTE_OBSERVER } from '../../../../shared/cards'; import { WebSocketState } from '../../../../shared/serverMessages'; -import { compareCardValues } from '../../helpers/compareVotes'; export const getChartVoteData = ({ votes, @@ -11,32 +10,41 @@ export const getChartVoteData = ({ datasets: ChartDataset<'bar', number[]>[]; } => { const labels = scale.filter( - (value) => ![VOTE_OBSERVER, VOTE_NOTE_VOTED, VOTE_COFFEE].includes(value), - ); - const votesByValue: Partial> = Object.fromEntries( - labels.map((value) => [value, 0]), + (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[value] = (votesByValue[value] ?? 0) + 1; + votesByValue.set(value, (votesByValue.get(value) ?? 0) + 1); } - } - - const accumulatedVotes = Object.entries(votesByValue) as [CardValue, number][]; - accumulatedVotes.sort(([value1], [value2]) => compareCardValues(value1, value2)); + 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 datasets: ChartDataset<'bar', number[]>[] = [ { - data: accumulatedVotes.map(([, numberOfVotes]) => numberOfVotes), + data: slicedAccumulatedVotes.map(([, numberOfVotes]) => numberOfVotes), backgroundColor: [getComputedStyle(document.body).getPropertyValue('--primary')], borderColor: getComputedStyle(document.body).getPropertyValue('--background'), }, ]; return { - labels, + labels: slicedLabels, datasets, }; }; From 89224825e56224aa232eeb47746e99f8fa4cb130 Mon Sep 17 00:00:00 2001 From: Thilo Aschebrock Date: Fri, 15 Sep 2023 16:26:55 +1000 Subject: [PATCH 3/5] Upgrade node version in test to 18 Signed-off-by: Thilo Aschebrock --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From aff20bbc340c3d4cb3af4ae56257deffcb95c475 Mon Sep 17 00:00:00 2001 From: Thilo Aschebrock Date: Mon, 18 Sep 2023 11:23:17 +1000 Subject: [PATCH 4/5] Switch to from chartjs to chartist Signed-off-by: Thilo Aschebrock --- frontend/index.html | 1 + frontend/src/Components/BarChart/BarChart.css | 18 +++ .../Components/BarChart/BarChart.module.css | 5 + frontend/src/Components/BarChart/BarChart.tsx | 112 +++--------------- .../BarChart/getChartVoteData.test.ts | 21 ++-- .../Components/BarChart/getChartVoteData.ts | 21 ++-- package-lock.json | 96 +++++++++++---- package.json | 3 +- 8 files changed, 135 insertions(+), 142 deletions(-) create mode 100644 frontend/src/Components/BarChart/BarChart.css create mode 100644 frontend/src/Components/BarChart/BarChart.module.css 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 index be3b7de3..45ec0f52 100644 --- a/frontend/src/Components/BarChart/BarChart.tsx +++ b/frontend/src/Components/BarChart/BarChart.tsx @@ -1,102 +1,26 @@ -import { - Chart, - ChartOptions, - Legend, - BarController, - Tooltip, - CategoryScale, - LinearScale, - BarElement, -} from 'chart.js'; -import { useContext, useEffect, useRef } from 'preact/hooks'; -import { useDeepCompareMemoize } from '../../helpers/helpers'; -import { ColorMode } from '../ColorModeProvider/ColorModeProvider'; +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'; -Chart.register(BarController, CategoryScale, LinearScale, BarElement, Legend, Tooltip); - export const BarChart = connectToWebSocket(({ socket }) => { - const canvasRef = useRef(null); - const chartRef = useRef | null>(null); - const { isDark } = useContext(ColorMode); - const { labels, datasets } = useDeepCompareMemoize(getChartVoteData(socket.state)); - - 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: 'bar', - data: { - labels, - datasets, - }, - options: getChartOptions(), - }); - - return () => { - if (chartRef.current) { - chartRef.current.destroy(); - chartRef.current = null; - } - }; - }, []); - - if (!datasets.length) { - return null; - } + const { data, high } = useDeepCompareMemoize(getChartVoteData(socket.state)); return ( -
- - Your browser does not support the canvas element. - -
+ <> + + + ); }); - -const getChartOptions = (): ChartOptions<'bar'> => ({ - layout: { - padding: 32, - }, - scales: { - x: { - grid: { color: getComputedStyle(document.body).getPropertyValue('--text-tertiary') }, - ticks: { - color: getComputedStyle(document.body).getPropertyValue('--text-primary'), - }, - }, - y: { - min: 0, - ticks: { - stepSize: 1, - color: getComputedStyle(document.body).getPropertyValue('--text-primary'), - }, - grid: { color: getComputedStyle(document.body).getPropertyValue('--text-tertiary') }, - title: { - display: true, - text: 'Votes', - color: getComputedStyle(document.body).getPropertyValue('--text-primary'), - }, - }, - }, - plugins: { - legend: { - display: false, - labels: { color: getComputedStyle(document.body).getPropertyValue('--text-primary') }, - }, - }, -}); diff --git a/frontend/src/Components/BarChart/getChartVoteData.test.ts b/frontend/src/Components/BarChart/getChartVoteData.test.ts index 99f9d5a8..45cba4da 100644 --- a/frontend/src/Components/BarChart/getChartVoteData.test.ts +++ b/frontend/src/Components/BarChart/getChartVoteData.test.ts @@ -3,7 +3,7 @@ import { getChartVoteData } from './getChartVoteData'; describe('The getChartVoteData function', () => { it('accumulates votes correctly', () => { - const { datasets, labels } = getChartVoteData({ + const { data, high } = getChartVoteData({ votes: { user1: '2', user2: '2', @@ -15,12 +15,13 @@ describe('The getChartVoteData function', () => { }, scale: ['0', '0.5', '1', '2', '3', '4', '5'], }); - expect(datasets[0].data).toEqual([1, 2, 3, 0, 1]); - expect(labels).toEqual(['0.5', '1', '2', '3', '4']); + 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 { datasets, labels } = getChartVoteData({ + const { data, high } = getChartVoteData({ votes: { user1: '1', user2: vote, @@ -28,18 +29,20 @@ describe('The getChartVoteData function', () => { }, scale: ['0', '0.5', '1', '2', '3'], }); - expect(datasets[0].data).toEqual([1, 1]); - expect(labels).toEqual(['1', '?']); + 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 { datasets, labels } = getChartVoteData({ + const { data, high } = getChartVoteData({ votes: { user1: VOTE_COFFEE, }, scale: ['0', '0.5', '1', '2', '3', '4', '5'], }); - expect(datasets[0].data).toEqual([]); - expect(labels).toEqual(['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 index 842c9d7a..7fc04215 100644 --- a/frontend/src/Components/BarChart/getChartVoteData.ts +++ b/frontend/src/Components/BarChart/getChartVoteData.ts @@ -1,4 +1,3 @@ -import { ChartDataset } from 'chart.js'; import { CardValue, VOTE_COFFEE, VOTE_NOTE_VOTED, VOTE_OBSERVER } from '../../../../shared/cards'; import { WebSocketState } from '../../../../shared/serverMessages'; @@ -6,8 +5,8 @@ export const getChartVoteData = ({ votes, scale, }: Pick): { - labels: CardValue[]; - datasets: ChartDataset<'bar', number[]>[]; + data: { labels: CardValue[]; series: [number[]] }; + high: number; } => { const labels = scale.filter( (value) => ![VOTE_OBSERVER, VOTE_NOTE_VOTED, VOTE_COFFEE, '?'].includes(value), @@ -35,16 +34,14 @@ export const getChartVoteData = ({ const slicedLabels = slicedAccumulatedVotes.length ? [...labels.slice(firstVoteIndex, lastVoteIndex + 1), ...otherVotesByValue.keys()] : [...labels, ...otherVotesByValue.keys()]; - const datasets: ChartDataset<'bar', number[]>[] = [ - { - data: slicedAccumulatedVotes.map(([, numberOfVotes]) => numberOfVotes), - backgroundColor: [getComputedStyle(document.body).getPropertyValue('--primary')], - borderColor: getComputedStyle(document.body).getPropertyValue('--background'), - }, - ]; + + const data = slicedAccumulatedVotes.map(([, numberOfVotes]) => numberOfVotes); return { - labels: slicedLabels, - datasets, + data: { + labels: slicedLabels, + series: [data], + }, + high: Math.max(1, ...data), }; }; 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 0409cbac..ad85da5b 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,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", @@ -56,6 +56,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", From b5dccfc46e74fdc7ce291702e15e609d6cfddc1f Mon Sep 17 00:00:00 2001 From: Thilo Aschebrock Date: Mon, 18 Sep 2023 11:42:22 +1000 Subject: [PATCH 5/5] Lazy load bar chart Signed-off-by: Thilo Aschebrock --- .../ResultsPage/ResultsPage.module.css | 4 ++ .../Components/ResultsPage/ResultsPage.tsx | 68 ++++++++++--------- 2 files changed, 41 insertions(+), 31 deletions(-) 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 54ea39a5..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 { BarChart } from '../BarChart/BarChart'; 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,33 +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)} + + ))} + + +
+
- - -)); + ); +});