Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/components/charts/AdvancedChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export const AdvancedChart = () => {
const _output: TVChartFormattedData[] = [];
seasonsData.data.forEach((seasonData) => {
// Verify a datapoint is available for this season (some data, like tractor, is not since season 1)
if (selectedChart.priceScaleKey in seasonData) {
if (seasonData[selectedChart.priceScaleKey] !== undefined && seasonData[selectedChart.priceScaleKey] !== null) {
const formatValue = selectedChart.valueFormatter;
const dataPoint = {
value: formatValue(seasonData[selectedChart.priceScaleKey]),
Expand Down Expand Up @@ -262,6 +262,11 @@ export const AdvancedChart = () => {
</div>
<CalendarButton setTimePeriod={setTimePeriod} />
</div>
{seasonsData.hasUnavailableApiData && (
<p role="status" className="px-4 py-2 text-sm text-pinto-light">
Some chart data is temporarily unavailable. Available charts are still shown.
</p>
)}
{selectedCharts.length > 0 ? (
<TVChart
selected={selectedCharts}
Expand Down
35 changes: 3 additions & 32 deletions src/components/charts/TVChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "lightweight-charts";
import { MutableRefObject, useEffect, useMemo, useRef, useState } from "react";
import TooltipSimple from "../TooltipSimple";
import { getChartDataPoint } from "./chartDataPoint";

export type TVChartFormattedData = {
time: Time;
Expand Down Expand Up @@ -405,40 +406,10 @@ const TVChart = ({ formattedData, height = 500, timePeriod, selected, seasonInpu
}
}

// Get initial data point values
function getDataPoint(mode: string) {
if (!formattedData) return undefined;

let _time = 0;
const _value: number[] = [];
let _season = 0;

selected.forEach((_, i) => {
const selectedData = formattedData[i];
if (!selectedData || selectedData.length === 0) return;

const dataIndex = mode === "last" ? selectedData.length - 1 : 0;
if (!selectedData[dataIndex]) return;

_time = Math.max(_time, selectedData[dataIndex]?.time?.valueOf() as number);
_season = Math.max(_season, selectedData[dataIndex]?.customValues?.season || 0);
_value.push(selectedData[dataIndex]?.value);
});

if (_time === 0) return undefined;

return {
time: new Date(_time * 1000)?.toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" }),
value: _value,
season: _season,
timestamp: _time,
};
}

// Set initial points only if we have valid data
if (dataWasSet) {
const lastPoint = getDataPoint("last");
const firstPoint = getDataPoint("first");
const lastPoint = getChartDataPoint(formattedData, selected.length, "last");
const firstPoint = getChartDataPoint(formattedData, selected.length, "first");

if (lastPoint) setLastDataPoint(lastPoint);
if (firstPoint) setFirstDataPoint(firstPoint);
Expand Down
22 changes: 22 additions & 0 deletions src/components/charts/__tests__/chartDataPoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { UTCTimestamp } from "lightweight-charts";
import { describe, expect, it } from "vitest";
import { getChartDataPoint } from "../chartDataPoint";

const prices = [
{ time: 1793412057 as UTCTimestamp, value: 0.032, customValues: { season: 17027 } },
{ time: 1793415657 as UTCTimestamp, value: 0.033, customValues: { season: 17028 } },
];

describe("chart legend with unavailable series", () => {
it.each(["first", "last"] as const)(
"keeps %s values under their own labels when the first series is empty",
(mode) => {
const point = getChartDataPoint([[], prices, []], 3, mode);
expect(point?.value).toEqual([undefined, mode === "first" ? 0.032 : 0.033, undefined]);
expect(point?.season).toBe(mode === "first" ? 17027 : 17028);
},
);
it("does not invent a data point when every series is unavailable", () => {
expect(getChartDataPoint([[], []], 2, "last")).toBeUndefined();
});
});
34 changes: 34 additions & 0 deletions src/components/charts/chartDataPoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { TVChartFormattedData } from "./TVChart";

export function getChartDataPoint(
formattedData: TVChartFormattedData[][] | undefined,
numberOfSeries: number,
mode: "first" | "last",
) {
if (!formattedData) return undefined;

let _time = 0;
const _value: (number | undefined)[] = Array(numberOfSeries).fill(undefined);
let _season = 0;

Array.from({ length: numberOfSeries }).forEach((_, i) => {
const selectedData = formattedData[i];
if (!selectedData || selectedData.length === 0) return;

const dataIndex = mode === "last" ? selectedData.length - 1 : 0;
if (!selectedData[dataIndex]) return;

_time = Math.max(_time, selectedData[dataIndex]?.time?.valueOf() as number);
_season = Math.max(_season, selectedData[dataIndex]?.customValues?.season || 0);
_value[i] = selectedData[dataIndex]?.value;
});

if (_time === 0) return undefined;

return {
time: new Date(_time * 1000)?.toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" }),
value: _value,
season: _season,
timestamp: _time,
};
}
116 changes: 116 additions & 0 deletions src/state/__tests__/fixtures/advanced-chart.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
"seasons": [
{
"id": "17028",
"sunriseBlock": "53313155",
"rewardBeans": "0",
"price": "0.033041",
"deltaBeans": "1780901",
"raining": false,
"season": 17028,
"createdAt": "1793415657"
},
{
"id": "17027",
"sunriseBlock": "53311355",
"rewardBeans": "0",
"price": "0.033025",
"deltaBeans": "1780901",
"raining": false,
"season": 17027,
"createdAt": "1793412057"
}
],
"fieldHourlySnapshots": [
{
"id": "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f-17028",
"caseId": "1027",
"issuedSoil": "266480000",
"deltaSownBeans": "0",
"sownBeans": "13512605109982",
"deltaPodDemand": "0",
"blocksToSoldOutSoil": null,
"podRate": "3259.178343635143932583281748148457",
"temperature": "4755",
"deltaTemperature": "0.5",
"season": 17028,
"cultivationTemperature": "3488.5",
"harvestableIndex": "3155543269527",
"harvestablePods": "21777279235",
"harvestedPods": "3133765990292",
"numberOfSowers": 390,
"numberOfSows": 5691,
"podIndex": "46436088761157",
"realRateOfReturn": "1469.386519778457068490663115523138",
"seasonBlock": "53313155",
"soil": "266480000",
"soilSoldOut": false,
"unharvestablePods": "43280545491630"
},
{
"id": "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f-17027",
"caseId": "1027",
"issuedSoil": "266478717",
"deltaSownBeans": "0",
"sownBeans": "13512605109982",
"deltaPodDemand": "0",
"blocksToSoldOutSoil": null,
"podRate": "3259.615484643863734878209906363725",
"temperature": "4754.5",
"deltaTemperature": "0.5",
"season": 17027,
"cultivationTemperature": "3488.5",
"harvestableIndex": "3155543269527",
"harvestablePods": "21777279235",
"harvestedPods": "3133765990292",
"numberOfSowers": 390,
"numberOfSows": 5691,
"podIndex": "46436088761157",
"realRateOfReturn": "1469.947009841029523088569265707797",
"seasonBlock": "53311355",
"soil": "266478717",
"soilSoldOut": false,
"unharvestablePods": "43280545491630"
}
],
"siloHourlySnapshots": [
{
"id": "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f-17028",
"beanToMaxLpGpPerBdvRatio": "0",
"deltaBeanToMaxLpGpPerBdvRatio": "0",
"season": 17028,
"stalk": "392841813830493899432259",
"cropRatio": "50",
"deltaCropRatio": "0"
},
{
"id": "0xd1a0d188e861ed9d15773a2f3574a2e94134ba8f-17027",
"beanToMaxLpGpPerBdvRatio": "0",
"deltaBeanToMaxLpGpPerBdvRatio": "0",
"season": 17027,
"stalk": "392841813830493899432259",
"cropRatio": "50",
"deltaCropRatio": "0"
}
],
"gaugesInfoHourlySnapshots": [
{
"id": "gauges-17028",
"g0CultivationFactor": "1",
"g1BlightFactor": "0",
"g1ConvertDownPenalty": "0.08466532500037156",
"g2BdvConvertedThisSeason": "0",
"g2BonusStalkPerBdv": "1940949367",
"g2MaxConvertCapacity": "51381879"
},
{
"id": "gauges-17027",
"g0CultivationFactor": "1",
"g1BlightFactor": "0",
"g1ConvertDownPenalty": "0.08474835557550424",
"g2BdvConvertedThisSeason": "0",
"g2BonusStalkPerBdv": "1940949367",
"g2MaxConvertCapacity": "51381879"
}
]
}
106 changes: 106 additions & 0 deletions src/state/__tests__/useSeasonsData.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
import useSeasonsData from "../useSeasonsData";
import stalkData from "./fixtures/advanced-chart.json";

const api = vi.hoisted(() => ({
apy: undefined as any,
tractor: undefined as any,
inflow: undefined as any,
market: [] as any[],
prices: {} as any,
}));
vi.mock("wagmi", () => ({ useChainId: () => 8453 }));
vi.mock("../useTokenData", () => ({
default: () => ({ mainToken: { address: "0xb170000aeefa790fa61d6e837d1035906839a3c8", decimals: 6 } }),
}));
vi.mock("../seasonal/queries/useSeasonalInternalQueries", () => ({
default: () => ({ data: [], isLoading: false }),
useMultiSeasonalQueries: () => ({ data: stalkData, isLoading: false }),
}));
vi.mock("../seasonal/queries/useSeasonalAPY", () => ({
APYWindow: { DAILY: 24, WEEKLY: 168, MONTHLY: 720 },
useSeasonalAPYs: () => ({ data: api.apy, isLoading: false, isError: !api.apy }),
}));
vi.mock("../seasonal/queries/useSeasonalTractorSnapshots", () => ({
default: () => ({ data: api.tractor, isLoading: false, isError: !api.tractor }),
}));
vi.mock("../seasonal/queries/useSeasonalInflowSnapshots", () => ({
default: () => ({ data: api.inflow, isLoading: false, isError: !api.inflow }),
}));
vi.mock("../seasonal/queries/useSeasonalMarketPerformance", () => ({
SMPChartType: {},
useMarketPerformanceCalc: () => api.prices,
useSeasonalMarketPerformanceData: () => ({ data: api.market, isLoading: false }),
}));

function readSeasons(options = {}) {
let result: ReturnType<typeof useSeasonsData> | undefined;
function Probe() {
result = useSeasonsData(17028, 17028, {
seasonSync: true,
beanData: false,
basinData: false,
marketPerformanceData: false,
...options,
});
return null;
}
renderToStaticMarkup(createElement(Probe));
if (!result) throw new Error("Season probe did not render");
return result;
}

beforeEach(() => {
api.apy = undefined;
api.tractor = undefined;
api.inflow = undefined;
api.market = [];
api.prices = {};
});

describe("All explorer during a Pinto API outage", () => {
it("does not report errors for disabled API sources on the Seasons page", () => {
const result = readSeasons({ apyData: false, tractorData: false, inflowData: false });
expect(result.hasUnavailableApiData).toBe(false);
expect(result.data).toHaveLength(1);
});
it.each([undefined, []])("keeps subgraph prices when API results are %s", (missing) => {
api.tractor = missing;
api.inflow = missing;
const { data, hasUnavailableApiData } = readSeasons();
expect(hasUnavailableApiData).toBe(true);
expect(data).toHaveLength(1);
expect(data[0].season).toBe(17028);
expect(data[0].price.toNumber()).toBe(0.033);
expect(data[0].timestamp).toBe(1793415657);
expect(data[0]).not.toHaveProperty("pinto24h");
expect(data[0]).not.toHaveProperty("tractorSownPinto");
expect(data[0]).not.toHaveProperty("inflowAllDeltaNet");
});

it("shows real zero APY values when the API recovers, without filling missing windows with zero", () => {
api.apy = { 24: [{ season: 17028, value: 0, timestamp: new Date(1793415657000) }] };
const { data } = readSeasons();
expect(data).toHaveLength(1);
expect(data[0].pinto24h).toBe(0);
expect(data[0]).not.toHaveProperty("pinto7d");
expect(data[0]).not.toHaveProperty("pinto30d");
});
});

it("keeps price history when a market token has shorter or missing history", () => {
api.market = [
{ season: 17027, timestamp: "1793412057" },
{ season: 17028, timestamp: "1793415657" },
];
api.prices = { WETH: [{ season: 17028, value: 2000 }], cbETH: [], cbBTC: [], WSOL: [] };
const { data } = readSeasons({ marketPerformanceData: true });
expect(data[0].price.toNumber()).toBe(0.033);
expect(data[0].marketPriceWeth).toBe(2000);
expect(data[1].marketPriceWeth).toBeUndefined();
expect(data[0].marketPriceCbbtc).toBeUndefined();
expect(data[1].season).toBe(17027);
expect(data[1].timestamp).toBe(1793412057);
});
Loading
Loading