tweak: Charts

Add line, reorganize display

Also fix nasty bug where probabilities are inverted in frontpage
This commit is contained in:
NunoSempere 2022-04-28 15:22:22 -04:00
parent 55b320c528
commit 340d99b485
4 changed files with 174 additions and 116 deletions

6
.eslintrc Normal file
View File

@ -0,0 +1,6 @@
{
"extends": ["next", "prettier"],
"rules": {
"next/no-document-import-in-page": "off"
}
}

View File

@ -1,7 +1,15 @@
import React from "react"; import React from "react";
import { import {
VictoryAxis, VictoryChart, VictoryGroup, VictoryLabel, VictoryLegend, VictoryScatter, VictoryAxis,
VictoryTheme, VictoryTooltip, VictoryVoronoiContainer VictoryChart,
VictoryGroup,
VictoryLabel,
VictoryLegend,
VictoryScatter,
VictoryLine,
VictoryTheme,
VictoryTooltip,
VictoryVoronoiContainer,
} from "victory"; } from "victory";
import { QuestionWithHistoryFragment } from "../../fragments.generated"; import { QuestionWithHistoryFragment } from "../../fragments.generated";
@ -48,26 +56,25 @@ const getVictoryGroup = (data, i) => {
return ( return (
<VictoryGroup color={colors[i] || "darkgray"} data={dataAsXy(data)}> <VictoryGroup color={colors[i] || "darkgray"} data={dataAsXy(data)}>
<VictoryScatter <VictoryScatter
name={`scatter-${i}`}
//style={{ labels: { display: "none" } }} //style={{ labels: { display: "none" } }}
size={({ active }) => (active ? 3.75 : 3)} size={({ active }) => (active ? 3.75 : 3)}
//labels={() => null} //labels={() => null}
//labelComponent={<span></span>} //labelComponent={<span></span>}
/> />
{/* Doesn't work well with tooltips <VictoryLine
<VictoryLine name={`line-${i}`}
name={`line${i}`} //style={{ labels: { display: "none" } }}
//style={{ labels: { display: "none" } }} //labels={() => null}
//labels={() => null} //labelComponent={<span></span>}
//labelComponent={<span></span>} />
/>
*/}
</VictoryGroup> </VictoryGroup>
); );
}; };
export const HistoryChart: React.FC<Props> = ({ question }) => { export const HistoryChart: React.FC<Props> = ({ question }) => {
let height = 400; let height = 300;
let width = 500; let width = 500;
let padding = { top: 20, bottom: 50, left: 50, right: 100 }; let padding = { top: 20, bottom: 50, left: 50, right: 100 };
// let dataSetsNames = ["Yes", "No", "Maybe", "Perhaps", "Possibly"]; // let dataSetsNames = ["Yes", "No", "Maybe", "Perhaps", "Possibly"];
@ -78,6 +85,7 @@ export const HistoryChart: React.FC<Props> = ({ question }) => {
}); });
dataSetsNames = [...new Set(dataSetsNames)].slice(0, 5); // take the first 5 dataSetsNames = [...new Set(dataSetsNames)].slice(0, 5); // take the first 5
let dataSets = []; let dataSets = [];
/*
dataSetsNames.forEach((name) => { dataSetsNames.forEach((name) => {
let newDataset = []; let newDataset = [];
question.history.forEach((item) => { question.history.forEach((item) => {
@ -96,112 +104,145 @@ export const HistoryChart: React.FC<Props> = ({ question }) => {
}); });
dataSets.push(newDataset); dataSets.push(newDataset);
}); });
*/
dataSetsNames.forEach((name) => {
let newDataset = [];
let previousDate = -Infinity;
for (let item of question.history) {
let relevantItemsArray = item.options.filter((x) => x.name == name);
let date = new Date(item.timestamp * 1000);
if (
relevantItemsArray.length == 1 &&
item.timestamp - previousDate > 12 * 60 * 60
) {
let relevantItem = relevantItemsArray[0];
// if (relevantItem.type == "PROBABILITY") {
let result = {
date,
probability: relevantItem.probability,
};
newDataset.push(result);
// }
previousDate = item.timestamp;
}
}
dataSets.push(newDataset);
});
let dataSetsLength = dataSets.length; let dataSetsLength = dataSets.length;
return ( return (
<div className="grid grid-rows-1 bg-white p-10"> <div className="flex justify-center items-center w-full">
<a <div className="w-9/12">
className="textinherit no-underline" <a
href={question.url} className="textinherit no-underline"
target="_blank" href={question.url}
> target="_blank"
>
{/*
<h1 className="text-3xl font-normal text-center mt-5"> <h1 className="text-3xl font-normal text-center mt-5">
{question.title} {question.title}
</h1> </h1>
</a> */}
<VictoryChart </a>
domainPadding={20} <VictoryChart
padding={padding} domainPadding={20}
theme={VictoryTheme.material} padding={padding}
height={height} theme={VictoryTheme.material}
width={width} height={height}
containerComponent={ width={width}
<VictoryVoronoiContainer containerComponent={
labels={({ datum }) => `${datum.x}: ${Math.round(datum.y * 100)}%`} <VictoryVoronoiContainer
labelComponent={ labels={({ datum }) =>
<VictoryTooltip `${datum.x}: ${Math.round(datum.y * 100)}%`
pointerLength={0} }
dy={-12} labelComponent={
style={{ <VictoryTooltip
fontSize: 10, pointerLength={0}
fill: "black", dy={-12}
strokeWidth: 0.05, style={{
}} fontSize: 10,
flyoutStyle={{ fill: "black",
stroke: "black", strokeWidth: 0.05,
fill: "white", }}
}} flyoutStyle={{
flyoutWidth={80} stroke: "black",
cornerRadius={0} fill: "white",
flyoutPadding={7} }}
/> flyoutWidth={80}
} cornerRadius={0}
voronoiBlacklist={ flyoutPadding={7}
["line0", "line1", "line2", "line3", "line4"] />
}
voronoiBlacklist={
["line-0", "line-1", "line-2", "line-3", "line-4"]
//Array.from(Array(5).keys()).map((x, i) => `line${i}`) //Array.from(Array(5).keys()).map((x, i) => `line${i}`)
// see: https://github.com/FormidableLabs/victory/issues/545 // see: https://github.com/FormidableLabs/victory/issues/545
} }
/> />
} }
domain={{ domain={{
y: [0, 1], y: [0, 1],
}} }}
> >
<VictoryLegend <VictoryLegend
x={width - 100} x={width - 100}
y={height / 2 - 18 - (dataSetsLength - 1) * 13} y={height / 2 - 18 - (dataSetsLength - 1) * 13}
orientation="vertical" orientation="vertical"
gutter={20} gutter={20}
style={{ border: { stroke: "black" }, title: { fontSize: 20 } }} style={{ border: { stroke: "black" }, title: { fontSize: 20 } }}
data={ data={
Array.from(Array(dataSetsLength).keys()).map((i) => ({ Array.from(Array(dataSetsLength).keys()).map((i) => ({
name: dataSetsNames[i], name: dataSetsNames[i],
symbol: { fill: colors[i] }, symbol: { fill: colors[i] },
})) }))
/*[ /*[
{ name: "One", symbol: { fill: "tomato", type: "star" } }, { name: "One", symbol: { fill: "tomato", type: "star" } },
{ name: "Two", symbol: { fill: "orange" } }, { name: "Two", symbol: { fill: "orange" } },
{ name: "Three", symbol: { fill: "gold" } }, { name: "Three", symbol: { fill: "gold" } },
]*/ ]*/
} }
/> />
{dataSets.slice(0, 5).map((dataset, i) => getVictoryGroup(dataset, i))} {dataSets
<VictoryAxis .slice(0, 5)
// tickValues specifies both the number of ticks and where .map((dataset, i) => getVictoryGroup(dataset, i))}
// they are placed on the axis <VictoryAxis
// tickValues={dataAsXy.map((datum) => datum.x)} // tickValues specifies both the number of ticks and where
// tickFormat={dataAsXy.map((datum) => datum.x)} // they are placed on the axis
tickCount={7} // tickValues={dataAsXy.map((datum) => datum.x)}
style={{ // tickFormat={dataAsXy.map((datum) => datum.x)}
grid: { stroke: null, strokeWidth: 0.5 }, tickCount={7}
}} style={{
//axisLabelComponent={ grid: { stroke: null, strokeWidth: 0.5 },
// <VictoryLabel dy={40} style={{ fontSize: 10, fill: "gray" }} /> }}
//} //axisLabelComponent={
// label="Date (dd/mm/yy)" // <VictoryLabel dy={40} style={{ fontSize: 10, fill: "gray" }} />
tickLabelComponent={ //}
<VictoryLabel // label="Date (dd/mm/yy)"
dy={0} tickLabelComponent={
angle={-30} <VictoryLabel
style={{ fontSize: 10, fill: "gray" }} dy={0}
/> angle={-30}
} style={{ fontSize: 10, fill: "gray" }}
/> />
<VictoryAxis }
dependentAxis />
// tickFormat specifies how ticks should be displayed <VictoryAxis
tickFormat={(x) => `${x * 100}%`} dependentAxis
style={{ // tickFormat specifies how ticks should be displayed
grid: { stroke: "#D3D3D3", strokeWidth: 0.5 }, tickFormat={(x) => `${x * 100}%`}
}} style={{
tickLabelComponent={ grid: { stroke: "#D3D3D3", strokeWidth: 0.5 },
<VictoryLabel dy={0} style={{ fontSize: 10, fill: "gray" }} /> }}
} tickLabelComponent={
/> <VictoryLabel dy={0} style={{ fontSize: 10, fill: "gray" }} />
</VictoryChart> }
/>
</VictoryChart>
</div>
</div> </div>
); );
}; };

