118 lines
3.0 KiB
TypeScript
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,
|
|
};
|
|
};
|