metaforecast/src/web/questions/components/HistoryChart/utils.ts
2022-05-09 23:27:51 +04:00

118 lines
3.0 KiB
TypeScript

import { addDays, startOfDay, startOfToday, startOfTomorrow } from "date-fns";
import { QuestionWithHistoryFragment } from "../../../fragments.generated";
import { isQuestionBinary } from "../../../utils";
import { isFullQuestionOption } from "../../utils";
export type ChartSeries = { x: Date; y: number; name: string }[];
export const MAX_LINES = 5;
// number of colors should match MAX_LINES
// colors are taken from tailwind, https://tailwindcss.com/docs/customizing-colors
export const chartColors = [
"#0284C7", // sky-600
"#DC2626", // red-600
"#15803D", // green-700
"#7E22CE", // purple-700
"#F59E0B", // amber-500
];
const goldenRatio = (1 + Math.sqrt(5)) / 2;
// used both for chart and for ssr placeholder
export const width = 750;
export const height = width / goldenRatio;
export type ChartData = {
seriesList: ChartSeries[];
seriesNames: string[];
maxProbability: number;
minDate: Date;
maxDate: Date;
};
export const buildChartData = (
question: QuestionWithHistoryFragment
): ChartData => {
let seriesNames = question.options
.filter(isFullQuestionOption)
.sort((a, b) => {
if (a.probability > b.probability) {
return -1;
} else if (a.probability < b.probability) {
return 1;
}
return a.name < b.name ? -1 : 1; // needed for stable sorting - otherwise it's possible to get order mismatch in SSR vs client-side
})
.map((o) => o.name)
.slice(0, MAX_LINES);
const isBinary = isQuestionBinary(question);
if (isBinary) {
seriesNames = ["Yes"];
}
const nameToIndex = Object.fromEntries(
seriesNames.map((name, i) => [name, i])
);
let seriesList: ChartSeries[] = [...Array(seriesNames.length)].map((x) => []);
const sortedHistory = question.history.sort((a, b) =>
a.timestamp < b.timestamp ? -1 : 1
);
{
let previousDate = -Infinity;
for (const item of sortedHistory) {
if (item.timestamp - previousDate < 12 * 60 * 60) {
continue;
}
const date = new Date(item.timestamp * 1000);
for (const option of item.options) {
if (option.name == null || option.probability == null) {
continue;
}
const idx = nameToIndex[option.name];
if (idx === undefined) {
continue;
}
const result = {
x: date,
y: option.probability,
name: option.name,
};
seriesList[idx].push(result);
}
previousDate = item.timestamp;
}
}
let maxProbability = 0;
for (const dataSet of seriesList) {
for (const item of dataSet) {
maxProbability = Math.max(maxProbability, item.y);
}
}
const minDate = sortedHistory.length
? startOfDay(new Date(sortedHistory[0].timestamp * 1000))
: startOfToday();
const maxDate = sortedHistory.length
? addDays(
startOfDay(
new Date(sortedHistory[sortedHistory.length - 1].timestamp * 1000)
),
1
)
: startOfTomorrow();
return {
seriesList,
seriesNames,
maxProbability,
minDate,
maxDate,
};
};