feat: highlight lines, refactorings

This commit is contained in:
Vyacheslav Matyukhin 2022-05-07 01:17:37 +04:00
parent 290cb48960
commit cf8d79b8e4
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C

View File

@ -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 (
<VictoryGroup color={colors[i] || "darkgray"} data={data} key={i}>
<VictoryLine
name={`line-${i}`}
style={{
data: {
strokeOpacity: highlight ? 1 : 0.5,
},
}}
/>
<VictoryScatter
name={`scatter-${i}`}
size={({ active }) => (active || highlight ? 3.75 : 3)}
/>
<VictoryLine name={`line-${i}`} />
</VictoryGroup>
);
};
@ -63,13 +79,11 @@ const Legend: React.FC<{
);
};
export const HistoryChart: React.FC<Props> = ({ question }) => {
const [highlight, setHighlight] = useState<number | undefined>(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<Props> = ({ 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);
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<Props> = ({ question }) => {
)
: startOfTomorrow();
const result = {
dataSets,
dataSetsNames,
maxProbability,
minDate,
maxDate,
};
return result;
};
export const HistoryChart: React.FC<Props> = ({ question }) => {
const [highlight, setHighlight] = useState<number | undefined>(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 (
<div className="flex items-center flex-col sm:flex-row">
<VictoryChart
@ -190,7 +228,7 @@ export const HistoryChart: React.FC<Props> = ({ 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<Props> = ({ question }) => {
y: [0, domainMax],
}}
>
{dataSets.map((dataset, i) =>
getVictoryGroup({ data: dataset, i, highlight: i === highlight })
)}
<VictoryAxis
tickCount={Math.min(7, differenceInDays(maxDate, minDate) + 1)}
style={{
@ -234,6 +269,13 @@ export const HistoryChart: React.FC<Props> = ({ 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
}
</VictoryChart>
<Legend
items={dataSetsNames.map((name, i) => ({ name, color: colors[i] }))}