diff --git a/src/web/questions/components/HistoryChart.tsx b/src/web/questions/components/HistoryChart.tsx index 673a3c0..852140f 100644 --- a/src/web/questions/components/HistoryChart.tsx +++ b/src/web/questions/components/HistoryChart.tsx @@ -1,7 +1,7 @@ import { addDays, differenceInDays, format, startOfDay, startOfToday, startOfTomorrow } from "date-fns"; -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { VictoryAxis, VictoryChart, VictoryGroup, VictoryLabel, VictoryLine, VictoryScatter, VictoryTheme, VictoryTooltip, VictoryVoronoiContainer @@ -15,7 +15,17 @@ interface Props { type DataSet = { x: Date; y: number; name: string }[]; -const colors = ["dodgerblue", "crimson", "seagreen", "darkviolet", "turquoise"]; +const MAX_LINES = 5; + +// number of colors should match MAX_LINES +// colors are taken from tailwind, https://tailwindcss.com/docs/customizing-colors +const colors = [ + "#0284C7", // sky-600 + "#DC2626", // red-600 + "#15803D", // green-700 + "#7E22CE", // purple-700 + "#F59E0B", // amber-500 +]; // can't be replaced with React component, VictoryChart requires VictoryGroup elements to be immediate children const getVictoryGroup = ({ @@ -29,12 +39,18 @@ const getVictoryGroup = ({ }) => { return ( + (active || highlight ? 3.75 : 3)} /> - - ); }; @@ -63,13 +79,11 @@ const Legend: React.FC<{ ); }; -export const HistoryChart: React.FC = ({ question }) => { - const [highlight, setHighlight] = useState(undefined); - +const buildDataSets = (question: QuestionWithHistoryFragment) => { let dataSetsNames = question.options .sort((a, b) => (a.probability > b.probability ? -1 : 1)) - .map((o) => o.name); - dataSetsNames = [...new Set(dataSetsNames)].slice(0, 5); // take the first 5 + .map((o) => o.name) + .slice(0, MAX_LINES); const isBinary = (dataSetsNames[0] === "Yes" && dataSetsNames[1] === "No") || @@ -78,51 +92,45 @@ export const HistoryChart: React.FC = ({ question }) => { dataSetsNames = ["Yes"]; } - let dataSets: DataSet[] = []; - let maxProbability = 0; + const nameToIndex = Object.fromEntries( + dataSetsNames.map((name, i) => [name, i]) + ); + let dataSets: DataSet[] = [...Array(dataSetsNames.length)].map((x) => []); const sortedHistory = question.history.sort((a, b) => a.timestamp < b.timestamp ? -1 : 1 ); - for (const name of dataSetsNames) { - let newDataset: DataSet = []; + { let previousDate = -Infinity; for (const item of sortedHistory) { - const relevantItemsArray = item.options.filter((x) => x.name === name); + if (item.timestamp - previousDate < 12 * 60 * 60) { + continue; + } const date = new Date(item.timestamp * 1000); - if ( - relevantItemsArray.length === 1 && - item.timestamp - previousDate > 12 * 60 * 60 - ) { - let relevantItem = relevantItemsArray[0]; + + for (const option of item.options) { + const idx = nameToIndex[option.name]; + if (idx === undefined) { + continue; + } const result = { x: date, - y: relevantItem.probability, - name: relevantItem.name, + y: option.probability, + name: option.name, }; - maxProbability = - relevantItem.probability > maxProbability - ? relevantItem.probability - : maxProbability; - newDataset.push(result); - previousDate = item.timestamp; + dataSets[idx].push(result); } + previousDate = item.timestamp; } - dataSets.push(newDataset); } - const domainMax = - maxProbability < 0.5 ? Math.round(10 * (maxProbability + 0.05)) / 10 : 1; - const goldenRatio = (1 + Math.sqrt(5)) / 2; - const width = 750; - const height = width / goldenRatio; - const padding = { - top: 20, - bottom: 60, - left: 60, - right: 20, - }; + let maxProbability = 0; + for (const dataSet of dataSets) { + for (const item of dataSet) { + maxProbability = Math.max(maxProbability, item.y); + } + } const minDate = sortedHistory.length ? startOfDay(new Date(sortedHistory[0].timestamp * 1000)) @@ -136,6 +144,36 @@ export const HistoryChart: React.FC = ({ question }) => { ) : startOfTomorrow(); + const result = { + dataSets, + dataSetsNames, + maxProbability, + minDate, + maxDate, + }; + return result; +}; + +export const HistoryChart: React.FC = ({ question }) => { + const [highlight, setHighlight] = useState(undefined); + + const { dataSets, dataSetsNames, maxProbability, minDate, maxDate } = useMemo( + () => buildDataSets(question), + [question] + ); + + const domainMax = + maxProbability < 0.5 ? Math.round(10 * (maxProbability + 0.05)) / 10 : 1; + const goldenRatio = (1 + Math.sqrt(5)) / 2; + const width = 750; + const height = width / goldenRatio; + const padding = { + top: 20, + bottom: 60, + left: 60, + right: 20, + }; + return (
= ({ question }) => { } radius={50} voronoiBlacklist={ - [...Array(5).keys()].map((i) => `line-${i}`) + [...Array(MAX_LINES).keys()].map((i) => `line-${i}`) // see: https://github.com/FormidableLabs/victory/issues/545 } /> @@ -204,9 +242,6 @@ export const HistoryChart: React.FC = ({ question }) => { y: [0, domainMax], }} > - {dataSets.map((dataset, i) => - getVictoryGroup({ data: dataset, i, highlight: i === highlight }) - )} = ({ question }) => { // tickFormat specifies how ticks should be displayed tickFormat={(x) => `${x * 100}%`} /> + { + dataSets + .map((dataSet, i) => + getVictoryGroup({ data: dataSet, i, highlight: i === highlight }) + ) + .reverse() // affects svg render order, we want to render largest datasets on top of others + } ({ name, color: colors[i] }))}