fix: further chart tweaks
This commit is contained in:
parent
340d99b485
commit
bbe0fd5982
|
@ -31,6 +31,25 @@ let getDate0 = (x) => {
|
||||||
return date.toISOString().slice(5, 10).replaceAll("-", "/");
|
return date.toISOString().slice(5, 10).replaceAll("-", "/");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let formatOptionName = (name) => {
|
||||||
|
return name.length > 10 ? name.slice(0, 8) + "..." : name;
|
||||||
|
};
|
||||||
|
|
||||||
|
let getLength = (str) => {
|
||||||
|
let capitalLetterLengthMultiplier = 1.25;
|
||||||
|
let smallLetterMultiplier = 0.8;
|
||||||
|
let numUpper = (str.match(/[A-Z]/g) || []).length;
|
||||||
|
let numSmallLetters = (str.match(/[fijlrt]/g) || []).length;
|
||||||
|
let numSpaces = (str.match(/[\s]/g) || []).length;
|
||||||
|
let length =
|
||||||
|
str.length +
|
||||||
|
-numUpper -
|
||||||
|
numSmallLetters +
|
||||||
|
numUpper * capitalLetterLengthMultiplier +
|
||||||
|
(numSmallLetters + numSpaces) * smallLetterMultiplier;
|
||||||
|
return length;
|
||||||
|
};
|
||||||
|
|
||||||
let timestampToString = (x) => {
|
let timestampToString = (x) => {
|
||||||
// for real timestamps
|
// for real timestamps
|
||||||
console.log(x);
|
console.log(x);
|
||||||
|
@ -49,6 +68,7 @@ let dataAsXy = (data) =>
|
||||||
data.map((datum) => ({
|
data.map((datum) => ({
|
||||||
x: timestampToString(datum.date), //getDate(datum.date * (1000 * 60 * 60 * 24)),
|
x: timestampToString(datum.date), //getDate(datum.date * (1000 * 60 * 60 * 24)),
|
||||||
y: datum.probability,
|
y: datum.probability,
|
||||||
|
name: datum.name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const colors = ["dodgerblue", "crimson", "seagreen", "darkviolet", "turquoise"];
|
const colors = ["dodgerblue", "crimson", "seagreen", "darkviolet", "turquoise"];
|
||||||
|
@ -74,10 +94,6 @@ const getVictoryGroup = (data, i) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HistoryChart: React.FC<Props> = ({ question }) => {
|
export const HistoryChart: React.FC<Props> = ({ question }) => {
|
||||||
let height = 300;
|
|
||||||
let width = 500;
|
|
||||||
let padding = { top: 20, bottom: 50, left: 50, right: 100 };
|
|
||||||
// let dataSetsNames = ["Yes", "No", "Maybe", "Perhaps", "Possibly"];
|
|
||||||
let dataSetsNames = [];
|
let dataSetsNames = [];
|
||||||
question.history.forEach((item) => {
|
question.history.forEach((item) => {
|
||||||
let optionNames = item.options.map((option) => option.name);
|
let optionNames = item.options.map((option) => option.name);
|
||||||
|
@ -85,28 +101,10 @@ 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 = [];
|
||||||
/*
|
let maxProbability = 0;
|
||||||
dataSetsNames.forEach((name) => {
|
let longestNameLength = 0;
|
||||||
let newDataset = [];
|
|
||||||
question.history.forEach((item) => {
|
|
||||||
let relevantItemsArray = item.options.filter((x) => x.name == name);
|
|
||||||
let date = new Date(item.timestamp * 1000);
|
|
||||||
if (relevantItemsArray.length == 1) {
|
|
||||||
let relevantItem = relevantItemsArray[0];
|
|
||||||
// if (relevantItem.type == "PROBABILITY") {
|
|
||||||
let result = {
|
|
||||||
date,
|
|
||||||
probability: relevantItem.probability,
|
|
||||||
};
|
|
||||||
newDataset.push(result);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dataSets.push(newDataset);
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
dataSetsNames.forEach((name) => {
|
for (let name of dataSetsNames) {
|
||||||
let newDataset = [];
|
let newDataset = [];
|
||||||
let previousDate = -Infinity;
|
let previousDate = -Infinity;
|
||||||
for (let item of question.history) {
|
for (let item of question.history) {
|
||||||
|
@ -121,31 +119,46 @@ export const HistoryChart: React.FC<Props> = ({ question }) => {
|
||||||
let result = {
|
let result = {
|
||||||
date,
|
date,
|
||||||
probability: relevantItem.probability,
|
probability: relevantItem.probability,
|
||||||
|
name: relevantItem.name,
|
||||||
};
|
};
|
||||||
|
maxProbability =
|
||||||
|
relevantItem.probability > maxProbability
|
||||||
|
? relevantItem.probability
|
||||||
|
: maxProbability;
|
||||||
|
let length = getLength(relevantItem.name);
|
||||||
|
longestNameLength =
|
||||||
|
length > longestNameLength ? length : longestNameLength;
|
||||||
newDataset.push(result);
|
newDataset.push(result);
|
||||||
// }
|
// }
|
||||||
previousDate = item.timestamp;
|
previousDate = item.timestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dataSets.push(newDataset);
|
dataSets.push(newDataset);
|
||||||
});
|
}
|
||||||
|
let letterLength = 7;
|
||||||
|
let labelLegendStart = 45;
|
||||||
|
|
||||||
|
let domainMax =
|
||||||
|
maxProbability < 0.5 ? Math.round(10 * (maxProbability + 0.05)) / 10 : 1;
|
||||||
let dataSetsLength = dataSets.length;
|
let dataSetsLength = dataSets.length;
|
||||||
|
let goldenRatio = (1 + Math.sqrt(5)) / 2;
|
||||||
|
let width = 750;
|
||||||
|
let height = width / goldenRatio;
|
||||||
|
let padding = {
|
||||||
|
top: 20,
|
||||||
|
bottom: 50,
|
||||||
|
left: 0,
|
||||||
|
right: labelLegendStart + letterLength * longestNameLength,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center w-full">
|
<div className="flex justify-center items-center w-full">
|
||||||
<div className="w-9/12">
|
<div className="w-10/12">
|
||||||
<a
|
<a
|
||||||
className="text‑inherit no-underline"
|
className="text‑inherit no-underline"
|
||||||
href={question.url}
|
href={question.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
></a>
|
||||||
{/*
|
|
||||||
<h1 className="text-3xl font-normal text-center mt-5">
|
|
||||||
{question.title}
|
|
||||||
</h1>
|
|
||||||
*/}
|
|
||||||
</a>
|
|
||||||
<VictoryChart
|
<VictoryChart
|
||||||
domainPadding={20}
|
domainPadding={20}
|
||||||
padding={padding}
|
padding={padding}
|
||||||
|
@ -154,15 +167,16 @@ export const HistoryChart: React.FC<Props> = ({ question }) => {
|
||||||
width={width}
|
width={width}
|
||||||
containerComponent={
|
containerComponent={
|
||||||
<VictoryVoronoiContainer
|
<VictoryVoronoiContainer
|
||||||
labels={({ datum }) =>
|
labels={({ datum }) => `Not shown`}
|
||||||
`${datum.x}: ${Math.round(datum.y * 100)}%`
|
|
||||||
}
|
|
||||||
labelComponent={
|
labelComponent={
|
||||||
<VictoryTooltip
|
<VictoryTooltip
|
||||||
pointerLength={0}
|
pointerLength={0}
|
||||||
dy={-12}
|
dy={-12}
|
||||||
|
text={({ datum }) =>
|
||||||
|
`${datum.name}: ${Math.round(datum.y * 100)}%`
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 10,
|
fontSize: 15,
|
||||||
fill: "black",
|
fill: "black",
|
||||||
strokeWidth: 0.05,
|
strokeWidth: 0.05,
|
||||||
}}
|
}}
|
||||||
|
@ -170,40 +184,31 @@ export const HistoryChart: React.FC<Props> = ({ question }) => {
|
||||||
stroke: "black",
|
stroke: "black",
|
||||||
fill: "white",
|
fill: "white",
|
||||||
}}
|
}}
|
||||||
flyoutWidth={80}
|
|
||||||
cornerRadius={0}
|
cornerRadius={0}
|
||||||
flyoutPadding={7}
|
flyoutPadding={7}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
voronoiBlacklist={
|
voronoiBlacklist={
|
||||||
["line-0", "line-1", "line-2", "line-3", "line-4"]
|
["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, domainMax],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<VictoryLegend
|
<VictoryLegend
|
||||||
x={width - 100}
|
x={width - labelLegendStart - letterLength * longestNameLength}
|
||||||
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" }, labels: { fontSize: 15 } }}
|
||||||
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: "Two", symbol: { fill: "orange" } },
|
|
||||||
{ name: "Three", symbol: { fill: "gold" } },
|
|
||||||
]*/
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{dataSets
|
{dataSets
|
||||||
|
@ -224,9 +229,9 @@ export const HistoryChart: React.FC<Props> = ({ question }) => {
|
||||||
// label="Date (dd/mm/yy)"
|
// label="Date (dd/mm/yy)"
|
||||||
tickLabelComponent={
|
tickLabelComponent={
|
||||||
<VictoryLabel
|
<VictoryLabel
|
||||||
dy={0}
|
dy={10}
|
||||||
angle={-30}
|
angle={-30}
|
||||||
style={{ fontSize: 10, fill: "gray" }}
|
style={{ fontSize: 15, fill: "gray" }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -238,7 +243,7 @@ export const HistoryChart: React.FC<Props> = ({ question }) => {
|
||||||
grid: { stroke: "#D3D3D3", strokeWidth: 0.5 },
|
grid: { stroke: "#D3D3D3", strokeWidth: 0.5 },
|
||||||
}}
|
}}
|
||||||
tickLabelComponent={
|
tickLabelComponent={
|
||||||
<VictoryLabel dy={0} style={{ fontSize: 10, fill: "gray" }} />
|
<VictoryLabel dy={0} style={{ fontSize: 15, fill: "gray" }} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</VictoryChart>
|
</VictoryChart>
|
||||||
|
|
227
src/web/questions/components/QuestionIndicators.tsx
Normal file
227
src/web/questions/components/QuestionIndicators.tsx
Normal file
|
@ -0,0 +1,227 @@
|
||||||
|
import { QuestionFragment } from "../../fragments.generated";
|
||||||
|
|
||||||
|
type QualityIndicator = QuestionFragment["qualityIndicators"];
|
||||||
|
type IndicatorName = keyof QualityIndicator;
|
||||||
|
|
||||||
|
// this duplication can probably be simplified with typescript magic, but this is good enough for now
|
||||||
|
type UsedIndicatorName =
|
||||||
|
| "volume"
|
||||||
|
| "numForecasters"
|
||||||
|
| "spread"
|
||||||
|
| "sharesVolume"
|
||||||
|
| "liquidity"
|
||||||
|
| "tradeVolume"
|
||||||
|
| "openInterest";
|
||||||
|
|
||||||
|
const qualityIndicatorLabels: { [k in UsedIndicatorName]: string } = {
|
||||||
|
// numForecasts: null,
|
||||||
|
// stars: null,
|
||||||
|
// yesBid: "Yes bid",
|
||||||
|
// yesAsk: "Yes ask",
|
||||||
|
volume: "Volume",
|
||||||
|
numForecasters: "Forecasters",
|
||||||
|
spread: "Spread",
|
||||||
|
sharesVolume: "Shares vol.",
|
||||||
|
liquidity: "Liquidity",
|
||||||
|
tradeVolume: "Volume",
|
||||||
|
openInterest: "Interest",
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatNumber = (num) => {
|
||||||
|
if (Number(num) < 1000) {
|
||||||
|
return Number(num).toFixed(0);
|
||||||
|
} else if (num < 10000) {
|
||||||
|
return (Number(num) / 1000).toFixed(1) + "k";
|
||||||
|
} else {
|
||||||
|
return (Number(num) / 1000).toFixed(0) + "k";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Display functions*/
|
||||||
|
|
||||||
|
const getPercentageSymbolIfNeeded = ({
|
||||||
|
indicator,
|
||||||
|
platform,
|
||||||
|
}: {
|
||||||
|
indicator: UsedIndicatorName;
|
||||||
|
platform: string;
|
||||||
|
}) => {
|
||||||
|
let indicatorsWhichNeedPercentageSymbol: IndicatorName[] = ["spread"];
|
||||||
|
if (indicatorsWhichNeedPercentageSymbol.includes(indicator)) {
|
||||||
|
return "%";
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrencySymbolIfNeeded = ({
|
||||||
|
indicator,
|
||||||
|
platform,
|
||||||
|
}: {
|
||||||
|
indicator: UsedIndicatorName;
|
||||||
|
platform: string;
|
||||||
|
}) => {
|
||||||
|
const indicatorsWhichNeedCurrencySymbol: IndicatorName[] = [
|
||||||
|
"volume",
|
||||||
|
"tradeVolume",
|
||||||
|
"openInterest",
|
||||||
|
"liquidity",
|
||||||
|
];
|
||||||
|
let dollarPlatforms = ["predictit", "kalshi", "polymarket"];
|
||||||
|
if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) {
|
||||||
|
if (dollarPlatforms.includes(platform)) {
|
||||||
|
return "$";
|
||||||
|
} else {
|
||||||
|
return "£";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const FirstQualityIndicator: React.FC<{
|
||||||
|
question: QuestionFragment;
|
||||||
|
}> = ({ question }) => {
|
||||||
|
if (question.qualityIndicators.numForecasts) {
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
<span>Forecasts:</span>
|
||||||
|
<span className="font-bold">
|
||||||
|
{Number(question.qualityIndicators.numForecasts).toFixed(0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const QualityIndicatorsList: React.FC<{
|
||||||
|
question: QuestionFragment;
|
||||||
|
}> = ({ question }) => {
|
||||||
|
return (
|
||||||
|
<div className="text-sm">
|
||||||
|
<FirstQualityIndicator question={question} />
|
||||||
|
{Object.entries(question.qualityIndicators).map((entry, i) => {
|
||||||
|
const indicatorLabel = qualityIndicatorLabels[entry[0]];
|
||||||
|
if (!indicatorLabel || entry[1] === null) return;
|
||||||
|
const indicator = entry[0] as UsedIndicatorName; // guaranteed by the previous line
|
||||||
|
const value = entry[1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={indicator}>
|
||||||
|
<span>{indicatorLabel}:</span>
|
||||||
|
<span className="font-bold">
|
||||||
|
{`${getCurrencySymbolIfNeeded({
|
||||||
|
indicator,
|
||||||
|
platform: question.platform.id,
|
||||||
|
})}${formatNumber(value)}${getPercentageSymbolIfNeeded({
|
||||||
|
indicator,
|
||||||
|
platform: question.platform.id,
|
||||||
|
})}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Database-like functions
|
||||||
|
export function getstars(numstars: number) {
|
||||||
|
let stars = "★★☆☆☆";
|
||||||
|
switch (numstars) {
|
||||||
|
case 0:
|
||||||
|
stars = "☆☆☆☆☆";
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
stars = "★☆☆☆☆";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
stars = "★★☆☆☆";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
stars = "★★★☆☆";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
stars = "★★★★☆";
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
stars = "★★★★★";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
stars = "★★☆☆☆";
|
||||||
|
}
|
||||||
|
return stars;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStarsColor(numstars: number) {
|
||||||
|
let color = "text-yellow-400";
|
||||||
|
switch (numstars) {
|
||||||
|
case 0:
|
||||||
|
color = "text-red-400";
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
color = "text-red-400";
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
color = "text-orange-400";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
color = "text-yellow-400";
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
color = "text-green-400";
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
color = "text-blue-400";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
color = "text-yellow-400";
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
question: QuestionFragment;
|
||||||
|
expandFooterToFullWidth: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuestionFooter: React.FC<Props> = ({
|
||||||
|
question,
|
||||||
|
expandFooterToFullWidth,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`grid grid-cols-3 ${
|
||||||
|
expandFooterToFullWidth ? "justify-between" : ""
|
||||||
|
} text-gray-500 mb-2 mt-1`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`self-center col-span-1 ${getStarsColor(
|
||||||
|
question.qualityIndicators.stars
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{getstars(question.qualityIndicators.stars)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
expandFooterToFullWidth ? "place-self-center" : "self-center"
|
||||||
|
} col-span-1 font-bold`}
|
||||||
|
>
|
||||||
|
{question.platform.label
|
||||||
|
.replace("Good Judgment Open", "GJOpen")
|
||||||
|
.replace(/ /g, "\u00a0")}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
expandFooterToFullWidth
|
||||||
|
? "justify-self-end mr-4"
|
||||||
|
: "justify-self-center"
|
||||||
|
} col-span-1`}
|
||||||
|
>
|
||||||
|
<QualityIndicatorsList question={question} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -61,7 +61,7 @@ const QuestionCardContents: React.FC<{
|
||||||
|
|
||||||
*/}
|
*/}
|
||||||
|
|
||||||
<h1 className="text-xl place-self-center w-full text-center mt-20">
|
<h1 className="pt-10 text-xl place-self-center w-full text-center ">
|
||||||
{"Question description"}
|
{"Question description"}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user