= ({
entries.forEach((e, idx) => {
const used = resolutions[idx]?.used;
if (!used) return;
+ if (!hasDataForMetricAndGeo(e.timeseriesdata, selectedPlot, used)) return;
e.timeseriesdata?.data
?.filter(
(d) =>
diff --git a/src/components/DownloadDataset.test.tsx b/src/components/DownloadDataset.test.tsx
index 4fba5b43..395bf941 100644
--- a/src/components/DownloadDataset.test.tsx
+++ b/src/components/DownloadDataset.test.tsx
@@ -41,4 +41,27 @@ describe("", () => {
screen.getByText(/Minor deviations from the source data may occur/),
).toBeInTheDocument();
});
+
+ it("says the download is complete only when it runs past the plots", () => {
+ const { rerender } = render(
+ ,
+ );
+ expect(screen.queryByText(/show years up to 2050/)).toBeNull();
+
+ rerender(
+ ,
+ );
+ expect(
+ screen.getByText(
+ "The plots on this page show years up to 2050. This download contains every year.",
+ ),
+ ).toBeInTheDocument();
+ });
});
diff --git a/src/components/DownloadDataset.tsx b/src/components/DownloadDataset.tsx
index e99d981c..da8faf42 100644
--- a/src/components/DownloadDataset.tsx
+++ b/src/components/DownloadDataset.tsx
@@ -1,11 +1,17 @@
// src/components/DownloadDataset.tsx
import React from "react";
import AdditionalInfoBox from "./AdditionalInfoBox";
+import { MAX_PLOT_YEAR } from "../utils/plotYears";
type Props = {
label: string;
href: string;
summary?: string;
+ /**
+ * The dataset runs past the last year the plots show, so the summary's year
+ * range and the plots disagree; say that the download is complete.
+ */
+ extendsPastPlots?: boolean;
className?: string;
};
@@ -13,6 +19,7 @@ export default function DownloadDataset({
label,
href,
summary,
+ extendsPastPlots = false,
className,
}: Props) {
return (
@@ -34,6 +41,12 @@ export default function DownloadDataset({
{summary && (
{summary}
)}
+ {extendsPastPlots && (
+
+ The plots on this page show years up to {MAX_PLOT_YEAR}. This download
+ contains every year.
+
+ )}
Note: Minor deviations from the source data may occur due to light
processing required for standardizing pathways to a common format.
diff --git a/src/components/PlotGrid.test.tsx b/src/components/PlotGrid.test.tsx
index 74cfc8ac..ea08ceda 100644
--- a/src/components/PlotGrid.test.tsx
+++ b/src/components/PlotGrid.test.tsx
@@ -1,14 +1,16 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import PlotGrid from "./PlotGrid";
+import MultiLineChart from "./MultiLineChart";
import type { Geography } from "../types";
vi.mock("./MultiLineChart", () => ({
- default: () =>
,
+ default: vi.fn(() => ),
}));
vi.mock("./NormalizedStackedAreaChart", () => ({
- default: () => ,
+ default: vi.fn(() => ),
}));
+const mockedMultiLineChart = vi.mocked(MultiLineChart);
const SEA: Geography = {
global: false,
@@ -59,6 +61,47 @@ describe("PlotGrid", () => {
expect(screen.getAllByTestId("multi-line-chart")).toHaveLength(2);
});
+ describe("years after 2050", () => {
+ const row = (metric: string, year: number) => ({
+ ...rows(metric, "Global")[0],
+ year: String(year),
+ });
+
+ it("hands the charts no year after 2050", () => {
+ mockedMultiLineChart.mockClear();
+ render(
+ row("capacity", y)),
+ }}
+ pathwayGeography={SEA}
+ />,
+ );
+
+ expect(mockedMultiLineChart).toHaveBeenCalled();
+ mockedMultiLineChart.mock.calls.forEach(([props]) => {
+ expect(props.data.data.map((d) => d.year)).toEqual(["2030", "2050"]);
+ });
+ });
+
+ it("drops a panel left with a single year once later years are cut", () => {
+ render(
+ row("capacity", y)),
+ ...[2030, 2040].map((y) => row("generation", y)),
+ ],
+ }}
+ pathwayGeography={SEA}
+ />,
+ );
+
+ expect(screen.queryByText("Capacity")).toBeNull();
+ expect(screen.getByText("Generation")).toBeInTheDocument();
+ });
+ });
+
it("skips a plot type that has no data at any geography", () => {
render(
= ({
- timeseriesdata,
+ timeseriesdata: fullTimeseriesdata,
datasetId,
pathwayGeography,
requestedGeography = null,
@@ -78,6 +79,12 @@ export const PlotGrid: React.FC = ({
title,
className = "",
}) => {
+ // Capped once, here, so everything below agrees with what is drawn.
+ const timeseriesdata = useMemo(
+ () => capToPlotYears(fullTimeseriesdata),
+ [fullTimeseriesdata],
+ );
+
const availableGeographies = useMemo(() => {
if (!timeseriesdata?.data) return [];
return Array.from(
diff --git a/src/pages/PathwayDetailPage.tsx b/src/pages/PathwayDetailPage.tsx
index 239bc06b..4c1a0dd8 100644
--- a/src/pages/PathwayDetailPage.tsx
+++ b/src/pages/PathwayDetailPage.tsx
@@ -37,6 +37,7 @@ import type { TimeSeries } from "../components/PlotSelector";
import PlotGrid from "../components/PlotGrid";
import { PLOT_ORDER } from "../components/PlotPanel";
import getTemperatureColor from "../utils/getTemperatureColor";
+import { extendsPastPlotYears } from "../utils/plotYears";
import TextWithTooltip from "../components/TextWithTooltip";
import RegionMembersTooltip from "../components/RegionMembersTooltip";
import {
@@ -417,7 +418,7 @@ const PathwayDetailPage: React.FC = () => {
{tsIndexLoaded && datasets.length > 0
- ? datasets.map((d) => {
+ ? datasets.map((d, i) => {
const label = d.label ?? d.datasetId;
const summary = summarizeSummary(d.summary);
return (
@@ -426,6 +427,11 @@ const PathwayDetailPage: React.FC = () => {
label={label}
href={d.path}
summary={summary}
+ // The plots draw the first dataset, so only it is known to
+ // run past them.
+ extendsPastPlots={
+ i === 0 && extendsPastPlotYears(timeseriesdata)
+ }
/>
);
})
diff --git a/src/utils/plotYears.test.ts b/src/utils/plotYears.test.ts
new file mode 100644
index 00000000..708317b7
--- /dev/null
+++ b/src/utils/plotYears.test.ts
@@ -0,0 +1,40 @@
+import { describe, it, expect } from "vitest";
+import {
+ MAX_PLOT_YEAR,
+ capToPlotYears,
+ extendsPastPlotYears,
+} from "./plotYears";
+
+const rows = (...years: (string | number)[]) => ({
+ data: years.map((year) => ({ year, value: 1 })),
+});
+
+describe("capToPlotYears", () => {
+ it("keeps 2050 and drops every later year", () => {
+ expect(MAX_PLOT_YEAR).toBe(2050);
+ const capped = capToPlotYears(rows(2040, 2050, 2051, 2100));
+ expect(capped?.data.map((d) => d.year)).toEqual([2040, 2050]);
+ });
+
+ it("reads string years as numbers", () => {
+ const capped = capToPlotYears(rows("2030", "2050", "2060"));
+ expect(capped?.data.map((d) => d.year)).toEqual(["2030", "2050"]);
+ });
+
+ it("keeps the other fields of the timeseries", () => {
+ const capped = capToPlotYears({ ...rows(2060), datasetId: "x" });
+ expect(capped).toEqual({ data: [], datasetId: "x" });
+ });
+
+ it("passes null through", () => {
+ expect(capToPlotYears(null)).toBeNull();
+ });
+});
+
+describe("extendsPastPlotYears", () => {
+ it("is true only when some row is after 2050", () => {
+ expect(extendsPastPlotYears(rows(2020, 2050))).toBe(false);
+ expect(extendsPastPlotYears(rows(2020, "2055"))).toBe(true);
+ expect(extendsPastPlotYears(null)).toBe(false);
+ });
+});
diff --git a/src/utils/plotYears.ts b/src/utils/plotYears.ts
new file mode 100644
index 00000000..96756de8
--- /dev/null
+++ b/src/utils/plotYears.ts
@@ -0,0 +1,35 @@
+/**
+ * The last year the benchmark plots show (#1010). Pathways end anywhere from
+ * 2030 to 2100, and plots with wildly different x-axes are hard to compare,
+ * so every plot stops here. Downloads still carry the full data.
+ */
+export const MAX_PLOT_YEAR = 2050;
+
+interface YearRow {
+ year: string | number;
+}
+
+/**
+ * The timeseries without its rows after {@link MAX_PLOT_YEAR}. Apply it where
+ * rows enter the plots, before anything is derived from them, so the shared
+ * y-axis, which panels show and the segment badges all agree with what is
+ * drawn.
+ *
+ * `Number()` because the type says string but the JSON carries numbers.
+ */
+export function capToPlotYears(
+ timeseries: T | null,
+): T | null {
+ if (!timeseries?.data) return timeseries;
+ return {
+ ...timeseries,
+ data: timeseries.data.filter((d) => Number(d.year) <= MAX_PLOT_YEAR),
+ };
+}
+
+/** Whether a timeseries has rows the plots leave out. */
+export function extendsPastPlotYears(
+ timeseries: { data: readonly YearRow[] } | null,
+): boolean {
+ return timeseries?.data?.some((d) => Number(d.year) > MAX_PLOT_YEAR) ?? false;
+}