diff --git a/src/web/questions/components/HistoryChart.tsx b/src/web/questions/components/HistoryChart.tsx index a874f79..673a3c0 100644 --- a/src/web/questions/components/HistoryChart.tsx +++ b/src/web/questions/components/HistoryChart.tsx @@ -1,10 +1,10 @@ import { addDays, differenceInDays, format, startOfDay, startOfToday, startOfTomorrow } from "date-fns"; -import React from "react"; +import React, { useState } from "react"; import { - VictoryAxis, VictoryChart, VictoryGroup, VictoryLabel, VictoryLegend, VictoryLine, - VictoryScatter, VictoryTheme, VictoryTooltip, VictoryVoronoiContainer + VictoryAxis, VictoryChart, VictoryGroup, VictoryLabel, VictoryLine, VictoryScatter, + VictoryTheme, VictoryTooltip, VictoryVoronoiContainer } from "victory"; import { QuestionWithHistoryFragment } from "../../fragments.generated"; @@ -13,37 +13,25 @@ interface Props { question: QuestionWithHistoryFragment; } -const formatOptionName = (name: string) => { - return name.length > 20 ? name.slice(0, 17) + "..." : name; -}; - -const getLength = (str: string): number => { - // TODO - measure with temporary DOM element instead? - const capitalLetterLengthMultiplier = 1.25; - const smallLetterMultiplier = 0.8; - const numUpper = (str.match(/[A-Z]/g) || []).length; - const numSmallLetters = (str.match(/[fijlrt]/g) || []).length; - const numSpaces = (str.match(/[\s]/g) || []).length; - const length = - str.length + - -numUpper - - numSmallLetters + - numUpper * capitalLetterLengthMultiplier + - (numSmallLetters + numSpaces) * smallLetterMultiplier; - return length; -}; - type DataSet = { x: Date; y: number; name: string }[]; const colors = ["dodgerblue", "crimson", "seagreen", "darkviolet", "turquoise"]; // can't be replaced with React component, VictoryChart requires VictoryGroup elements to be immediate children -const getVictoryGroup = ({ data, i }: { data: DataSet; i: number }) => { +const getVictoryGroup = ({ + data, + i, + highlight, +}: { + data: DataSet; + i: number; + highlight?: boolean; +}) => { return ( (active ? 3.75 : 3)} + size={({ active }) => (active || highlight ? 3.75 : 3)} /> @@ -51,7 +39,33 @@ const getVictoryGroup = ({ data, i }: { data: DataSet; i: number }) => { ); }; +const Legend: React.FC<{ + items: { name: string; color: string }[]; + setHighlight: (i: number | undefined) => void; +}> = ({ items, setHighlight }) => { + return ( +
setHighlight(undefined)}> + {items.map((item, i) => ( +
setHighlight(i)} + > + + + + + {item.name} + +
+ ))} +
+ ); +}; + export const HistoryChart: React.FC = ({ question }) => { + const [highlight, setHighlight] = useState(undefined); + let dataSetsNames = question.options .sort((a, b) => (a.probability > b.probability ? -1 : 1)) .map((o) => o.name); @@ -66,7 +80,6 @@ export const HistoryChart: React.FC = ({ question }) => { let dataSets: DataSet[] = []; let maxProbability = 0; - let longestNameLength = 0; const sortedHistory = question.history.sort((a, b) => a.timestamp < b.timestamp ? -1 : 1 @@ -75,7 +88,7 @@ export const HistoryChart: React.FC = ({ question }) => { for (const name of dataSetsNames) { let newDataset: DataSet = []; let previousDate = -Infinity; - for (let item of sortedHistory) { + for (const item of sortedHistory) { const relevantItemsArray = item.options.filter((x) => x.name === name); const date = new Date(item.timestamp * 1000); if ( @@ -92,9 +105,6 @@ export const HistoryChart: React.FC = ({ question }) => { relevantItem.probability > maxProbability ? relevantItem.probability : maxProbability; - let length = getLength(formatOptionName(relevantItem.name)); - longestNameLength = - length > longestNameLength ? length : longestNameLength; newDataset.push(result); previousDate = item.timestamp; } @@ -102,12 +112,8 @@ export const HistoryChart: React.FC = ({ question }) => { dataSets.push(newDataset); } - const letterLength = 7; - const labelLegendStart = 45; - const domainMax = maxProbability < 0.5 ? Math.round(10 * (maxProbability + 0.05)) / 10 : 1; - const dataSetsLength = dataSets.length; const goldenRatio = (1 + Math.sqrt(5)) / 2; const width = 750; const height = width / goldenRatio; @@ -115,14 +121,9 @@ export const HistoryChart: React.FC = ({ question }) => { top: 20, bottom: 60, left: 60, - right: labelLegendStart + letterLength * longestNameLength, + right: 20, }; - const legendData = Array.from(Array(dataSetsLength).keys()).map((i) => ({ - name: formatOptionName(dataSetsNames[i]), - symbol: { fill: colors[i] }, - })); - const minDate = sortedHistory.length ? startOfDay(new Date(sortedHistory[0].timestamp * 1000)) : startOfToday(); @@ -136,111 +137,108 @@ export const HistoryChart: React.FC = ({ question }) => { : startOfTomorrow(); return ( - "Not shown"} - labelComponent={ - - } - text={({ datum }) => - `${datum.name}: ${Math.round(datum.y * 100)}%\n${format( - datum.x, - "yyyy-MM-dd" - )}` - } - style={{ - fontSize: 16, // needs to be set here and not just in labelComponent for text size calculations - fontFamily: - '"Gill Sans", "Gill Sans MT", "SerĀ­avek", "Trebuchet MS", sans-serif', - // default font family from Victory, need to be specified explicitly for some reason, otherwise text size gets miscalculated - }} - flyoutStyle={{ - stroke: "#999", - fill: "white", - }} - cornerRadius={4} - flyoutPadding={{ top: 4, bottom: 4, left: 12, right: 12 }} - /> - } - radius={50} - voronoiBlacklist={ - [...Array(5).keys()].map((i) => `line-${i}`) - // see: https://github.com/FormidableLabs/victory/issues/545 - } - /> - } - scale={{ - x: "time", - y: "linear", - }} - domain={{ - x: [minDate, maxDate], - y: [0, domainMax], - }} - > - - - {dataSets - .slice(0, 5) - .map((dataset, i) => getVictoryGroup({ data: dataset, i }))} - + "Not shown"} + labelComponent={ + + } + text={({ datum }) => + `${datum.name}: ${Math.round(datum.y * 100)}%\n${format( + datum.x, + "yyyy-MM-dd" + )}` + } + style={{ + fontSize: 18, // needs to be set here and not just in labelComponent for text size calculations + fontFamily: + '"Gill Sans", "Gill Sans MT", "SerĀ­avek", "Trebuchet MS", sans-serif', + // default font family from Victory, need to be specified explicitly for some reason, otherwise text size gets miscalculated + }} + flyoutStyle={{ + stroke: "#999", + fill: "white", + }} + cornerRadius={4} + flyoutPadding={{ top: 4, bottom: 4, left: 12, right: 12 }} + /> + } + radius={50} + voronoiBlacklist={ + [...Array(5).keys()].map((i) => `line-${i}`) + // see: https://github.com/FormidableLabs/victory/issues/545 + } /> } - scale={{ x: "time" }} - tickFormat={(t) => format(t, "yyyy-MM-dd")} - /> - - } - // tickFormat specifies how ticks should be displayed - tickFormat={(x) => `${x * 100}%`} + domain={{ + x: [minDate, maxDate], + y: [0, domainMax], + }} + > + {dataSets.map((dataset, i) => + getVictoryGroup({ data: dataset, i, highlight: i === highlight }) + )} + + } + scale={{ x: "time" }} + tickFormat={(t) => format(t, "yyyy-MM-dd")} + /> + + } + // tickFormat specifies how ticks should be displayed + tickFormat={(x) => `${x * 100}%`} + /> + + ({ name, color: colors[i] }))} + setHighlight={setHighlight} /> - + ); }; diff --git a/src/web/utils.ts b/src/web/utils.ts index 9196286..f4c5c81 100644 --- a/src/web/utils.ts +++ b/src/web/utils.ts @@ -12,7 +12,8 @@ export const getBasePath = () => { }; export const cleanText = (text: string): string => { - // Note: should no longer be necessary + // Note: should no longer be necessary? + // Still needed for e.g. /questions/rootclaim-what-caused-the-disappearance-of-malaysia-airlines-flight-370 let textString = !!text ? text : ""; textString = textString .replaceAll("] (", "](") diff --git a/tailwind.config.js b/tailwind.config.js index 114a989..9ac34f5 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -6,6 +6,9 @@ module.exports = { backgroundImage: { quri: "url('/icons/logo.svg')", }, + maxWidth: { + 160: "160px", + }, }, }, variants: {