From f5db56fda4690303f8e812e07d5963e41a000ffc Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Mon, 11 Nov 2024 17:54:16 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(grapher)=20support=20multiple=20ch?= =?UTF-8?q?art=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/VariableEditPage.tsx | 17 +- adminSiteServer/testPageRouter.tsx | 35 ++-- baker/countryProfiles.tsx | 12 +- baker/updateChartEntities.ts | 6 +- ...808331-AddAvailableTabsToGrapherConfigs.ts | 198 ++++++++++++++++++ db/model/Variable.ts | 3 +- devTools/svgTester/utils.ts | 8 +- .../explorer/src/Explorer.jsdom.test.tsx | 6 +- .../explorer/src/Explorer.sample.tsx | 3 +- .../@ourworldindata/explorer/src/Explorer.tsx | 4 +- .../captionedChart/CaptionedChart.stories.tsx | 2 +- .../src/captionedChart/CaptionedChart.tsx | 4 +- .../grapher/src/chart/ChartUtils.tsx | 6 +- .../grapher/src/controls/ContentSwitchers.tsx | 65 +++++- .../grapher/src/controls/SettingsMenu.tsx | 2 +- .../src/controls/controlsRow/ControlsRow.tsx | 3 +- .../src/controls/settings/AbsRelToggle.tsx | 2 +- .../grapher/src/core/Grapher.jsdom.test.ts | 40 ++-- .../grapher/src/core/Grapher.stories.tsx | 6 +- .../grapher/src/core/Grapher.tsx | 136 ++++++++---- .../grapher/src/core/LegacyToOwidTable.ts | 6 +- .../src/dataTable/DataTable.jsdom.test.tsx | 4 +- .../grapher/src/dataTable/DataTable.sample.ts | 8 +- .../src/dataTable/DataTable.stories.tsx | 4 +- .../grapher/src/mapCharts/MapChart.sample.ts | 3 +- .../grapher/src/mapCharts/MapChart.tsx | 3 +- .../src/schema/defaultGrapherConfig.ts | 5 +- .../src/schema/grapher-schema.006.yaml | 60 +++--- .../types/src/grapherTypes/GrapherTypes.ts | 23 +- packages/@ourworldindata/utils/src/Util.ts | 31 ++- .../utils/src/grapherConfigUtils.test.ts | 28 +-- packages/@ourworldindata/utils/src/index.ts | 2 + site/gdocs/components/Chart.tsx | 2 + .../components/KeyIndicatorCollection.tsx | 19 +- site/multiembedder/MultiEmbedder.tsx | 25 +-- 35 files changed, 579 insertions(+), 202 deletions(-) create mode 100644 db/migration/1731316808331-AddAvailableTabsToGrapherConfigs.ts diff --git a/adminSiteClient/VariableEditPage.tsx b/adminSiteClient/VariableEditPage.tsx index 572a5830c3f..88061f314d7 100644 --- a/adminSiteClient/VariableEditPage.tsx +++ b/adminSiteClient/VariableEditPage.tsx @@ -687,18 +687,22 @@ class VariableEditor extends React.Component<{ @computed private get grapherConfig(): GrapherInterface { const { variable } = this.props const grapherConfig = variable.grapherConfig - if (grapherConfig) + if (grapherConfig) { + // TODO: add map tab if missing return { ...grapherConfig, - hasMapTab: true, - tab: GrapherTabOption.map, + // hasMapTab: true, + tab: GrapherTabOption.WorldMap, } - else + } else { return { yAxis: { min: 0 }, map: { columnSlug: this.props.variable.id.toString() }, - tab: GrapherTabOption.map, - hasMapTab: true, + tab: GrapherTabOption.WorldMap, + availableTabs: [ + GrapherTabOption.Table, + GrapherTabOption.WorldMap, + ], dimensions: [ { property: DimensionProperty.y, @@ -707,6 +711,7 @@ class VariableEditor extends React.Component<{ }, ], } + } } dispose!: IReactionDisposer diff --git a/adminSiteServer/testPageRouter.tsx b/adminSiteServer/testPageRouter.tsx index b0e5a5ac063..4d29bb4d435 100644 --- a/adminSiteServer/testPageRouter.tsx +++ b/adminSiteServer/testPageRouter.tsx @@ -149,8 +149,9 @@ async function propsFromQueryParams( if (params.type) { if (params.type === ChartTypeName.WorldMap) { + // TODO: add hasMapTab to chart configs table? query = query.andWhereRaw(`cc.full->>"$.hasMapTab" = "true"`) - tab = tab || GrapherTabOption.map + tab = tab || GrapherTabOption.WorldMap } else { if (params.type === "LineChart") { query = query.andWhereRaw( @@ -165,7 +166,8 @@ async function propsFromQueryParams( { type: params.type } ) } - tab = tab || GrapherTabOption.chart + // TODO + // tab = tab || GrapherTabOption.chart } } @@ -173,26 +175,30 @@ async function propsFromQueryParams( query = query.andWhereRaw( `cc.full->>'$.yAxis.canChangeScaleType' = "true" OR cc.full->>'$.xAxis.canChangeScaleType' = "true"` ) - tab = GrapherTabOption.chart + // TODO + // tab = GrapherTabOption.chart } if (params.comparisonLines) { query = query.andWhereRaw( `cc.full->'$.comparisonLines[0].yEquals' != ''` ) - tab = GrapherTabOption.chart + // TODO + // tab = GrapherTabOption.chart } if (params.stackMode) { query = query.andWhereRaw(`cc.full->'$.stackMode' = :stackMode`, { stackMode: params.stackMode, }) - tab = GrapherTabOption.chart + // TODO + // tab = GrapherTabOption.chart } if (params.relativeToggle) { query = query.andWhereRaw(`cc.full->>'$.hideRelativeToggle' = "false"`) - tab = GrapherTabOption.chart + // TODO + // tab = GrapherTabOption.chart } if (params.categoricalLegend) { @@ -202,7 +208,7 @@ async function propsFromQueryParams( query = query.andWhereRaw( `json_length(cc.full->'$.map.colorScale.customCategoryColors') > 1` ) - tab = GrapherTabOption.map + tab = GrapherTabOption.WorldMap } if (params.mixedTimeTypes) { @@ -242,13 +248,14 @@ async function propsFromQueryParams( query = query.andWhereRaw(`charts.id IN (${params.ids})`) } - if (tab === GrapherTabOption.map) { - query = query.andWhereRaw(`cc.full->>"$.hasMapTab" = "true"`) - } else if (tab === GrapherTabOption.chart) { - query = query.andWhereRaw( - `COALESCE(cc.full->>"$.hasChartTab", "true") = "true"` - ) - } + // TODO + // if (tab === GrapherTabOption.WorldMap) { + // query = query.andWhereRaw(`cc.full->>"$.hasMapTab" = "true"`) + // } else if (tab === GrapherTabOption.chart) { + // query = query.andWhereRaw( + // `COALESCE(cc.full->>"$.hasChartTab", "true") = "true"` + // ) + // } if (datasetIds.length > 0) { const datasetIds = params.datasetIds diff --git a/baker/countryProfiles.tsx b/baker/countryProfiles.tsx index 3c774d1aed1..5d21f95062f 100644 --- a/baker/countryProfiles.tsx +++ b/baker/countryProfiles.tsx @@ -16,7 +16,13 @@ import { CountryProfilePage, } from "../site/CountryProfilePage.js" import { SiteBaker } from "./SiteBaker.js" -import { countries, getCountryBySlug, JsonError } from "@ourworldindata/utils" +import { + countries, + getCountryBySlug, + JsonError, + getMainChartTypeFromConfig, + hasChartTabFromConfig, +} from "@ourworldindata/utils" import { renderToHtmlPage } from "./siteRenderers.js" import { dataAsDF } from "../db/model/Variable.js" import pl from "nodejs-polars" @@ -37,8 +43,8 @@ function bakeCache(cacheKey: any, retriever: () => T): T { } const checkShouldShowIndicator = (grapher: GrapherInterface) => - (grapher.hasChartTab ?? true) && - (grapher.type ?? "LineChart") === "LineChart" && + hasChartTabFromConfig(grapher) && + getMainChartTypeFromConfig(grapher) === "LineChart" && grapher.dimensions?.length === 1 // Find the charts that will be shown on the country profile page (if they have that country) diff --git a/baker/updateChartEntities.ts b/baker/updateChartEntities.ts index 10264e28155..4fa8ffa87ce 100644 --- a/baker/updateChartEntities.ts +++ b/baker/updateChartEntities.ts @@ -95,9 +95,11 @@ const obtainAvailableEntitiesForGrapherConfig = async ( ) grapher.receiveOwidData(variableData) + // TODO: add hasMaptab and hasChartTab as computed props + // If the grapher has a chart tab, then the available entities there are the "most interesting" ones to us if (grapher.hasChartTab) { - grapher.tab = GrapherTabOption.chart + grapher.tab = GrapherTabOption.LineChart // TODO // If the grapher allows for changing or multi-selecting entities, then let's index all entities the // user can choose from. Otherwise, we'll just use the default-selected entities. @@ -112,7 +114,7 @@ const obtainAvailableEntitiesForGrapherConfig = async ( return grapher.tableForSelection.availableEntityNames as string[] else return grapher.selectedEntityNames ?? [] } else if (grapher.hasMapTab) { - grapher.tab = GrapherTabOption.map + grapher.tab = GrapherTabOption.WorldMap // On a map tab, tableAfterAuthorTimelineAndActiveChartTransform contains all // mappable entities for which data is available return grapher.tableAfterAuthorTimelineAndActiveChartTransform diff --git a/db/migration/1731316808331-AddAvailableTabsToGrapherConfigs.ts b/db/migration/1731316808331-AddAvailableTabsToGrapherConfigs.ts new file mode 100644 index 00000000000..d707476057f --- /dev/null +++ b/db/migration/1731316808331-AddAvailableTabsToGrapherConfigs.ts @@ -0,0 +1,198 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class AddAvailableTabsToGrapherConfigs1731316808331 + implements MigrationInterface +{ + private async updateSchema( + queryRunner: QueryRunner, + newSchema: string + ): Promise { + await queryRunner.query( + ` + -- sql + UPDATE chart_configs + SET + patch = JSON_SET(patch, '$.$schema', ?), + full = JSON_SET(full, '$.$schema', ?) + `, + [newSchema, newSchema] + ) + } + + private async addAvailableTabsToGrapherConfigs( + queryRunner: QueryRunner, + config: "patch" | "full" + ): Promise { + // case 1: + // hasMapTab=false, hasChartTab=false -> availableTabs=[] + await queryRunner.query( + ` + -- sql + UPDATE chart_configs + SET + ?? = JSON_SET(??, '$.availableTabs', JSON_ARRAY('Table')) + WHERE + (?? ->> '$.hasMapTab' = 'false' OR ?? ->> '$.hasMapTab' IS NULL) + AND (?? ->> '$.hasChartTab' = 'false') + `, + [config, config, config, config, config] + ) + + // case 2: + // hasMapTab=false, hasChartTab=true -> availableTabs=[chartType] + await queryRunner.query( + ` + -- sql + UPDATE chart_configs + SET + ?? = JSON_SET(??, '$.availableTabs', JSON_ARRAY('Table', COALESCE(?? ->> '$.type', 'LineChart'))) + WHERE + (?? ->> '$.hasMapTab' = 'false' OR ?? ->> '$.hasMapTab' IS NULL) + AND (?? ->> '$.hasChartTab' = 'true' OR ?? ->> '$.hasChartTab' IS NULL) + `, + [config, config, config, config, config, config, config] + ) + + // case 3: + // hasMapTab=true, hasChartTab=false -> availableTabs=["WorldMap"] + await queryRunner.query( + ` + -- sql + UPDATE chart_configs + SET + ?? = JSON_SET(??, '$.availableTabs', JSON_ARRAY('Table', 'WorldMap')) + WHERE + (?? ->> '$.hasMapTab' = 'true') + AND (?? ->> '$.hasChartTab' = 'false') + `, + [config, config, config, config] + ) + + // case 4: + // hasMapTab=true, hasChartTab=true -> availableTabs=["WorldMap", chartType] + await queryRunner.query( + ` + -- sql + UPDATE chart_configs + SET + ?? = JSON_SET(??, '$.availableTabs', JSON_ARRAY('Table', 'WorldMap', COALESCE(?? ->> '$.type', 'LineChart'))) + WHERE + (?? ->> '$.hasMapTab' = 'true') + AND (?? ->> '$.hasChartTab' = 'true' OR ?? ->> '$.hasChartTab' IS NULL) + `, + [config, config, config, config, config, config] + ) + } + + private async removeAvailableTabsFromGrapherConfigs( + queryRunner: QueryRunner + ): Promise { + // TODO + } + + private async updateTabField( + queryRunner: QueryRunner, + config: "patch" | "full" + ): Promise { + await queryRunner.query( + ` + -- sql + UPDATE chart_configs + SET ?? = JSON_SET(??, '$.tab', 'WorldMap') + WHERE ?? ->> '$.tab' = 'map' + `, + [config, config, config] + ) + + await queryRunner.query( + ` + -- sql + UPDATE chart_configs + SET ?? = JSON_SET(??, '$.tab', 'Table') + WHERE ?? ->> '$.tab' = 'table' + `, + [config, config, config] + ) + + await queryRunner.query( + ` + -- sql + UPDATE chart_configs + SET ?? = JSON_SET(??, '$.tab', COALESCE(?? ->> '$.type', 'LineChart')) + WHERE ?? ->> '$.tab' = 'chart' + `, + [config, config, config, config] + ) + } + + private async removeTypeHasMapTabAndHasChartTabFields( + queryRunner: QueryRunner + ): Promise { + await queryRunner.query(` + -- sql + UPDATE chart_configs + SET patch = JSON_REMOVE(patch, '$.type', '$.hasChartTab', '$.hasMapTab') + `) + } + + private async addTypeHasMapTabAndHasChartTabFields( + queryRunner: QueryRunner + ): Promise { + // TODO + } + + private async addDerivedTypeColumn( + queryRunner: QueryRunner + ): Promise { + // TODO: not future-proof + await queryRunner.query( + `-- sql + ALTER TABLE chart_configs + ADD COLUMN type VARCHAR(255) GENERATED ALWAYS AS + ( + CASE + WHEN JSON_LENGTH(full, '$.availableTabs') = 0 THEN NULL + WHEN full ->> '$.availableTabs[0]' != 'Map' THEN full ->> '$.availableTabs[0]' + ELSE full ->> '$.availableTabs[1]' + END + ) + VIRTUAL AFTER slug; + ` + ) + } + + public async removeDerivedTypeColumn( + queryRunner: QueryRunner + ): Promise { + await queryRunner.query( + `-- sql + ALTER TABLE chart_configs + DROP COLUMN type; + ` + ) + } + + public async up(queryRunner: QueryRunner): Promise { + await this.addAvailableTabsToGrapherConfigs(queryRunner, "patch") + await this.addAvailableTabsToGrapherConfigs(queryRunner, "full") + await this.updateTabField(queryRunner, "patch") + await this.updateTabField(queryRunner, "full") + // await this.removeTypeHasMapTabAndHasChartTabFields(queryRunner) + // await this.updateSchema( + // queryRunner, + // "https://files.ourworldindata.org/schemas/grapher-schema.007.json" + // ) + } + + public async down(queryRunner: QueryRunner): Promise { + // TODO: order! + // await this.removeAvailableTabsFromGrapherConfigs(queryRunner) + // await this.revertTabFieldMigration(queryRunner) + // await this.addTypeHasMapTabAndHasChartTabFields(queryRunner) + // await this.updateSchema( + // queryRunner, + // "https://files.ourworldindata.org/schemas/grapher-schema.006.json" + // ) + // await this.removeDerivedTypeColumn(queryRunner) + } +} diff --git a/db/model/Variable.ts b/db/model/Variable.ts index 7dd6130a28e..7bd1c71e56a 100644 --- a/db/model/Variable.ts +++ b/db/model/Variable.ts @@ -7,6 +7,7 @@ import { omitUndefinedValues, mergeGrapherConfigs, diffGrapherConfigs, + getMainChartTypeFromConfig, } from "@ourworldindata/utils" import { getVariableDataRoute, @@ -878,7 +879,7 @@ export async function getVariableOfDatapageIfApplicable( // showing a data page. if ( yVariableIds.length === 1 && - (grapher.type !== ChartTypeName.ScatterPlot || + (getMainChartTypeFromConfig(grapher) !== ChartTypeName.ScatterPlot || xVariableIds.length === 0) ) { const variableId = yVariableIds[0] diff --git a/devTools/svgTester/utils.ts b/devTools/svgTester/utils.ts index 239466c9af7..a094edb2765 100644 --- a/devTools/svgTester/utils.ts +++ b/devTools/svgTester/utils.ts @@ -1,5 +1,6 @@ import { ChartTypeName, GrapherTabOption } from "@ourworldindata/types" import { + getMainChartTypeFromConfig, MultipleOwidVariableDataDimensionsMap, OwidVariableMixedData, OwidVariableWithSourceAndDimension, @@ -205,7 +206,8 @@ export async function findChartViewsToGenerate( const grapherConfig = await parseGrapherConfig(chartId, { inDir }) const slug = grapherConfig.slug ?? chartId.toString() - const chartType = grapherConfig.type ?? ChartTypeName.LineChart + const chartType = + getMainChartTypeFromConfig(grapherConfig) ?? ChartTypeName.LineChart const queryStrings = options.shouldTestAllViews ? queryStringsByChartType[chartType] @@ -283,7 +285,9 @@ export async function findValidChartIds( const grapherConfig = await parseGrapherConfig(grapherId, { inDir, }) - const chartType = grapherConfig.type ?? ChartTypeName.LineChart + const chartType = + getMainChartTypeFromConfig(grapherConfig) ?? + ChartTypeName.LineChart if (chartTypes.includes(chartType)) { validChartIds.push(grapherId) } diff --git a/packages/@ourworldindata/explorer/src/Explorer.jsdom.test.tsx b/packages/@ourworldindata/explorer/src/Explorer.jsdom.test.tsx index 644852c0684..1b6a6410138 100755 --- a/packages/@ourworldindata/explorer/src/Explorer.jsdom.test.tsx +++ b/packages/@ourworldindata/explorer/src/Explorer.jsdom.test.tsx @@ -30,20 +30,20 @@ describe(Explorer, () => { explorer.onChangeChoice("Gas")("All GHGs (CO₂eq)") - if (explorer.grapher) explorer.grapher.tab = GrapherTabOption.table + if (explorer.grapher) explorer.grapher.tab = GrapherTabOption.Table else throw Error("where's the grapher?") expect(explorer.queryParams.tab).toEqual("table") explorer.onChangeChoice("Gas")("CO₂") expect(explorer.queryParams.tab).toEqual("table") - explorer.grapher.tab = GrapherTabOption.chart + explorer.grapher.tab = GrapherTabOption.Table }) it("switches to first tab if current tab does not exist in new view", () => { const explorer = element.instance() as Explorer expect(explorer.queryParams.tab).toBeUndefined() - if (explorer.grapher) explorer.grapher.tab = GrapherTabOption.map + if (explorer.grapher) explorer.grapher.tab = GrapherTabOption.WorldMap else throw Error("where's the grapher?") expect(explorer.queryParams.tab).toEqual("map") diff --git a/packages/@ourworldindata/explorer/src/Explorer.sample.tsx b/packages/@ourworldindata/explorer/src/Explorer.sample.tsx index a15b805a6f5..d7646e8a5ac 100644 --- a/packages/@ourworldindata/explorer/src/Explorer.sample.tsx +++ b/packages/@ourworldindata/explorer/src/Explorer.sample.tsx @@ -54,7 +54,8 @@ export const SampleExplorerOfGraphers = (props?: Partial) => { property: DimensionProperty.y, }, ], - tab: GrapherTabOption.chart, + // TODO + tab: GrapherTabOption.LineChart, owidDataset: new Map([ [ 142609, diff --git a/packages/@ourworldindata/explorer/src/Explorer.tsx b/packages/@ourworldindata/explorer/src/Explorer.tsx index 65498e161e7..d133ac2ab4d 100644 --- a/packages/@ourworldindata/explorer/src/Explorer.tsx +++ b/packages/@ourworldindata/explorer/src/Explorer.tsx @@ -450,11 +450,11 @@ export class Explorer // preserve the previous tab if that's still available in the new view; // and use the first tab otherwise, ignoring the table const tabsWithoutTable = this.grapher.availableTabs.filter( - (tab) => tab !== GrapherTabOption.table + (tab) => tab !== GrapherTabOption.Table ) newGrapherParams.tab = this.grapher.availableTabs.includes(previousTab) ? previousTab - : tabsWithoutTable[0] ?? GrapherTabOption.table + : tabsWithoutTable[0] ?? GrapherTabOption.Table this.grapher.populateFromQueryParams(newGrapherParams) diff --git a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.stories.tsx b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.stories.tsx index 63157d73e38..ccf152c3cae 100644 --- a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.stories.tsx +++ b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.stories.tsx @@ -48,7 +48,7 @@ export const StaticLineChartForExport = (): React.ReactElement => { } export const MapChart = (): React.ReactElement => ( - + ) export const StackedArea = (): React.ReactElement => ( { const { manager } = this const bounds = this.boundsForChartArea const ChartClass = - ChartComponentClassMap.get(this.chartTypeName) ?? DefaultChartClass + ChartComponentClassMap.get( + this.manager.tab as unknown as ChartTypeName + ) ?? DefaultChartClass // Todo: make FacetChart a chart type name? if (this.isFaceted) diff --git a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx index 7a17f9980dd..01c75d34ae7 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx +++ b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx @@ -1,6 +1,10 @@ import React from "react" import { Box, getCountryByName } from "@ourworldindata/utils" -import { SeriesStrategy, EntityName } from "@ourworldindata/types" +import { + SeriesStrategy, + EntityName, + GrapherTabOption, +} from "@ourworldindata/types" import { LineChartSeries } from "../lineCharts/LineChartConstants" import { SelectionArray } from "../selection/SelectionArray" import { ChartManager } from "./ChartManager" diff --git a/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx b/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx index 051ba277ffc..c0c94733e9d 100644 --- a/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx +++ b/packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx @@ -78,14 +78,14 @@ export class ContentSwitchers extends React.Component<{ }, 0) } - private tabIcon(tab: GrapherTabOption): React.ReactElement { + private getTabIcon(tab: GrapherTabOption): React.ReactElement { const { manager } = this switch (tab) { - case GrapherTabOption.table: + case GrapherTabOption.Table: return - case GrapherTabOption.map: + case GrapherTabOption.WorldMap: return - case GrapherTabOption.chart: + default: const chartIcon = manager.isLineChartThatTurnedIntoDiscreteBar ? chartIcons[ChartTypeName.DiscreteBar] : chartIcons[this.chartType] @@ -93,17 +93,66 @@ export class ContentSwitchers extends React.Component<{ } } + private getTabTextLabel(tab: GrapherTabOption): string { + switch (tab) { + case GrapherTabOption.Table: + return "Table" + case GrapherTabOption.WorldMap: + return "Map" + case GrapherTabOption.LineChart: + return "Line" + case GrapherTabOption.SlopeChart: + return "Slope" + default: + return "Chart" + } + } + + private getTrackingKey(tab: GrapherTabOption): string { + switch (tab) { + case GrapherTabOption.Table: + return "chart_click_table" + case GrapherTabOption.WorldMap: + return "chart_click_map" + case GrapherTabOption.LineChart: + return "chart_click_line" + case GrapherTabOption.SlopeChart: + return "chart_click_slope" + default: + return "chart_click_chart" + } + } + + private getAriaLabel(tab: GrapherTabOption): string { + switch (tab) { + case GrapherTabOption.Table: + return "Table" + case GrapherTabOption.WorldMap: + return "Map" + case GrapherTabOption.LineChart: + return "Line chart" + case GrapherTabOption.SlopeChart: + return "Slope chart" + default: + return "Chart" + } + } + @computed private get tabLabels(): TabLabel[] { return this.availableTabs.map((tab) => ({ element: ( - {this.tabIcon(tab)} - {this.showTabLabels && {tab}} + {this.getTabIcon(tab)} + {this.showTabLabels && ( + + {this.getTabTextLabel(tab)} + + )} ), buttonProps: { - "data-track-note": "chart_click_" + tab, - "aria-label": tab, + "data-track-note": this.getTrackingKey(tab), + "aria-label": this.getAriaLabel(tab), }, })) } diff --git a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx index 53cfcae4414..e5db9ab1ab3 100644 --- a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx +++ b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx @@ -64,7 +64,7 @@ export interface SettingsMenuManager hideTableFilterToggle?: boolean // chart state - type: ChartTypeName + type: ChartTypeName // TODO: optional? isRelativeMode?: boolean selection?: SelectionArray | EntityName[] canChangeAddOrHighlightEntities?: boolean diff --git a/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx b/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx index 0ae2db8b681..9ac495943a1 100644 --- a/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx +++ b/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx @@ -2,7 +2,8 @@ import React from "react" import { computed } from "mobx" import { observer } from "mobx-react" -import { Bounds, DEFAULT_BOUNDS, GrapherTabOption } from "@ourworldindata/utils" +import { Bounds, DEFAULT_BOUNDS } from "@ourworldindata/utils" +import { GrapherTabOption } from "@ourworldindata/types" import { ContentSwitchers, ContentSwitchersManager } from "../ContentSwitchers" import { diff --git a/packages/@ourworldindata/grapher/src/controls/settings/AbsRelToggle.tsx b/packages/@ourworldindata/grapher/src/controls/settings/AbsRelToggle.tsx index 7db655a3e87..7f942188266 100644 --- a/packages/@ourworldindata/grapher/src/controls/settings/AbsRelToggle.tsx +++ b/packages/@ourworldindata/grapher/src/controls/settings/AbsRelToggle.tsx @@ -9,7 +9,7 @@ const { LineChart, ScatterPlot } = ChartTypeName export interface AbsRelToggleManager { stackMode?: StackMode relativeToggleLabel?: string - type: ChartTypeName + type: ChartTypeName // TODO: optional? } @observer diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.jsdom.test.ts b/packages/@ourworldindata/grapher/src/core/Grapher.jsdom.test.ts index c8ad4c18e69..d13069e5b3e 100755 --- a/packages/@ourworldindata/grapher/src/core/Grapher.jsdom.test.ts +++ b/packages/@ourworldindata/grapher/src/core/Grapher.jsdom.test.ts @@ -98,12 +98,15 @@ it("a bad chart type does not crash grapher", () => { }) it("does not preserve defaults in the object (except for the schema)", () => { - expect(new Grapher({ tab: GrapherTabOption.chart }).toObject()).toEqual({ - $schema: defaultGrapherConfig.$schema, + // TODO + expect(new Grapher({ tab: GrapherTabOption.LineChart }).toObject()).toEqual( + { + $schema: defaultGrapherConfig.$schema, - // TODO: ideally, selectedEntityNames is not serialised for an empty object - selectedEntityNames: [], - }) + // TODO: ideally, selectedEntityNames is not serialised for an empty object + selectedEntityNames: [], + } + ) }) const unit = "% of children under 5" @@ -211,24 +214,27 @@ it("can generate a url with country selection even if there is no entity code", describe("hasTimeline", () => { it("charts with timeline", () => { const grapher = new Grapher(legacyConfig) - grapher.type = ChartTypeName.LineChart + grapher.availableTabs = [GrapherTabOption.LineChart] expect(grapher.hasTimeline).toBeTruthy() - grapher.type = ChartTypeName.SlopeChart + grapher.availableTabs = [GrapherTabOption.SlopeChart] expect(grapher.hasTimeline).toBeTruthy() - grapher.type = ChartTypeName.StackedArea + grapher.availableTabs = [GrapherTabOption.StackedArea] expect(grapher.hasTimeline).toBeTruthy() - grapher.type = ChartTypeName.StackedBar + grapher.availableTabs = [GrapherTabOption.StackedBar] expect(grapher.hasTimeline).toBeTruthy() - grapher.type = ChartTypeName.DiscreteBar + grapher.availableTabs = [GrapherTabOption.DiscreteBar] expect(grapher.hasTimeline).toBeTruthy() }) it("map tab has timeline even if chart doesn't", () => { const grapher = new Grapher(legacyConfig) grapher.hideTimeline = true - grapher.type = ChartTypeName.LineChart + grapher.availableTabs = [ + GrapherTabOption.WorldMap, + GrapherTabOption.LineChart, + ] expect(grapher.hasTimeline).toBeFalsy() - grapher.tab = GrapherTabOption.map + grapher.tab = GrapherTabOption.WorldMap expect(grapher.hasTimeline).toBeTruthy() grapher.map.hideTimeline = true expect(grapher.hasTimeline).toBeFalsy() @@ -482,7 +488,7 @@ describe("urls", () => { isPublished: true, slug: "foo", bakedGrapherURL: "/grapher", - tab: GrapherTabOption.map, + tab: GrapherTabOption.WorldMap, }) expect(grapher.embedUrl).toEqual("/grapher/foo?tab=map") }) @@ -834,7 +840,7 @@ describe("year parameter (applies to map only)", () => { }) it(`encode ${test.name}`, () => { const params = toQueryParams({ - tab: GrapherTabOption.map, + tab: GrapherTabOption.WorldMap, map: { time: test.param }, }) expect(params.time).toEqual(test.query) @@ -900,7 +906,7 @@ describe("year parameter (applies to map only)", () => { it(`encode ${test.name}`, () => { const grapher = getGrapher() grapher.updateFromObject({ - tab: GrapherTabOption.map, + tab: GrapherTabOption.WorldMap, map: { time: test.param }, }) const params = grapher.changedParams @@ -955,9 +961,9 @@ it("considers map tolerance before using column tolerance", () => { const grapher = new Grapher({ table, - type: ChartTypeName.WorldMap, + availableTabs: [GrapherTabOption.Table, GrapherTabOption.WorldMap], ySlugs: "gdp", - tab: GrapherTabOption.map, + tab: GrapherTabOption.WorldMap, map: new MapConfig({ timeTolerance: 1, columnSlug: "gdp", time: 2002 }), }) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.stories.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.stories.tsx index a5d3b43b6bc..903929204d0 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.stories.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.stories.tsx @@ -90,15 +90,15 @@ export const StackedArea = (): React.ReactElement => { export const MapFirst = (): React.ReactElement => { const model = { ...basics, - tab: GrapherTabOption.map, + tab: GrapherTabOption.WorldMap, } return } export const BlankGrapher = (): React.ReactElement => { const model = { - type: ChartTypeName.WorldMap, - tab: GrapherTabOption.map, + availableTabs: [GrapherTabOption.Table, GrapherTabOption.WorldMap], + tab: GrapherTabOption.WorldMap, table: BlankOwidTable(), hasMapTab: true, } diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 20c99b3a5bc..63eee3dc1d3 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -1,20 +1,12 @@ import React from "react" import ReactDOMServer from "react-dom/server.js" -import { - observable, - computed, - action, - autorun, - runInAction, - reaction, -} from "mobx" +import { observable, computed, action, autorun, reaction } from "mobx" import { bind } from "decko" import { uniqWith, isEqual, uniq, slugify, - identity, lowerCaseFirstLetterUnlessAbbreviation, isMobile, next, @@ -350,7 +342,6 @@ export class Grapher SlopeChartManager { @observable.ref $schema = defaultGrapherConfig.$schema - @observable.ref type = ChartTypeName.LineChart @observable.ref id?: number = undefined @observable.ref version = 1 @observable.ref slug?: string = undefined @@ -364,6 +355,9 @@ export class Grapher @observable.ref internalNotes?: string = undefined @observable.ref originUrl?: string = undefined + @observable availableTabs?: GrapherTabOption[] = [GrapherTabOption.Table] + @observable.ref tab?: GrapherTabOption + @observable hideAnnotationFieldsInTitle?: AnnotationFieldsInTitle = undefined @observable.ref minTime?: TimeBound = undefined @@ -384,9 +378,6 @@ export class Grapher @observable.ref hideScatterLabels?: boolean = undefined @observable.ref zoomToSelection?: boolean = undefined @observable.ref showYearLabels?: boolean = undefined // Always show year in labels for bar charts - @observable.ref hasChartTab = true - @observable.ref hasMapTab = false - @observable.ref tab = GrapherTabOption.chart @observable.ref isPublished?: boolean = undefined @observable.ref baseColorScheme?: ColorSchemeName = undefined @observable.ref invertColorScheme?: boolean = undefined @@ -603,15 +594,31 @@ export class Grapher this.setDimensionsFromConfigs(obj.dimensions) } + private mapQueryParamToGrapherTab( + tab: string + ): GrapherTabOption | undefined { + switch (tab) { + case "table": + return GrapherTabOption.Table + case "map": + return GrapherTabOption.WorldMap + case "line": + return GrapherTabOption.LineChart + case "slope": + return GrapherTabOption.SlopeChart + case "chart": + return this.mainChartType as unknown as GrapherTabOption + default: + return undefined + } + } + @action.bound populateFromQueryParams(params: GrapherQueryParams): void { // Set tab if specified - const tab = params.tab - if (tab) { - if (this.availableTabs.includes(tab as any)) { - this.tab = tab as GrapherTabOption - } else { - console.error("Unexpected tab: " + tab) - } + if (params.tab) { + const tab = this.mapQueryParamToGrapherTab(params.tab) + if (tab) this.tab = tab + else console.error("Unexpected tab: " + params.tab) } // Set overlay if specified @@ -693,16 +700,47 @@ export class Grapher ) as TimeBounds } + // TODO + @computed get mainChartType(): ChartTypeName { + return (this.availableTabs?.find( + (tab) => + tab !== GrapherTabOption.Table && + tab !== GrapherTabOption.WorldMap + ) ?? GrapherTabOption.LineChart) as unknown as ChartTypeName + } + + // TODO: should be CURRENT chart type + @computed get type(): ChartTypeName { + if (this.isOnChartTab) return this.tab as unknown as ChartTypeName + return this.mainChartType + } + + @computed get hasChartTab(): boolean { + // TODO: do differtnly + return ( + (this.availableTabs?.filter( + (tab) => + tab !== GrapherTabOption.Table && + tab !== GrapherTabOption.WorldMap + ).length || 0) > 0 + ) + } + + @computed get hasMapTab(): boolean { + const { availableTabs = [] } = this + return availableTabs.includes(GrapherTabOption.WorldMap) + } + @computed get isOnChartTab(): boolean { - return this.tab === GrapherTabOption.chart + return Object.values(ChartTypeName).includes(this.tab as any) } @computed get isOnMapTab(): boolean { - return this.tab === GrapherTabOption.map + return this.tab === GrapherTabOption.WorldMap } @computed get isOnTableTab(): boolean { - return this.tab === GrapherTabOption.table + return this.tab === GrapherTabOption.Table } @computed get isOnChartOrMapTab(): boolean { @@ -1274,11 +1312,12 @@ export class Grapher () => this.downloadLegacyDataFromOwidVariableIds() ) ) + // TODO: const disposers = [ - autorun(() => { - if (!this.availableTabs.includes(this.tab)) - runInAction(() => (this.tab = this.availableTabs[0])) - }), + // autorun(() => { + // if (!this.availableTabsIncludingTable.includes(this.tab)) + // runInAction(() => (this.tab = this.availableTabsIncludingTable[0])) + // }), autorun(() => { const validDimensions = this.validDimensions if (!isEqual(this.dimensions, validDimensions)) @@ -1479,14 +1518,6 @@ export class Grapher }) } - @computed get availableTabs(): GrapherTabOption[] { - return [ - this.hasTableTab && GrapherTabOption.table, - this.hasMapTab && GrapherTabOption.map, - this.hasChartTab && GrapherTabOption.chart, - ].filter(identity) as GrapherTabOption[] - } - @computed get currentSubtitle(): string { const subtitle = this.subtitle if (subtitle !== undefined) return subtitle @@ -1505,7 +1536,7 @@ export class Grapher return !!( !this.forceHideAnnotationFieldsInTitle?.entity && - this.tab === GrapherTabOption.chart && + this.isOnChartTab && (seriesStrategy !== SeriesStrategy.entity || !this.showLegend) && selectedEntityNames.length === 1 && (showEntityAnnotation || @@ -1577,23 +1608,21 @@ export class Grapher // we don't have more than one distinct time point in our data, so it doesn't make sense to show a timeline if (this.times.length <= 1) return false + // TODO: tab might be undefined, currentTab? switch (this.tab) { // the map tab has its own `hideTimeline` option - case GrapherTabOption.map: + case GrapherTabOption.WorldMap: return !this.map.hideTimeline - // use the chart-level `hideTimeline` option - case GrapherTabOption.chart: - return !this.hideTimeline - // use the chart-level `hideTimeline` option for the table, with some exceptions - case GrapherTabOption.table: + case GrapherTabOption.Table: // always show the timeline for charts that plot time on the x-axis if (this.hasTimeDimension) return true return !this.hideTimeline + // use the chart-level `hideTimeline` option default: - return false + return !this.hideTimeline } } @@ -2370,7 +2399,8 @@ export class Grapher } @action.bound private toggleTabCommand(): void { - this.tab = next(this.availableTabs, this.tab) + // TODO + // this.tab = next(this.availableTabsIncludingTable, this.tab) } @action.bound private togglePlayingCommand(): void { @@ -3131,9 +3161,25 @@ export class Grapher debounceMode = false + // TODO + private tabToQueryParam(tab: GrapherTabOption): string { + switch (tab) { + case GrapherTabOption.Table: + return "table" + case GrapherTabOption.WorldMap: + return "map" + case GrapherTabOption.LineChart: + return "line" + case GrapherTabOption.SlopeChart: + return "slope" + default: + return "chart" + } + } + @computed.struct get allParams(): GrapherQueryParams { const params: GrapherQueryParams = {} - params.tab = this.tab + params.tab = this.tab ? this.tabToQueryParam(this.tab) : undefined params.xScale = this.xAxis.scaleType params.yScale = this.yAxis.scaleType params.stackMode = this.stackMode diff --git a/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts b/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts index dad8561d05c..c65dd877e4e 100644 --- a/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts +++ b/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts @@ -37,6 +37,7 @@ import { OwidChartDimensionInterface, OwidVariableType, memoize, + getMainChartTypeFromConfig, } from "@ourworldindata/utils" import { isContinentsVariableId } from "./GrapherConstants" @@ -198,9 +199,10 @@ export const legacyToOwidTableAndDimensions = ( // We do this by dropping the column. We interpolate before which adds an originalTime // column which can be used to recover the time. const targetTime = dimension?.targetYear + const chartType = getMainChartTypeFromConfig(grapherConfig) if ( - (grapherConfig.type === ChartTypeName.ScatterPlot || - grapherConfig.type === ChartTypeName.Marimekko) && + (chartType === ChartTypeName.ScatterPlot || + chartType === ChartTypeName.Marimekko) && isNumber(targetTime) ) { variableTable = variableTable diff --git a/packages/@ourworldindata/grapher/src/dataTable/DataTable.jsdom.test.tsx b/packages/@ourworldindata/grapher/src/dataTable/DataTable.jsdom.test.tsx index 0825a56dece..94383bce657 100755 --- a/packages/@ourworldindata/grapher/src/dataTable/DataTable.jsdom.test.tsx +++ b/packages/@ourworldindata/grapher/src/dataTable/DataTable.jsdom.test.tsx @@ -70,8 +70,8 @@ describe("when you select a range of years", () => { let view: ReactWrapper beforeAll(() => { const grapher = childMortalityGrapher({ - type: ChartTypeName.LineChart, - tab: GrapherTabOption.table, + availableTabs: [GrapherTabOption.Table, GrapherTabOption.LineChart], + tab: GrapherTabOption.Table, }) grapher.timelineHandleTimeBounds = [1950, 2019] diff --git a/packages/@ourworldindata/grapher/src/dataTable/DataTable.sample.ts b/packages/@ourworldindata/grapher/src/dataTable/DataTable.sample.ts index 5bf81c3a0bc..eae6d20e645 100644 --- a/packages/@ourworldindata/grapher/src/dataTable/DataTable.sample.ts +++ b/packages/@ourworldindata/grapher/src/dataTable/DataTable.sample.ts @@ -38,7 +38,7 @@ export const childMortalityGrapher = ( ] return new Grapher({ hasMapTab: true, - tab: GrapherTabOption.map, + tab: GrapherTabOption.WorldMap, dimensions, ...props, owidDataset: createOwidTestDataset([ @@ -85,7 +85,7 @@ export const GrapherWithIncompleteData = ( }, ] return new Grapher({ - tab: GrapherTabOption.table, + tab: GrapherTabOption.Table, dimensions, ...props, owidDataset: createOwidTestDataset([{ metadata, data }]), @@ -125,7 +125,7 @@ export const GrapherWithAggregates = ( }, ] return new Grapher({ - tab: GrapherTabOption.table, + tab: GrapherTabOption.Table, dimensions, ...props, owidDataset: createOwidTestDataset([ @@ -169,7 +169,7 @@ export const GrapherWithMultipleVariablesAndMultipleYears = ( } return new Grapher({ - tab: GrapherTabOption.table, + tab: GrapherTabOption.Table, dimensions, ...props, owidDataset: createOwidTestDataset([ diff --git a/packages/@ourworldindata/grapher/src/dataTable/DataTable.stories.tsx b/packages/@ourworldindata/grapher/src/dataTable/DataTable.stories.tsx index e0279bfaf88..41dfb90f232 100644 --- a/packages/@ourworldindata/grapher/src/dataTable/DataTable.stories.tsx +++ b/packages/@ourworldindata/grapher/src/dataTable/DataTable.stories.tsx @@ -88,8 +88,8 @@ export const FromLegacy = (): React.ReactElement => { export const FromLegacyWithTimeRange = (): React.ReactElement => { const grapher = childMortalityGrapher({ - type: ChartTypeName.LineChart, - tab: GrapherTabOption.chart, + availableTabs: [GrapherTabOption.Table, GrapherTabOption.LineChart], + tab: GrapherTabOption.LineChart, }) grapher.startHandleTimeBound = 1950 grapher.endHandleTimeBound = 2019 diff --git a/packages/@ourworldindata/grapher/src/mapCharts/MapChart.sample.ts b/packages/@ourworldindata/grapher/src/mapCharts/MapChart.sample.ts index a118a1d6969..946d5d3e83c 100644 --- a/packages/@ourworldindata/grapher/src/mapCharts/MapChart.sample.ts +++ b/packages/@ourworldindata/grapher/src/mapCharts/MapChart.sample.ts @@ -2,9 +2,10 @@ import { DimensionProperty } from "@ourworldindata/utils" import { GrapherProgrammaticInterface } from "../core/Grapher" import { GrapherTabOption } from "@ourworldindata/types" +// TODO: rewrite export const legacyMapGrapher: GrapherProgrammaticInterface = { hasMapTab: true, - tab: GrapherTabOption.map, + tab: GrapherTabOption.WorldMap, map: { timeTolerance: 5, }, diff --git a/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx b/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx index b8d9343a8fc..97561d46dc8 100644 --- a/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx +++ b/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx @@ -289,7 +289,8 @@ export class MapChart if (!ev.shiftKey) { this.selectionArray.setSelectedEntities([entityName]) - this.manager.tab = GrapherTabOption.chart + // TODO + this.manager.tab = GrapherTabOption.LineChart if ( this.manager.isLineChartThatTurnedIntoDiscreteBar && this.manager.hasTimeline diff --git a/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts b/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts index 1243ba4e985..a2f4c89383c 100644 --- a/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts +++ b/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts @@ -15,6 +15,7 @@ export const outdatedSchemaVersions = [ export const defaultGrapherConfig = { $schema: "https://files.ourworldindata.org/schemas/grapher-schema.006.json", + availableTabs: ["Table"], map: { projection: "World", hideTimeline: false, @@ -37,9 +38,7 @@ export const defaultGrapherConfig = { canChangeScaleType: false, facetDomain: "shared", }, - tab: "chart", matchingEntitiesOnly: false, - hasChartTab: true, hideLegend: false, hideLogo: false, timelineMinTime: "earliest", @@ -60,8 +59,6 @@ export const defaultGrapherConfig = { facettingLabelByYVariables: "metric", addCountryMode: "add-country", compareEndPointsOnly: false, - type: "LineChart", - hasMapTab: false, stackMode: "absolute", minTime: "earliest", hideAnnotationFieldsInTitle: { diff --git a/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml b/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml index f906da32f22..7deecf56afd 100644 --- a/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml +++ b/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml @@ -25,6 +25,37 @@ properties: type: integer description: Internal DB id. Useful internally for OWID but not required if just using grapher directly. minimum: 0 + availableTabs: + type: array + description: The tabs that are available for selection. The "Table" tab is always available. + default: ["Table"] + items: + type: string + enum: + - Table + - WorldMap + - LineChart + - ScatterPlot + - StackedArea + - DiscreteBar + - StackedDiscreteBar + - SlopeChart + - StackedBar + - Marimekko + tab: + type: string + description: The tab that is shown initially + enum: + - Table + - WorldMap + - LineChart + - ScatterPlot + - StackedArea + - DiscreteBar + - StackedDiscreteBar + - SlopeChart + - StackedBar + - Marimekko map: type: object description: Configuration of the world map chart @@ -111,22 +142,10 @@ properties: If not provided, a default is chosen based on the chart type. yAxis: $ref: "#/$defs/axis" - tab: - type: string - description: The tab that is shown initially - default: chart - enum: - - chart - - map - - table matchingEntitiesOnly: type: boolean default: false description: Exclude entities that do not belong in any color group - hasChartTab: - type: boolean - default: true - description: Whether to show the (non-map) chart tab hideLegend: type: boolean default: false @@ -369,23 +388,6 @@ properties: title: type: string description: Big title text of the chart - type: - type: string - description: Which type of chart should be shown (hasMapChart can be used to always also show a map chart) - default: LineChart - enum: - - LineChart - - ScatterPlot - - StackedArea - - DiscreteBar - - StackedDiscreteBar - - SlopeChart - - StackedBar - - Marimekko - hasMapTab: - type: boolean - default: false - description: Indicates if the map tab should be shown stackMode: type: string description: Stack mode. Only absolute and relative are actively used. diff --git a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts index 5954b1e985c..53f457d60d1 100644 --- a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts +++ b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts @@ -173,9 +173,16 @@ export type SeriesName = string export type SeriesColorMap = Map export enum GrapherTabOption { - chart = "chart", - map = "map", - table = "table", + Table = "Table", + WorldMap = "WorldMap", + LineChart = "LineChart", + ScatterPlot = "ScatterPlot", + StackedArea = "StackedArea", + DiscreteBar = "DiscreteBar", + StackedDiscreteBar = "StackedDiscreteBar", + SlopeChart = "SlopeChart", + StackedBar = "StackedBar", + Marimekko = "Marimekko", } export interface RelatedQuestionsConfig { @@ -528,7 +535,6 @@ export interface MapConfigInterface { // under the same rendering conditions it ought to remain visually identical export interface GrapherInterface extends SortConfig { $schema?: string - type?: ChartTypeName id?: number version?: number slug?: string @@ -536,6 +542,8 @@ export interface GrapherInterface extends SortConfig { subtitle?: string sourceDesc?: string note?: string + availableTabs?: GrapherTabOption[] + tab?: GrapherTabOption hideAnnotationFieldsInTitle?: AnnotationFieldsInTitle minTime?: TimeBound | TimeBoundValueStr maxTime?: TimeBound | TimeBoundValueStr @@ -556,9 +564,6 @@ export interface GrapherInterface extends SortConfig { hideTimeline?: boolean zoomToSelection?: boolean showYearLabels?: boolean // Always show year in labels for bar charts - hasChartTab?: boolean - hasMapTab?: boolean - tab?: GrapherTabOption relatedQuestion?: RelatedQuestionsConfig details?: DetailDictionary internalNotes?: string @@ -647,10 +652,10 @@ export const GRAPHER_QUERY_PARAM_KEYS: (keyof LegacyGrapherQueryParams)[] = [ // Another approach we may want to try is this: https://github.com/mobxjs/serializr export const grapherKeysToSerialize = [ "$schema", - "type", "id", "version", "slug", + "availableTabs", "title", "subtitle", "sourceDesc", @@ -673,8 +678,6 @@ export const grapherKeysToSerialize = [ "hideTimeline", "zoomToSelection", "showYearLabels", - "hasChartTab", - "hasMapTab", "tab", "internalNotes", "variantName", diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts index f62bdc5bb40..e2a05169e5c 100644 --- a/packages/@ourworldindata/utils/src/Util.ts +++ b/packages/@ourworldindata/utils/src/Util.ts @@ -177,6 +177,7 @@ import { GrapherInterface, ChartTypeName, DimensionProperty, + GrapherTabOption, } from "@ourworldindata/types" import { PointVector } from "./PointVector.js" import React from "react" @@ -1963,12 +1964,38 @@ export function traverseObjects>( return result } +export function hasChartTabFromConfig(config: GrapherInterface): boolean { + // TODO: what should happen for empty avail tabs? + const { availableTabs = [] } = config + const chartTypes = availableTabs.filter( + (tab) => + ![GrapherTabOption.Table, GrapherTabOption.WorldMap].includes(tab) + ) + return chartTypes.length > 0 +} + +export function getMainChartTypeFromConfig( + config: GrapherInterface +): ChartTypeName { + const { availableTabs = [] } = config + + const chartTypes = availableTabs.filter( + (tab) => + tab !== GrapherTabOption.Table && tab !== GrapherTabOption.WorldMap + ) + + // TODO: type + return chartTypes[0] as unknown as ChartTypeName +} + export function getParentVariableIdFromChartConfig( config: GrapherInterface ): number | undefined { - const { type = ChartTypeName.LineChart, dimensions } = config + const { dimensions } = config + + const chartType = getMainChartTypeFromConfig(config) + if (chartType === ChartTypeName.ScatterPlot) return undefined - if (type === ChartTypeName.ScatterPlot) return undefined if (!dimensions) return undefined const yVariableIds = dimensions diff --git a/packages/@ourworldindata/utils/src/grapherConfigUtils.test.ts b/packages/@ourworldindata/utils/src/grapherConfigUtils.test.ts index 882b0d74326..6dbef8ef821 100644 --- a/packages/@ourworldindata/utils/src/grapherConfigUtils.test.ts +++ b/packages/@ourworldindata/utils/src/grapherConfigUtils.test.ts @@ -243,14 +243,14 @@ describe(diffGrapherConfigs, () => { it("drops redundant entries", () => { expect( diffGrapherConfigs( - { tab: GrapherTabOption.map }, - { tab: GrapherTabOption.map } + { tab: GrapherTabOption.WorldMap }, + { tab: GrapherTabOption.WorldMap } ) ).toEqual({}) expect( diffGrapherConfigs( - { tab: GrapherTabOption.chart, title: "Chart" }, - { tab: GrapherTabOption.chart, title: "Reference chart" } + { tab: GrapherTabOption.LineChart, title: "Chart" }, + { tab: GrapherTabOption.LineChart, title: "Reference chart" } ) ).toEqual({ title: "Chart" }) }) @@ -260,7 +260,7 @@ describe(diffGrapherConfigs, () => { diffGrapherConfigs( { title: "Chart", - tab: GrapherTabOption.chart, + tab: GrapherTabOption.LineChart, map: { projection: MapProjectionName.World, hideTimeline: true, @@ -268,7 +268,7 @@ describe(diffGrapherConfigs, () => { }, { title: "Reference chart", - tab: GrapherTabOption.chart, + tab: GrapherTabOption.LineChart, map: { projection: MapProjectionName.World, hideTimeline: false, @@ -279,14 +279,14 @@ describe(diffGrapherConfigs, () => { expect( diffGrapherConfigs( { - tab: GrapherTabOption.chart, + tab: GrapherTabOption.LineChart, map: { projection: MapProjectionName.World, hideTimeline: true, }, }, { - tab: GrapherTabOption.chart, + tab: GrapherTabOption.LineChart, map: { projection: MapProjectionName.World, hideTimeline: true, @@ -300,11 +300,11 @@ describe(diffGrapherConfigs, () => { expect( diffGrapherConfigs( { - tab: GrapherTabOption.chart, + tab: GrapherTabOption.LineChart, title: "Chart", subtitle: undefined, }, - { tab: GrapherTabOption.chart, title: "Reference chart" } + { tab: GrapherTabOption.LineChart, title: "Reference chart" } ) ).toEqual({ title: "Chart" }) }) @@ -361,12 +361,12 @@ describe(diffGrapherConfigs, () => { it("is idempotent", () => { const config: GrapherInterface = { - tab: GrapherTabOption.chart, + tab: GrapherTabOption.LineChart, title: "Chart", subtitle: undefined, } const reference: GrapherInterface = { - tab: GrapherTabOption.chart, + tab: GrapherTabOption.LineChart, title: "Reference chart", } const diffedOnce = diffGrapherConfigs(config, reference) @@ -378,12 +378,12 @@ describe(diffGrapherConfigs, () => { describe("diff+merge", () => { it("are consistent", () => { const config: GrapherInterface = { - tab: GrapherTabOption.chart, + tab: GrapherTabOption.LineChart, title: "Chart", subtitle: "Chart subtitle", } const reference: GrapherInterface = { - tab: GrapherTabOption.chart, + tab: GrapherTabOption.LineChart, title: "Reference chart", } const diffedAndMerged = mergeGrapherConfigs( diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts index eacf00917fa..5f8160309e6 100644 --- a/packages/@ourworldindata/utils/src/index.ts +++ b/packages/@ourworldindata/utils/src/index.ts @@ -123,7 +123,9 @@ export { createTagGraph, formatInlineList, lazy, + getMainChartTypeFromConfig, getParentVariableIdFromChartConfig, + hasChartTabFromConfig, } from "./Util.js" export { diff --git a/site/gdocs/components/Chart.tsx b/site/gdocs/components/Chart.tsx index dbc57a3f973..2efeb7dc42f 100644 --- a/site/gdocs/components/Chart.tsx +++ b/site/gdocs/components/Chart.tsx @@ -175,6 +175,8 @@ const mapKeywordToGrapherConfig = ( // tabs + // TODO: migrate to available tabs, keep hasTableTab + case ChartTabKeyword.chart: return { hasChartTab: true } diff --git a/site/gdocs/components/KeyIndicatorCollection.tsx b/site/gdocs/components/KeyIndicatorCollection.tsx index 8539a5a70b3..1277857bd3a 100644 --- a/site/gdocs/components/KeyIndicatorCollection.tsx +++ b/site/gdocs/components/KeyIndicatorCollection.tsx @@ -25,10 +25,15 @@ import { Button } from "@ourworldindata/components" // keep in sync with $duration in KeyIndicatorCollection.scss const HEIGHT_ANIMATION_DURATION_IN_SECONDS = 0.4 -const tabIconMap: Record = { - [GrapherTabOption.chart]: faChartLine, - [GrapherTabOption.map]: faEarthAmericas, - [GrapherTabOption.table]: faTable, +function getTabIcon(tab: GrapherTabOption): IconDefinition { + switch (tab) { + case GrapherTabOption.Table: + return faTable + case GrapherTabOption.WorldMap: + return faEarthAmericas + default: + return faChartLine + } } export default function KeyIndicatorCollection({ @@ -241,13 +246,15 @@ function KeyIndicatorHeader({ if (!linkedChart) return null if (!linkedIndicator) return null + // TODO: translate old query params to new ones const { queryParams } = Url.fromURL(linkedChart.resolvedUrl) const tabFromQueryParams = queryParams.tab && isValidGrapherTab(queryParams.tab) ? queryParams.tab : undefined + // TODO const activeTab = - tabFromQueryParams || linkedChart.tab || GrapherTabOption.chart + tabFromQueryParams || linkedChart.tab || GrapherTabOption.LineChart // || GrapherTabOption.chart const source = block.source || linkedIndicator.attributionShort @@ -255,7 +262,7 @@ function KeyIndicatorHeader({
diff --git a/site/multiembedder/MultiEmbedder.tsx b/site/multiembedder/MultiEmbedder.tsx index 7e1639f85e9..4c44eedefed 100644 --- a/site/multiembedder/MultiEmbedder.tsx +++ b/site/multiembedder/MultiEmbedder.tsx @@ -210,18 +210,19 @@ class MultiEmbedder { : {} // make sure the tab of the active pane is visible - if (figureConfigAttr) { - const activeTab = - queryParams.tab || - grapherPageConfig.tab || - GrapherTabOption.chart - if (activeTab === GrapherTabOption.chart) - localConfig.hasChartTab = true - if (activeTab === GrapherTabOption.map) - localConfig.hasMapTab = true - if (activeTab === GrapherTabOption.table) - localConfig.hasTableTab = true - } + // TODO: translate query params... construct config + // if (figureConfigAttr) { + // const activeTab = + // queryParams.tab || + // grapherPageConfig.tab || + // GrapherTabOption.chart + // if (activeTab === GrapherTabOption.chart) + // localConfig.hasChartTab = true + // if (activeTab === GrapherTabOption.map) + // localConfig.hasMapTab = true + // if (activeTab === GrapherTabOption.table) + // localConfig.hasTableTab = true + // } const config = merge( {}, // merge mutates the first argument