diff --git a/src/web/questions/components/HistoryChart.tsx b/src/web/questions/components/HistoryChart.tsx index cd6a404..ad04e10 100644 --- a/src/web/questions/components/HistoryChart.tsx +++ b/src/web/questions/components/HistoryChart.tsx @@ -31,6 +31,25 @@ let getDate0 = (x) => { 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) => { // for real timestamps console.log(x); @@ -49,6 +68,7 @@ let dataAsXy = (data) => data.map((datum) => ({ x: timestampToString(datum.date), //getDate(datum.date * (1000 * 60 * 60 * 24)), y: datum.probability, + name: datum.name, })); const colors = ["dodgerblue", "crimson", "seagreen", "darkviolet", "turquoise"]; @@ -74,10 +94,6 @@ const getVictoryGroup = (data, i) => { }; export const HistoryChart: React.FC = ({ 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 = []; question.history.forEach((item) => { let optionNames = item.options.map((option) => option.name); @@ -85,28 +101,10 @@ export const HistoryChart: React.FC = ({ question }) => { }); dataSetsNames = [...new Set(dataSetsNames)].slice(0, 5); // take the first 5 let dataSets = []; - /* - dataSetsNames.forEach((name) => { - 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); - }); - */ + let maxProbability = 0; + let longestNameLength = 0; - dataSetsNames.forEach((name) => { + for (let name of dataSetsNames) { let newDataset = []; let previousDate = -Infinity; for (let item of question.history) { @@ -121,31 +119,46 @@ export const HistoryChart: React.FC = ({ question }) => { let result = { date, 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); // } previousDate = item.timestamp; } } 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 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 (
-
+
- {/* -

- {question.title} -

- */} -
+ > = ({ question }) => { width={width} containerComponent={ - `${datum.x}: ${Math.round(datum.y * 100)}%` - } + labels={({ datum }) => `Not shown`} labelComponent={ + `${datum.name}: ${Math.round(datum.y * 100)}%` + } style={{ - fontSize: 10, + fontSize: 15, fill: "black", strokeWidth: 0.05, }} @@ -170,40 +184,31 @@ export const HistoryChart: React.FC = ({ question }) => { stroke: "black", fill: "white", }} - flyoutWidth={80} cornerRadius={0} flyoutPadding={7} /> } voronoiBlacklist={ ["line-0", "line-1", "line-2", "line-3", "line-4"] - //Array.from(Array(5).keys()).map((x, i) => `line${i}`) // see: https://github.com/FormidableLabs/victory/issues/545 } /> } domain={{ - y: [0, 1], + y: [0, domainMax], }} > ({ - name: dataSetsNames[i], - symbol: { fill: colors[i] }, - })) - /*[ - { name: "One", symbol: { fill: "tomato", type: "star" } }, - { name: "Two", symbol: { fill: "orange" } }, - { name: "Three", symbol: { fill: "gold" } }, - ]*/ - } + style={{ border: { stroke: "black" }, labels: { fontSize: 15 } }} + data={Array.from(Array(dataSetsLength).keys()).map((i) => ({ + name: dataSetsNames[i], + symbol: { fill: colors[i] }, + }))} /> {dataSets @@ -224,9 +229,9 @@ export const HistoryChart: React.FC = ({ question }) => { // label="Date (dd/mm/yy)" tickLabelComponent={ } /> @@ -238,7 +243,7 @@ export const HistoryChart: React.FC = ({ question }) => { grid: { stroke: "#D3D3D3", strokeWidth: 0.5 }, }} tickLabelComponent={ - + } /> diff --git a/src/web/questions/components/QuestionIndicators.tsx b/src/web/questions/components/QuestionIndicators.tsx new file mode 100644 index 0000000..ecd37da --- /dev/null +++ b/src/web/questions/components/QuestionIndicators.tsx @@ -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 ( +
+ Forecasts:  + + {Number(question.qualityIndicators.numForecasts).toFixed(0)} + +
+ ); + } else { + return null; + } +}; + +const QualityIndicatorsList: React.FC<{ + question: QuestionFragment; +}> = ({ question }) => { + return ( +
+ + {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 ( +
+ {indicatorLabel}:  + + {`${getCurrencySymbolIfNeeded({ + indicator, + platform: question.platform.id, + })}${formatNumber(value)}${getPercentageSymbolIfNeeded({ + indicator, + platform: question.platform.id, + })}`} + +
+ ); + })} +
+ ); +}; + +// 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 = ({ + question, + expandFooterToFullWidth, +}) => { + return ( +
+
+ {getstars(question.qualityIndicators.stars)} +
+
+ {question.platform.label + .replace("Good Judgment Open", "GJOpen") + .replace(/ /g, "\u00a0")} +
+
+ +
+
+ ); +}; diff --git a/src/web/questions/pages/QuestionPage.tsx b/src/web/questions/pages/QuestionPage.tsx index 739c251..50eace2 100644 --- a/src/web/questions/pages/QuestionPage.tsx +++ b/src/web/questions/pages/QuestionPage.tsx @@ -61,7 +61,7 @@ const QuestionCardContents: React.FC<{ */} -

+

{"Question description"}