View File

@ -112,7 +112,8 @@ export const QuestionOptions: React.FC<{ options: Option[] }> = ({
const isBinary = const isBinary =
options.length === 2 && options.length === 2 &&
(options[0].name === "Yes" || options[0].name === "No"); (options[0].name === "Yes" || options[0].name === "No");
const getYesOption = (options) =>
options.find((option) => option.name == "Yes");
const optionsSorted = options.sort((a, b) => b.probability - a.probability); const optionsSorted = options.sort((a, b) => b.probability - a.probability);
const optionsMax5 = !!optionsSorted.slice ? optionsSorted.slice(0, 5) : []; // display max 5 options. const optionsMax5 = !!optionsSorted.slice ? optionsSorted.slice(0, 5) : []; // display max 5 options.
@ -121,17 +122,17 @@ export const QuestionOptions: React.FC<{ options: Option[] }> = ({
<div className="space-x-2"> <div className="space-x-2">
<span <span
className={`${primaryForecastColor( className={`${primaryForecastColor(
options[0].probability getYesOption(options).probability
)} text-white w-16 rounded-md px-1.5 py-0.5 font-bold`} )} text-white w-16 rounded-md px-1.5 py-0.5 font-bold`}
> >
{formatProbability(options[0].probability)} {formatProbability(getYesOption(options).probability)}
</span> </span>
<span <span
className={`${textColor( className={`${textColor(
options[0].probability getYesOption(options).probability
)} text-gray-500 inline-block`} )} text-gray-500 inline-block`}
> >
{primaryEstimateAsText(options[0].probability)} {primaryEstimateAsText(getYesOption(options).probability)}
</span> </span>
</div> </div>
); );

View File

@ -40,8 +40,8 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
const QuestionCardContents: React.FC<{ const QuestionCardContents: React.FC<{
question: QuestionWithHistoryFragment; question: QuestionWithHistoryFragment;
}> = ({ question }) => ( }> = ({ question }) => (
<div className="space-y-4"> <div className="grid grid-cols-1 space-y-4 place-items-center">
<h1> <h1 className="text-4xl place-self-center w-full text-center mt-10">
<a <a
className="text-black no-underline" className="text-black no-underline"
href={question.url} href={question.url}
@ -50,21 +50,31 @@ const QuestionCardContents: React.FC<{
{question.title} {question.title}
</a> </a>
</h1> </h1>
<QuestionFooter question={question} expandFooterToFullWidth={true} /> <HistoryChart question={question} />
{/*
<div className="flex justify-center items-center w-full">
<div className="w-6/12">
<QuestionFooter question={question} expandFooterToFullWidth={true} />
</div>
</div>
<QuestionOptions options={question.options} /> <QuestionOptions options={question.options} />
<ReactMarkdown linkTarget="_blank" className="font-normal"> */}
<h1 className="text-xl place-self-center w-full text-center mt-20">
{"Question description"}
</h1>
<ReactMarkdown linkTarget="_blank" className="font-normal w-9/12">
{question.description} {question.description}
</ReactMarkdown> </ReactMarkdown>
<HistoryChart question={question} />
</div> </div>
); );
const QuestionPage: NextPage<Props> = ({ id }) => { const QuestionPage: NextPage<Props> = ({ id }) => {
return ( return (
<Layout page="question"> <Layout page="question">
<div className="max-w-2xl mx-auto"> <div className="max-w-4xl mx-auto">
<Card highlightOnHover={false}> <Card highlightOnHover={false}>
<Query document={QuestionPageDocument} variables={{ id }}> <Query document={QuestionPageDocument} variables={{ id }}>
{({ data }) => <QuestionCardContents question={data.result} />} {({ data }) => <QuestionCardContents question={data.result} />}