- (manager.tab = this.availableTabs[index])
- }
+ labels={this.tabLabels}
+ activeIndex={this.activeTabIndex}
+ setActiveIndex={this.setTab}
/>
)
}
}
+
+function ContentSwitcherTab({
+ tab,
+ showLabel,
+ hasMultipleChartTypes,
+ isLineChartThatTurnedIntoDiscreteBar,
+}: {
+ tab: GrapherTabName
+ showLabel?: boolean
+ hasMultipleChartTypes?: boolean
+ isLineChartThatTurnedIntoDiscreteBar?: boolean
+}): React.ReactElement {
+ return (
+
+
+ {showLabel && (
+
+ {makeTabLabelText(tab, {
+ isLineChartThatTurnedIntoDiscreteBar,
+ hasMultipleChartTypes,
+ })}
+
+ )}
+
+ )
+}
+
+function TabIcon({
+ tab,
+ isLineChartThatTurnedIntoDiscreteBar,
+}: {
+ tab: GrapherTabName
+ isLineChartThatTurnedIntoDiscreteBar?: boolean
+}): React.ReactElement {
+ switch (tab) {
+ case GrapherTabName.Table:
+ return
+ case GrapherTabName.WorldMap:
+ return
+ default:
+ const chartIcon = isLineChartThatTurnedIntoDiscreteBar
+ ? chartIcons[ChartTypeName.DiscreteBar]
+ : chartIcons[tab as unknown as ChartTypeName]
+ return chartIcon
+ }
+}
+
+function makeTabLabelText(
+ tab: GrapherTabName,
+ options: {
+ isLineChartThatTurnedIntoDiscreteBar?: boolean
+ hasMultipleChartTypes?: boolean
+ }
+): string {
+ if (tab === GrapherTabName.Table) return "Table"
+ if (tab === GrapherTabName.WorldMap) return "Map"
+ if (!options.hasMultipleChartTypes) return "Chart"
+
+ switch (tab) {
+ case GrapherTabName.LineChart:
+ return options.isLineChartThatTurnedIntoDiscreteBar ? "Bar" : "Line"
+ case GrapherTabName.SlopeChart:
+ return "Slope"
+
+ // chart type labels are preliminary
+ case GrapherTabName.ScatterPlot:
+ return "Scatter"
+ case GrapherTabName.StackedArea:
+ return "Stacked area"
+ case GrapherTabName.StackedBar:
+ return "Stacked bar"
+ case GrapherTabName.DiscreteBar:
+ return "Bar"
+ case GrapherTabName.StackedDiscreteBar:
+ return "Stacked bar"
+ case GrapherTabName.Marimekko:
+ return "Marimekko"
+ default:
+ return "Chart"
+ }
+}
diff --git a/packages/@ourworldindata/grapher/src/controls/Controls.scss b/packages/@ourworldindata/grapher/src/controls/Controls.scss
index 87031973b05..b58d50d26d9 100644
--- a/packages/@ourworldindata/grapher/src/controls/Controls.scss
+++ b/packages/@ourworldindata/grapher/src/controls/Controls.scss
@@ -85,7 +85,7 @@ nav.controlsRow .chart-controls {
}
}
-@container grapher (max-width:550px) {
+@container grapher (max-width:575px) {
// collapse the Settings toggle down to just an icon on small screens
nav.controlsRow .settings-menu button.menu-toggle {
min-height: $control-row-height;
@@ -105,7 +105,7 @@ nav.controlsRow .chart-controls {
}
}
-@container grapher (max-width:475px) {
+@container grapher (max-width:525px) {
// hide the entity name in the Edit/Select/Switch button
nav.controlsRow .entity-selection-menu button.menu-toggle {
label span {
diff --git a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx
index daedbd24493..200bafc0967 100644
--- a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx
+++ b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx
@@ -67,7 +67,7 @@ export interface SettingsMenuManager
hideTableFilterToggle?: boolean
// chart state
- type: ChartTypeName
+ activeChartType?: ChartTypeName
isRelativeMode?: boolean
selection?: SelectionArray | EntityName[]
canChangeAddOrHighlightEntities?: boolean
@@ -104,6 +104,10 @@ export class SettingsMenu extends React.Component<{
return test.showSettingsMenuToggle
}
+ @computed get chartType(): ChartTypeName {
+ return this.manager.activeChartType ?? ChartTypeName.LineChart
+ }
+
@computed get maxWidth(): number {
return this.props.maxWidth ?? DEFAULT_BOUNDS.width
}
@@ -111,7 +115,7 @@ export class SettingsMenu extends React.Component<{
@computed get showYScaleToggle(): boolean | undefined {
if (this.manager.hideYScaleToggle) return false
if (this.manager.isRelativeMode) return false
- if ([StackedArea, StackedBar].includes(this.manager.type)) return false // We currently do not have these charts with log scale
+ if ([StackedArea, StackedBar].includes(this.chartType)) return false // We currently do not have these charts with log scale
return this.manager.yAxis.canChangeScaleType
}
@@ -126,15 +130,15 @@ export class SettingsMenu extends React.Component<{
return (
!this.manager.hideFacetYDomainToggle &&
this.manager.facetStrategy !== FacetStrategy.none &&
- this.manager.type !== StackedDiscreteBar
+ this.chartType !== StackedDiscreteBar
)
}
@computed get showZoomToggle(): boolean {
- const { type, hideZoomToggle } = this.manager
+ const { hideZoomToggle } = this.manager
return (
!hideZoomToggle &&
- type === ScatterPlot &&
+ this.chartType === ScatterPlot &&
this.selectionArray.hasSelection
)
}
@@ -142,16 +146,16 @@ export class SettingsMenu extends React.Component<{
@computed get showNoDataAreaToggle(): boolean {
return (
!this.manager.hideNoDataAreaToggle &&
- this.manager.type === Marimekko &&
+ this.chartType === Marimekko &&
this.manager.xColumnSlug !== undefined
)
}
@computed get showAbsRelToggle(): boolean {
- const { type, canToggleRelativeMode, hasTimeline, xOverrideTime } =
+ const { canToggleRelativeMode, hasTimeline, xOverrideTime } =
this.manager
if (!canToggleRelativeMode) return false
- if (type === ScatterPlot)
+ if (this.chartType === ScatterPlot)
return xOverrideTime === undefined && !!hasTimeline
return [
StackedArea,
@@ -160,7 +164,7 @@ export class SettingsMenu extends React.Component<{
ScatterPlot,
LineChart,
Marimekko,
- ].includes(type)
+ ].includes(this.chartType)
}
@computed get showFacetControl(): boolean {
@@ -169,7 +173,6 @@ export class SettingsMenu extends React.Component<{
availableFacetStrategies,
hideFacetControl,
isOnTableTab,
- type,
} = this.manager
// if there's no choice to be made, don't display a lone button
@@ -184,7 +187,7 @@ export class SettingsMenu extends React.Component<{
StackedBar,
StackedDiscreteBar,
LineChart,
- ].includes(type)
+ ].includes(this.chartType)
const hasProjection = filledDimensions.some(
(dim) => dim.display.isProjection
@@ -261,9 +264,8 @@ export class SettingsMenu extends React.Component<{
return this.props.manager
}
- @computed get chartType(): string {
- const { type } = this.manager
- return type.replace(/([A-Z])/g, " $1")
+ @computed get chartTypeLabel(): string {
+ return this.chartType.replace(/([A-Z])/g, " $1")
}
@computed get selectionArray(): SelectionArray {
@@ -390,10 +392,10 @@ export class SettingsMenu extends React.Component<{
}
@computed get menuContents(): JSX.Element {
- const { manager, chartType } = this
+ const { manager, chartTypeLabel } = this
const { isOnTableTab } = manager
- const menuTitle = `${isOnTableTab ? "Table" : chartType} settings`
+ const menuTitle = `${isOnTableTab ? "Table" : chartTypeLabel} settings`
return (
diff --git a/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx b/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx
index a2c5f9d38c2..a01b56ab0b7 100644
--- a/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx
+++ b/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx
@@ -2,7 +2,7 @@ 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 { ContentSwitchers, ContentSwitchersManager } from "../ContentSwitchers"
import {
@@ -25,7 +25,6 @@ export interface ControlsRowManager
MapProjectionMenuManager,
SettingsMenuManager {
sidePanelBounds?: Bounds
- availableTabs?: GrapherTabOption[]
showEntitySelectionToggle?: boolean
}
diff --git a/packages/@ourworldindata/grapher/src/controls/settings/AbsRelToggle.tsx b/packages/@ourworldindata/grapher/src/controls/settings/AbsRelToggle.tsx
index 7db655a3e87..fd9e6c17a55 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
+ activeChartType?: ChartTypeName
}
@observer
@@ -31,10 +31,10 @@ export class AbsRelToggle extends React.Component<{
}
@computed get tooltip(): string {
- const { type } = this.manager
- return type === ScatterPlot
+ const { activeChartType } = this.manager
+ return activeChartType === ScatterPlot
? "Show the percentage change per year over the the selected time range."
- : type === LineChart
+ : activeChartType === LineChart
? "Show proportional changes over time or actual values in their original units."
: "Show values as their share of the total or as actual values in their original units."
}
diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.jsdom.test.ts b/packages/@ourworldindata/grapher/src/core/Grapher.jsdom.test.ts
index 36e05f38c92..fd221f0eacd 100755
--- a/packages/@ourworldindata/grapher/src/core/Grapher.jsdom.test.ts
+++ b/packages/@ourworldindata/grapher/src/core/Grapher.jsdom.test.ts
@@ -9,6 +9,7 @@ import {
GrapherQueryParams,
LegacyGrapherInterface,
LegacyGrapherQueryParams,
+ GrapherTabName,
} from "@ourworldindata/types"
import {
TimeBoundValue,
@@ -71,7 +72,7 @@ it("can get dimension slots", () => {
const grapher = new Grapher()
expect(grapher.dimensionSlots.length).toBe(2)
- grapher.type = ChartTypeName.ScatterPlot
+ grapher.chartTypes = [ChartTypeName.ScatterPlot]
expect(grapher.dimensionSlots.length).toBe(4)
})
@@ -86,7 +87,7 @@ it("an empty Grapher serializes to an object that includes only the schema", ()
it("a bad chart type does not crash grapher", () => {
const input = {
- type: "fff" as any,
+ chartTypes: ["fff" as any],
}
expect(new Grapher(input).toObject()).toEqual({
...input,
@@ -211,22 +212,22 @@ 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.chartTypes = [ChartTypeName.LineChart]
expect(grapher.hasTimeline).toBeTruthy()
- grapher.type = ChartTypeName.SlopeChart
+ grapher.chartTypes = [ChartTypeName.SlopeChart]
expect(grapher.hasTimeline).toBeTruthy()
- grapher.type = ChartTypeName.StackedArea
+ grapher.chartTypes = [ChartTypeName.StackedArea]
expect(grapher.hasTimeline).toBeTruthy()
- grapher.type = ChartTypeName.StackedBar
+ grapher.chartTypes = [ChartTypeName.StackedBar]
expect(grapher.hasTimeline).toBeTruthy()
- grapher.type = ChartTypeName.DiscreteBar
+ grapher.chartTypes = [ChartTypeName.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.chartTypes = [ChartTypeName.LineChart]
expect(grapher.hasTimeline).toBeFalsy()
grapher.tab = GrapherTabOption.map
expect(grapher.hasTimeline).toBeTruthy()
@@ -379,7 +380,7 @@ describe("authors can use maxTime", () => {
const table = SynthesizeGDPTable({ timeRange: [2000, 2010] })
const grapher = new Grapher({
table,
- type: ChartTypeName.DiscreteBar,
+ chartTypes: [ChartTypeName.DiscreteBar],
selectedEntityNames: table.availableEntityNames,
maxTime: 2005,
ySlugs: "GDP",
@@ -509,6 +510,73 @@ describe("urls", () => {
grapher.populateFromQueryParams(url.queryParams)
expect(grapher.selection.selectedEntityNames).toEqual(["usa", "canada"])
})
+
+ it("parses tab=table correctly", () => {
+ const grapher = new Grapher()
+ grapher.populateFromQueryParams({ tab: "table" })
+ expect(grapher.activeTab).toEqual(GrapherTabName.Table)
+ })
+
+ it("parses tab=map correctly", () => {
+ const grapher = new Grapher()
+ grapher.populateFromQueryParams({ tab: "map" })
+ expect(grapher.activeTab).toEqual(GrapherTabName.WorldMap)
+ })
+
+ it("parses tab=chart correctly", () => {
+ const grapher = new Grapher({ chartTypes: [ChartTypeName.ScatterPlot] })
+ grapher.populateFromQueryParams({ tab: "chart" })
+ expect(grapher.activeTab).toEqual(GrapherTabName.ScatterPlot)
+ })
+
+ it("parses tab=line and tab=slope correctly", () => {
+ const grapher = new Grapher({
+ chartTypes: [ChartTypeName.LineChart, ChartTypeName.SlopeChart],
+ })
+ grapher.populateFromQueryParams({ tab: "line" })
+ expect(grapher.activeTab).toEqual(GrapherTabName.LineChart)
+ grapher.populateFromQueryParams({ tab: "slope" })
+ expect(grapher.activeTab).toEqual(GrapherTabName.SlopeChart)
+ })
+
+ it("switches to the first chart tab if the given chart isn't available", () => {
+ const grapher = new Grapher({
+ chartTypes: [ChartTypeName.LineChart, ChartTypeName.SlopeChart],
+ })
+ grapher.populateFromQueryParams({ tab: "bar" })
+ expect(grapher.activeTab).toEqual(GrapherTabName.LineChart)
+ })
+
+ it("switches to the map tab if no chart is available", () => {
+ const grapher = new Grapher({ chartTypes: [], hasMapTab: true })
+ grapher.populateFromQueryParams({ tab: "line" })
+ expect(grapher.activeTab).toEqual(GrapherTabName.WorldMap)
+ })
+
+ it("switches to the table tab if it's the only tab available", () => {
+ const grapher = new Grapher({ chartTypes: [] })
+ grapher.populateFromQueryParams({ tab: "line" })
+ expect(grapher.activeTab).toEqual(GrapherTabName.Table)
+ })
+
+ it("adds tab=chart to the URL if there is a single chart tab", () => {
+ const grapher = new Grapher({
+ hasMapTab: true,
+ tab: GrapherTabOption.map,
+ })
+ grapher.setTab(GrapherTabName.LineChart)
+ expect(grapher.changedParams.tab).toEqual("chart")
+ })
+
+ it("adds the chart type name as tab query param if there are multiple chart tabs", () => {
+ const grapher = new Grapher({
+ chartTypes: [ChartTypeName.LineChart, ChartTypeName.SlopeChart],
+ hasMapTab: true,
+ tab: GrapherTabOption.map,
+ })
+ grapher.setTab(GrapherTabName.LineChart)
+ expect(grapher.changedParams.tab).toEqual("line")
+ })
})
describe("time domain tests", () => {
@@ -918,7 +986,7 @@ it("correctly identifies activeColumnSlugs", () => {
`)
const grapher = new Grapher({
table,
- type: ChartTypeName.ScatterPlot,
+ chartTypes: [ChartTypeName.ScatterPlot],
xSlug: "gdp",
ySlugs: "child_mortality",
colorSlug: "continent",
@@ -955,9 +1023,9 @@ it("considers map tolerance before using column tolerance", () => {
const grapher = new Grapher({
table,
- type: ChartTypeName.WorldMap,
ySlugs: "gdp",
tab: GrapherTabOption.map,
+ hasMapTab: true,
map: new MapConfig({ timeTolerance: 1, columnSlug: "gdp", time: 2002 }),
})
@@ -1016,7 +1084,7 @@ describe("tableForSelection", () => {
const grapher = new Grapher({
table,
- type: ChartTypeName.ScatterPlot,
+ chartTypes: [ChartTypeName.ScatterPlot],
excludedEntities: [3],
xSlug: "x",
ySlugs: "y",
@@ -1052,7 +1120,7 @@ it("handles tolerance when there are gaps in ScatterPlot data", () => {
const grapher = new Grapher({
table,
- type: ChartTypeName.ScatterPlot,
+ chartTypes: [ChartTypeName.ScatterPlot],
xSlug: "x",
ySlugs: "y",
minTime: 1999,
diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.stories.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.stories.tsx
index a5d3b43b6bc..84248560305 100644
--- a/packages/@ourworldindata/grapher/src/core/Grapher.stories.tsx
+++ b/packages/@ourworldindata/grapher/src/core/Grapher.stories.tsx
@@ -49,7 +49,7 @@ export const Line = (): React.ReactElement =>
export const SlopeChart = (): React.ReactElement => {
const model = {
- type: ChartTypeName.SlopeChart,
+ chartTypes: [ChartTypeName.SlopeChart],
...basics,
}
return
@@ -57,7 +57,7 @@ export const SlopeChart = (): React.ReactElement => {
export const ScatterPlot = (): React.ReactElement => {
const model = {
- type: ChartTypeName.ScatterPlot,
+ chartTypes: [ChartTypeName.ScatterPlot],
...basics,
}
return
@@ -65,7 +65,7 @@ export const ScatterPlot = (): React.ReactElement => {
export const DiscreteBar = (): React.ReactElement => {
const model = {
- type: ChartTypeName.DiscreteBar,
+ chartTypes: [ChartTypeName.DiscreteBar],
...basics,
}
return
@@ -73,7 +73,7 @@ export const DiscreteBar = (): React.ReactElement => {
export const StackedBar = (): React.ReactElement => {
const model = {
- type: ChartTypeName.StackedBar,
+ chartTypes: [ChartTypeName.StackedBar],
...basics,
}
return
@@ -81,7 +81,7 @@ export const StackedBar = (): React.ReactElement => {
export const StackedArea = (): React.ReactElement => {
const model = {
- type: ChartTypeName.StackedArea,
+ chartTypes: [ChartTypeName.StackedArea],
...basics,
}
return
@@ -97,7 +97,6 @@ export const MapFirst = (): React.ReactElement => {
export const BlankGrapher = (): React.ReactElement => {
const model = {
- type: ChartTypeName.WorldMap,
tab: GrapherTabOption.map,
table: BlankOwidTable(),
hasMapTab: true,
@@ -115,7 +114,7 @@ export const NoMap = (): React.ReactElement => {
export const Faceting = (): React.ReactElement => {
const model = {
- type: ChartTypeName.StackedArea,
+ chartTypes: [ChartTypeName.StackedArea],
facet: FacetStrategy.entity,
...basics,
}
@@ -161,7 +160,7 @@ class PerfGrapher extends React.Component {
diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx
index 8f065684949..41574adb404 100644
--- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx
+++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx
@@ -67,6 +67,7 @@ import {
extractDetailsFromSyntax,
omit,
isTouchDevice,
+ areSetsEqual,
} from "@ourworldindata/utils"
import {
MarkdownTextWrap,
@@ -107,6 +108,8 @@ import {
Color,
GRAPHER_QUERY_PARAM_KEYS,
GrapherTooltipAnchor,
+ GrapherTabName,
+ GrapherTabQueryParam,
} from "@ourworldindata/types"
import {
BlankOwidTable,
@@ -133,6 +136,7 @@ import {
GRAPHER_FRAME_PADDING_HORIZONTAL,
GRAPHER_FRAME_PADDING_VERTICAL,
latestGrapherConfigSchema,
+ validChartTypeCombinations,
} from "../core/GrapherConstants"
import { loadVariableDataAndMetadata } from "./loadVariable"
import Cookies from "js-cookie"
@@ -194,6 +198,8 @@ import { ScatterPlotManager } from "../scatterCharts/ScatterPlotChartConstants"
import {
autoDetectSeriesStrategy,
autoDetectYColumnSlugs,
+ mapChartTypeNameToQueryParam,
+ mapQueryParamToChartTypeName,
} from "../chart/ChartUtils"
import classnames from "classnames"
import { GrapherAnalytics } from "./GrapherAnalytics"
@@ -305,6 +311,7 @@ export interface GrapherProgrammaticInterface extends GrapherInterface {
hideTableFilterToggle?: boolean
forceHideAnnotationFieldsInTitle?: AnnotationFieldsInTitle
hasTableTab?: boolean
+ hideChartTabs?: boolean
hideShareButton?: boolean
hideExploreTheDataButton?: boolean
hideRelatedQuestion?: boolean
@@ -354,7 +361,7 @@ export class Grapher
SlopeChartManager
{
@observable.ref $schema = latestGrapherConfigSchema
- @observable.ref type = ChartTypeName.LineChart
+ @observable.ref chartTypes = [ChartTypeName.LineChart]
@observable.ref id?: number = undefined
@observable.ref version = 1
@observable.ref slug?: string = undefined
@@ -388,9 +395,9 @@ 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 chartTab?: ChartTypeName // TODO: remove map from ChartTypeName
@observable.ref isPublished?: boolean = undefined
@observable.ref baseColorScheme?: ColorSchemeName = undefined
@observable.ref invertColorScheme?: boolean = undefined
@@ -613,13 +620,10 @@ export class Grapher
@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.setTab(tab)
+ else console.error("Unexpected tab: " + params.tab)
}
// Set overlay if specified
@@ -701,6 +705,29 @@ export class Grapher
) as TimeBounds
}
+ @computed get activeTab(): GrapherTabName {
+ if (this.tab === GrapherTabOption.table) return GrapherTabName.Table
+ if (this.tab === GrapherTabOption.map) return GrapherTabName.WorldMap
+ if (this.chartTab) return this.chartTab as unknown as GrapherTabName
+ return (
+ (this.chartType as unknown as GrapherTabName) ??
+ GrapherTabName.LineChart
+ )
+ }
+
+ @computed get activeChartType(): ChartTypeName | undefined {
+ if (!this.isOnChartTab) return undefined
+ return this.activeTab as unknown as ChartTypeName
+ }
+
+ @computed get chartType(): ChartTypeName | undefined {
+ return this.validChartTypes[0]
+ }
+
+ @computed get hasChartTab(): boolean {
+ return this.validChartTypes.length > 0
+ }
+
@computed get isOnChartTab(): boolean {
return this.tab === GrapherTabOption.chart
}
@@ -728,7 +755,7 @@ export class Grapher
@computed get showLegend(): boolean {
// hide the legend for stacked bar charts
// if the legend only ever shows a single entity
- if (this.isStackedBar) {
+ if (this.isOnStackedBarTab) {
const seriesStrategy =
this.chartInstance.seriesStrategy ||
autoDetectSeriesStrategy(this, true)
@@ -757,10 +784,9 @@ export class Grapher
// Depending on the chart type, the criteria for being able to select an entity are
// different; e.g. for scatterplots, the entity needs to (1) not be excluded and
// (2) needs to have data for the x and y dimension.
- let table =
- this.isScatter || this.isSlopeChart
- ? this.tableAfterAuthorTimelineAndActiveChartTransform
- : this.inputTable
+ let table = this.isScatter
+ ? this.tableAfterAuthorTimelineAndActiveChartTransform
+ : this.inputTable
if (!this.isReady) return table
@@ -955,7 +981,7 @@ export class Grapher
properties: [
// might be missing for charts within explorers or mdims
["slug", this.slug ?? "missing-slug"],
- ["chartType", this.type],
+ ["chartTypes", this.validChartTypes],
["tab", this.tab],
],
},
@@ -1212,7 +1238,7 @@ export class Grapher
@computed get isSingleTimeScatterAnimationActive(): boolean {
return (
this.isTimelineAnimationActive &&
- this.isScatter &&
+ this.isOnScatterTab &&
!this.isRelativeMode &&
!!this.areHandlesOnSameTimeBeforeAnimation
)
@@ -1269,6 +1295,19 @@ export class Grapher
this.disposers.forEach((dispose) => dispose())
}
+ @action.bound setTab(newTab: GrapherTabName): void {
+ if (newTab === GrapherTabName.Table) {
+ this.tab = GrapherTabOption.table
+ this.chartTab = undefined
+ } else if (newTab === GrapherTabName.WorldMap) {
+ this.tab = GrapherTabOption.map
+ this.chartTab = undefined
+ } else {
+ this.tab = GrapherTabOption.chart
+ this.chartTab = newTab as unknown as ChartTypeName
+ }
+ }
+
// todo: can we remove this?
// I believe these states can only occur during editing.
@action.bound private ensureValidConfigWhenEditing(): void {
@@ -1280,8 +1319,8 @@ export class Grapher
)
const disposers = [
autorun(() => {
- if (!this.availableTabs.includes(this.tab))
- runInAction(() => (this.tab = this.availableTabs[0]))
+ if (!this.availableTabs.includes(this.activeTab))
+ runInAction(() => this.setTab(this.availableTabs[0]))
}),
autorun(() => {
const validDimensions = this.validDimensions
@@ -1483,12 +1522,37 @@ export class Grapher
})
}
- @computed get availableTabs(): GrapherTabOption[] {
+ @computed get validChartTypes(): ChartTypeName[] {
+ const { chartTypes } = this
+
+ // all single-chart Graphers are valid
+ if (chartTypes.length <= 1) return chartTypes
+
+ const chartTypeSet = new Set(chartTypes)
+ for (const validCombination of validChartTypeCombinations) {
+ const validCombinationSet = new Set(validCombination)
+ if (areSetsEqual(chartTypeSet, validCombinationSet))
+ return validCombination
+ }
+
+ // if the given combination is not valid, then ignore all but the first chart type
+ return chartTypes.slice(0, 1)
+ }
+
+ @computed get validChartTypeSet(): Set {
+ return new Set(this.validChartTypes)
+ }
+
+ @computed get availableTabs(): GrapherTabName[] {
return [
- this.hasTableTab && GrapherTabOption.table,
- this.hasMapTab && GrapherTabOption.map,
- this.hasChartTab && GrapherTabOption.chart,
- ].filter(identity) as GrapherTabOption[]
+ this.hasTableTab && GrapherTabName.Table,
+ this.hasMapTab && GrapherTabName.WorldMap,
+ ...this.validChartTypes,
+ ].filter(identity) as GrapherTabName[]
+ }
+
+ @computed get hasMultipleChartTypes(): boolean {
+ return this.validChartTypes.length > 1
}
@computed get currentSubtitle(): string {
@@ -1527,9 +1591,9 @@ export class Grapher
(this.hasTimeline &&
// chart types that refer to the current time only in the timeline
(this.isLineChartThatTurnedIntoDiscreteBar ||
- this.isDiscreteBar ||
- this.isStackedDiscreteBar ||
- this.isMarimekko ||
+ this.isOnDiscreteBarTab ||
+ this.isOnStackedDiscreteBarTab ||
+ this.isOnMarimekkoTab ||
this.isOnMapTab)))
)
}
@@ -1539,7 +1603,7 @@ export class Grapher
!this.hideAnnotationFieldsInTitle?.changeInPrefix
return (
!this.forceHideAnnotationFieldsInTitle?.changeInPrefix &&
- this.isLineChart &&
+ this.isOnLineChartTab &&
this.isRelativeMode &&
showChangeInPrefix
)
@@ -1867,35 +1931,34 @@ export class Grapher
@computed
get typeExceptWhenLineChartAndSingleTimeThenWillBeBarChart(): ChartTypeName {
- // Switch to bar chart if a single year is selected. Todo: do we want to do this?
return this.isLineChartThatTurnedIntoDiscreteBar
? ChartTypeName.DiscreteBar
- : this.type
+ : this.activeChartType ?? ChartTypeName.LineChart
}
@computed get isLineChart(): boolean {
- return this.type === ChartTypeName.LineChart
+ return this.chartType === ChartTypeName.LineChart || !this.chartType
}
@computed get isScatter(): boolean {
- return this.type === ChartTypeName.ScatterPlot
+ return this.chartType === ChartTypeName.ScatterPlot
}
@computed get isStackedArea(): boolean {
- return this.type === ChartTypeName.StackedArea
+ return this.chartType === ChartTypeName.StackedArea
}
@computed get isSlopeChart(): boolean {
- return this.type === ChartTypeName.SlopeChart
+ return this.chartType === ChartTypeName.SlopeChart
}
@computed get isDiscreteBar(): boolean {
- return this.type === ChartTypeName.DiscreteBar
+ return this.chartType === ChartTypeName.DiscreteBar
}
@computed get isStackedBar(): boolean {
- return this.type === ChartTypeName.StackedBar
+ return this.chartType === ChartTypeName.StackedBar
}
@computed get isMarimekko(): boolean {
- return this.type === ChartTypeName.Marimekko
+ return this.chartType === ChartTypeName.Marimekko
}
@computed get isStackedDiscreteBar(): boolean {
- return this.type === ChartTypeName.StackedDiscreteBar
+ return this.chartType === ChartTypeName.StackedDiscreteBar
}
@computed get isLineChartThatTurnedIntoDiscreteBar(): boolean {
@@ -1925,6 +1988,38 @@ export class Grapher
return closestMinTime !== undefined && closestMinTime === closestMaxTime
}
+ @computed get isOnLineChartTab(): boolean {
+ return this.activeChartType === ChartTypeName.LineChart
+ }
+ @computed get isOnScatterTab(): boolean {
+ return this.activeChartType === ChartTypeName.ScatterPlot
+ }
+ @computed get isOnStackedAreaTab(): boolean {
+ return this.activeChartType === ChartTypeName.StackedArea
+ }
+ @computed get isOnSlopeChartTab(): boolean {
+ return this.activeChartType === ChartTypeName.SlopeChart
+ }
+ @computed get isOnDiscreteBarTab(): boolean {
+ return this.activeChartType === ChartTypeName.DiscreteBar
+ }
+ @computed get isOnStackedBarTab(): boolean {
+ return this.activeChartType === ChartTypeName.StackedBar
+ }
+ @computed get isOnMarimekkoTab(): boolean {
+ return this.activeChartType === ChartTypeName.Marimekko
+ }
+ @computed get isOnStackedDiscreteBarTab(): boolean {
+ return this.activeChartType === ChartTypeName.StackedDiscreteBar
+ }
+
+ @computed get hasLineChart(): boolean {
+ return this.validChartTypeSet.has(ChartTypeName.LineChart)
+ }
+ @computed get hasSlopeChart(): boolean {
+ return this.validChartTypeSet.has(ChartTypeName.SlopeChart)
+ }
+
@computed get supportsMultipleYColumns(): boolean {
return !(this.isScatter || this.isSlopeChart)
}
@@ -2037,14 +2132,14 @@ export class Grapher
@computed get mapIsClickable(): boolean {
return (
this.hasChartTab &&
- (this.isLineChart || this.isScatter) &&
+ (this.hasLineChart || this.isScatter) &&
!isMobile()
)
}
@computed get relativeToggleLabel(): string {
- if (this.isScatter) return "Display average annual change"
- else if (this.isLineChart) return "Display relative change"
+ if (this.isOnScatterTab) return "Display average annual change"
+ else if (this.isOnLineChartTab) return "Display relative change"
return "Display relative values"
}
@@ -2378,7 +2473,7 @@ export class Grapher
}
@action.bound private toggleTabCommand(): void {
- this.tab = next(this.availableTabs, this.tab)
+ this.setTab(next(this.availableTabs, this.activeTab))
}
@action.bound private togglePlayingCommand(): void {
@@ -2928,7 +3023,6 @@ export class Grapher
return this.frameBounds.width <= 420
}
- // SemiNarrow charts shorten their button labels to fit within the controls row
@computed get isSemiNarrow(): boolean {
if (this.isStatic) return false
return this.frameBounds.width <= 550
@@ -3162,9 +3256,58 @@ export class Grapher
debounceMode = false
+ private mapQueryParamToGrapherTab(tab: string): GrapherTabName | undefined {
+ const {
+ chartType: defaultChartType,
+ validChartTypeSet,
+ hasMapTab,
+ } = this
+
+ if (tab === GrapherTabQueryParam.Table) {
+ return GrapherTabName.Table
+ }
+ if (tab === GrapherTabQueryParam.WorldMap) {
+ return GrapherTabName.WorldMap
+ }
+
+ if (tab === GrapherTabQueryParam.Chart) {
+ if (defaultChartType) {
+ return defaultChartType as unknown as GrapherTabName
+ } else if (hasMapTab) {
+ return GrapherTabName.WorldMap
+ } else {
+ return GrapherTabName.Table
+ }
+ }
+
+ const chartTypeName = mapQueryParamToChartTypeName(tab)
+
+ if (!chartTypeName) return undefined
+
+ if (validChartTypeSet.has(chartTypeName)) {
+ return chartTypeName as unknown as GrapherTabName
+ } else if (defaultChartType) {
+ return defaultChartType as unknown as GrapherTabName
+ } else if (hasMapTab) {
+ return GrapherTabName.WorldMap
+ } else {
+ return GrapherTabName.Table
+ }
+ }
+
+ mapGrapherTabToQueryParam(tab: GrapherTabName): string {
+ if (tab === GrapherTabName.Table) return GrapherTabQueryParam.Table
+ if (tab === GrapherTabName.WorldMap)
+ return GrapherTabQueryParam.WorldMap
+
+ if (!this.hasMultipleChartTypes) return GrapherTabQueryParam.Chart
+
+ return mapChartTypeNameToQueryParam(tab as unknown as ChartTypeName)
+ }
+
@computed.struct get allParams(): GrapherQueryParams {
const params: GrapherQueryParams = {}
- params.tab = this.tab
+ params.tab = this.mapGrapherTabToQueryParam(this.activeTab)
params.xScale = this.xAxis.scaleType
params.yScale = this.yAxis.scaleType
params.stackMode = this.stackMode
@@ -3331,7 +3474,7 @@ export class Grapher
}
@computed get disablePlay(): boolean {
- return this.isSlopeChart
+ return this.isOnSlopeChartTab
}
@computed get animationEndTime(): Time {
@@ -3376,7 +3519,7 @@ export class Grapher
@computed get canChangeEntity(): boolean {
return (
this.hasChartTab &&
- !this.isScatter &&
+ !this.isOnScatterTab &&
!this.canSelectMultipleEntities &&
this.addCountryMode === EntitySelectionMode.SingleEntity &&
this.numSelectableEntityNames > 1
@@ -3387,11 +3530,11 @@ export class Grapher
return (
this.hasChartTab &&
this.canSelectMultipleEntities &&
- (this.isLineChart ||
- this.isStackedArea ||
- this.isStackedBar ||
- this.isDiscreteBar ||
- this.isStackedDiscreteBar)
+ (this.isOnLineChartTab ||
+ this.isOnStackedAreaTab ||
+ this.isOnStackedBarTab ||
+ this.isOnDiscreteBarTab ||
+ this.isOnStackedDiscreteBarTab)
)
}
@@ -3500,6 +3643,7 @@ export class Grapher
changeInPrefix: false,
}
@observable hasTableTab = true
+ @observable hideChartTabs = false
@observable hideShareButton = false
@observable hideExploreTheDataButton = true
@observable hideRelatedQuestion = false
diff --git a/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts b/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts
index 82dbc2ab89b..3921dfd04b0 100644
--- a/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts
+++ b/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts
@@ -1,4 +1,5 @@
import { defaultGrapherConfig } from "../schema/defaultGrapherConfig.js"
+import { ChartTypeName } from "@ourworldindata/types"
import type { GrapherProgrammaticInterface } from "./Grapher"
export const GRAPHER_EMBEDDED_FIGURE_ATTR = "data-grapher-src"
@@ -82,7 +83,7 @@ export enum Patterns {
noDataPatternForMapChart = "noDataPatternForMapChart",
}
-export const grapherInterfaceWithHiddenControlsOnly: GrapherProgrammaticInterface =
+export const grapherInterfaceWithHiddenControls: GrapherProgrammaticInterface =
{
hideRelativeToggle: true,
hideTimeline: true,
@@ -100,9 +101,17 @@ export const grapherInterfaceWithHiddenControlsOnly: GrapherProgrammaticInterfac
},
}
-export const grapherInterfaceWithHiddenTabsOnly: GrapherProgrammaticInterface =
- {
- hasChartTab: false,
- hasMapTab: false,
- hasTableTab: false,
- }
+export const grapherInterfaceWithHiddenTabs: GrapherProgrammaticInterface = {
+ hasMapTab: false,
+ hasTableTab: false,
+ hideChartTabs: true,
+}
+
+/**
+ * Chart type combinations that are currently supported.
+ *
+ * This also determines the order of chart types in the UI.
+ */
+export const validChartTypeCombinations = [
+ [ChartTypeName.LineChart, ChartTypeName.SlopeChart],
+]
diff --git a/packages/@ourworldindata/grapher/src/core/GrapherWithChartTypes.jsdom.test.tsx b/packages/@ourworldindata/grapher/src/core/GrapherWithChartTypes.jsdom.test.tsx
index b82ca6f925e..467fa2af3c4 100755
--- a/packages/@ourworldindata/grapher/src/core/GrapherWithChartTypes.jsdom.test.tsx
+++ b/packages/@ourworldindata/grapher/src/core/GrapherWithChartTypes.jsdom.test.tsx
@@ -50,7 +50,7 @@ const basicGrapherConfig: GrapherProgrammaticInterface = {
describe("grapher and discrete bar charts", () => {
const grapher = new Grapher({
- type: ChartTypeName.DiscreteBar,
+ chartTypes: [ChartTypeName.DiscreteBar],
...basicGrapherConfig,
})
expect(grapher.chartInstance.series.length).toBeGreaterThan(0)
diff --git a/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.test.ts b/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.test.ts
index 29c8fcf6302..c292f2ad16a 100755
--- a/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.test.ts
+++ b/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.test.ts
@@ -466,7 +466,7 @@ describe(legacyToOwidTableAndDimensions, () => {
it("joins targetTime", () => {
const scatterLegacyGrapherConfig = {
...legacyGrapherConfig,
- type: ChartTypeName.ScatterPlot,
+ chartTypes: [ChartTypeName.ScatterPlot],
}
const { table } = legacyToOwidTableAndDimensions(
diff --git a/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts b/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts
index dad8561d05c..19e1bd42d86 100644
--- a/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts
+++ b/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts
@@ -198,9 +198,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 = grapherConfig.chartTypes?.[0]
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..c52f0c833af 100755
--- a/packages/@ourworldindata/grapher/src/dataTable/DataTable.jsdom.test.tsx
+++ b/packages/@ourworldindata/grapher/src/dataTable/DataTable.jsdom.test.tsx
@@ -70,7 +70,7 @@ describe("when you select a range of years", () => {
let view: ReactWrapper
beforeAll(() => {
const grapher = childMortalityGrapher({
- type: ChartTypeName.LineChart,
+ chartTypes: [ChartTypeName.LineChart],
tab: GrapherTabOption.table,
})
grapher.timelineHandleTimeBounds = [1950, 2019]
diff --git a/packages/@ourworldindata/grapher/src/index.ts b/packages/@ourworldindata/grapher/src/index.ts
index 0b51f881722..81efb82e166 100644
--- a/packages/@ourworldindata/grapher/src/index.ts
+++ b/packages/@ourworldindata/grapher/src/index.ts
@@ -22,8 +22,8 @@ export {
ThereWasAProblemLoadingThisChart,
WorldEntityName,
Patterns,
- grapherInterfaceWithHiddenControlsOnly,
- grapherInterfaceWithHiddenTabsOnly,
+ grapherInterfaceWithHiddenControls,
+ grapherInterfaceWithHiddenTabs,
CONTINENTS_INDICATOR_ID,
POPULATION_INDICATOR_ID_USED_IN_ADMIN,
latestGrapherConfigSchema,
diff --git a/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts b/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts
index dd0f68334e2..79da7c8c82a 100644
--- a/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts
+++ b/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts
@@ -4,11 +4,17 @@
import { GrapherInterface } from "@ourworldindata/types"
-export const latestSchemaVersion = "005" as const
-export const outdatedSchemaVersions = ["001", "002", "003", "004"] as const
+export const latestSchemaVersion = "006" as const
+export const outdatedSchemaVersions = [
+ "001",
+ "002",
+ "003",
+ "004",
+ "005",
+] as const
export const defaultGrapherConfig = {
- $schema: "https://files.ourworldindata.org/schemas/grapher-schema.005.json",
+ $schema: "https://files.ourworldindata.org/schemas/grapher-schema.006.json",
map: {
projection: "World",
hideTimeline: false,
@@ -33,7 +39,6 @@ export const defaultGrapherConfig = {
},
tab: "chart",
matchingEntitiesOnly: false,
- hasChartTab: true,
hideLegend: false,
hideLogo: false,
timelineMinTime: "earliest",
@@ -54,7 +59,7 @@ export const defaultGrapherConfig = {
facettingLabelByYVariables: "metric",
addCountryMode: "add-country",
compareEndPointsOnly: false,
- type: "LineChart",
+ chartTypes: ["LineChart"],
hasMapTab: false,
stackMode: "absolute",
minTime: "earliest",
diff --git a/packages/@ourworldindata/grapher/src/schema/grapher-schema.005.yaml b/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml
similarity index 97%
rename from packages/@ourworldindata/grapher/src/schema/grapher-schema.005.yaml
rename to packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml
index 4465a68f6c0..67ae88895c5 100644
--- a/packages/@ourworldindata/grapher/src/schema/grapher-schema.005.yaml
+++ b/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml
@@ -1,7 +1,7 @@
$schema: "http://json-schema.org/draft-07/schema#"
# if you update the required keys, make sure that the mergeGrapherConfigs and
# diffGrapherConfigs functions both reflect this change
-$id: "https://files.ourworldindata.org/schemas/grapher-schema.005.json"
+$id: "https://files.ourworldindata.org/schemas/grapher-schema.006.json"
required:
- $schema
- dimensions
@@ -14,13 +14,13 @@ properties:
type: string
description: Url of the concrete schema version to use to validate this document
format: uri
- default: "https://files.ourworldindata.org/schemas/grapher-schema.005.json"
+ default: "https://files.ourworldindata.org/schemas/grapher-schema.006.json"
# for now, we only validate configs in our database using this schema.
# since we expect all configs in our database to be valid against the latest schema,
# we restrict the $schema field to a single value, the latest schema version.
# if we ever need to validate configs against multiple schema versions,
# we can remove this constraint.
- const: "https://files.ourworldindata.org/schemas/grapher-schema.005.json"
+ const: "https://files.ourworldindata.org/schemas/grapher-schema.006.json"
id:
type: integer
description: Internal DB id. Useful internally for OWID but not required if just using grapher directly.
@@ -123,10 +123,6 @@ properties:
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
@@ -368,19 +364,21 @@ 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
+ chartTypes:
+ type: array
+ description: Which chart types should be shown
+ default: ["LineChart"]
+ items:
+ type: string
+ enum:
+ - LineChart
+ - ScatterPlot
+ - StackedArea
+ - DiscreteBar
+ - StackedDiscreteBar
+ - SlopeChart
+ - StackedBar
+ - Marimekko
hasMapTab:
type: boolean
default: false
diff --git a/packages/@ourworldindata/grapher/src/schema/migrations/migrations.ts b/packages/@ourworldindata/grapher/src/schema/migrations/migrations.ts
index 7a4fefa30f0..2a860c0cb9a 100644
--- a/packages/@ourworldindata/grapher/src/schema/migrations/migrations.ts
+++ b/packages/@ourworldindata/grapher/src/schema/migrations/migrations.ts
@@ -15,6 +15,7 @@ import {
getSchemaVersion,
isLatestVersion,
} from "./helpers"
+import { ChartTypeName } from "@ourworldindata/types"
// see https://github.com/owid/owid-grapher/commit/26f2a0d1790c71bdda7e12f284ca552945d2f6ef
const migrateFrom001To002 = (
@@ -60,6 +61,23 @@ const migrateFrom004To005 = (
return config
}
+const migrateFrom005To006 = (
+ config: AnyConfigWithValidSchema
+): AnyConfigWithValidSchema => {
+ const { type = ChartTypeName.LineChart, hasChartTab = true } = config
+
+ // add types field
+ if (!hasChartTab) config.chartTypes = []
+ else if (type !== ChartTypeName.LineChart) config.chartTypes = [type]
+
+ // remove deprecated fields
+ delete config.type
+ delete config.hasChartTab
+
+ config.$schema = createSchemaForVersion("006")
+ return config
+}
+
export const runMigration = (
config: AnyConfigWithValidSchema
): AnyConfigWithValidSchema => {
@@ -70,5 +88,6 @@ export const runMigration = (
.with("002", () => migrateFrom002To003(config))
.with("003", () => migrateFrom003To004(config))
.with("004", () => migrateFrom004To005(config))
+ .with("005", () => migrateFrom005To006(config))
.exhaustive()
}
diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.jsdom.test.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.jsdom.test.tsx
index c4650faccd6..306f5376524 100644
--- a/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.jsdom.test.tsx
+++ b/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.jsdom.test.tsx
@@ -23,7 +23,7 @@ it("can filter years correctly", () => {
// TODO: why is it ySlugs and xSlug here instead of yColumnSlugs and xColumnSlug? Unify when we have config migrations?
const manager = {
- type: ChartTypeName.Marimekko,
+ chartTypes: [ChartTypeName.Marimekko],
table,
selection: table.availableEntityNames,
ySlugs: "percentBelow2USD",
@@ -133,7 +133,7 @@ it("shows no data points at the end", () => {
// TODO: why is it ySlugs and xSlug here instead of yColumnSlugs and xColumnSlug? Unify when we have config migrations?
const manager = {
- type: ChartTypeName.Marimekko,
+ chartTypes: [ChartTypeName.Marimekko],
table,
selection: table.availableEntityNames,
ySlugs: "percentBelow2USD",
@@ -233,7 +233,7 @@ test("interpolation works as expected", () => {
// TODO: why is it ySlugs and xSlug here instead of yColumnSlugs and xColumnSlug? Unify when we have config migrations?
const manager = {
- type: ChartTypeName.Marimekko,
+ chartTypes: [ChartTypeName.Marimekko],
table,
selection: table.availableEntityNames,
ySlugs: "percentBelow2USD",
@@ -344,7 +344,7 @@ it("can deal with y columns with missing values", () => {
// TODO: why is it ySlugs and xSlug here instead of yColumnSlugs and xColumnSlug? Unify when we have config migrations?
const manager = {
- type: ChartTypeName.Marimekko,
+ chartTypes: [ChartTypeName.Marimekko],
table,
selection: table.availableEntityNames,
ySlugs: "percentBelow2USD percentBelow10USD",
diff --git a/packages/@ourworldindata/types/src/dbTypes/ChartConfigs.ts b/packages/@ourworldindata/types/src/dbTypes/ChartConfigs.ts
index e3b91105b86..d6ad7227811 100644
--- a/packages/@ourworldindata/types/src/dbTypes/ChartConfigs.ts
+++ b/packages/@ourworldindata/types/src/dbTypes/ChartConfigs.ts
@@ -1,5 +1,8 @@
import { Base64String, JsonString } from "../domainTypes/Various.js"
-import { GrapherInterface } from "../grapherTypes/GrapherTypes.js"
+import {
+ ChartTypeName,
+ GrapherInterface,
+} from "../grapherTypes/GrapherTypes.js"
export const ChartConfigsTableName = "chart_configs"
export interface DbInsertChartConfig {
@@ -8,6 +11,7 @@ export interface DbInsertChartConfig {
full: JsonString
fullMd5?: Base64String
slug?: string | null
+ chartType?: ChartTypeName | null // TODO: exclude WorldMap
createdAt?: Date
updatedAt?: Date | null
}
diff --git a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts
index 05c80c40fb0..3a7a1c23b43 100644
--- a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts
+++ b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts
@@ -172,12 +172,54 @@ export type SeriesName = string
export type SeriesColorMap = Map
+/**
+ * Grapher tab specified in the config that determines the default tab to show.
+ * If `chart` is selected and Grapher has more than one chart tab, then the
+ * first chart tab will be active.
+ */
export enum GrapherTabOption {
chart = "chart",
map = "map",
table = "table",
}
+/**
+ * Internal tab names used in Grapher.
+ */
+export enum GrapherTabName {
+ Table = "Table",
+ WorldMap = "WorldMap",
+
+ // chart types
+ LineChart = "LineChart",
+ ScatterPlot = "ScatterPlot",
+ StackedArea = "StackedArea",
+ DiscreteBar = "DiscreteBar",
+ StackedDiscreteBar = "StackedDiscreteBar",
+ SlopeChart = "SlopeChart",
+ StackedBar = "StackedBar",
+ Marimekko = "Marimekko",
+}
+
+/**
+ * Valid values for the `tab` query parameter in Grapher.
+ */
+export enum GrapherTabQueryParam {
+ Chart = "chart",
+ Table = "table",
+ WorldMap = "map",
+
+ // chart types
+ LineChart = "line",
+ ScatterPlot = "scatter",
+ StackedArea = "stacked-area",
+ DiscreteBar = "discrete-bar",
+ StackedDiscreteBar = "stacked-discrete-bar",
+ SlopeChart = "slope",
+ StackedBar = "stacked-bar",
+ Marimekko = "marimekko",
+}
+
export interface RelatedQuestionsConfig {
text: string
url: string
@@ -535,7 +577,7 @@ export interface MapConfigInterface {
// under the same rendering conditions it ought to remain visually identical
export interface GrapherInterface extends SortConfig {
$schema?: string
- type?: ChartTypeName
+ chartTypes?: ChartTypeName[]
id?: number
version?: number
slug?: string
@@ -563,7 +605,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
relatedQuestions?: RelatedQuestionsConfig[]
@@ -654,7 +695,7 @@ 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",
+ "chartTypes",
"id",
"version",
"slug",
@@ -680,7 +721,6 @@ export const grapherKeysToSerialize = [
"hideTimeline",
"zoomToSelection",
"showYearLabels",
- "hasChartTab",
"hasMapTab",
"tab",
"internalNotes",
diff --git a/packages/@ourworldindata/types/src/index.ts b/packages/@ourworldindata/types/src/index.ts
index 914e651385b..347263145cf 100644
--- a/packages/@ourworldindata/types/src/index.ts
+++ b/packages/@ourworldindata/types/src/index.ts
@@ -75,6 +75,8 @@ export {
ColorSchemeName,
ChartTypeName,
GrapherTabOption,
+ GrapherTabName,
+ GrapherTabQueryParam,
StackMode,
EntitySelectionMode,
ScatterPointLabelStrategy,
diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts
index f62bdc5bb40..ccdd684a6f8 100644
--- a/packages/@ourworldindata/utils/src/Util.ts
+++ b/packages/@ourworldindata/utils/src/Util.ts
@@ -926,6 +926,9 @@ export const differenceOfSets = (sets: Set[]): Set => {
return diff
}
+export const areSetsEqual = (setA: Set, setB: Set): boolean =>
+ setA.size === setB.size && [...setA].every((value) => setB.has(value))
+
/** Tests whether the first argument is a strict subset of the second. The arguments do not have
to be sets yet, they can be any iterable. Sets will be created by the function internally */
export function isSubsetOf(
@@ -1966,9 +1969,10 @@ export function traverseObjects>(
export function getParentVariableIdFromChartConfig(
config: GrapherInterface
): number | undefined {
- const { type = ChartTypeName.LineChart, dimensions } = config
+ const { chartTypes, dimensions } = config
- if (type === ChartTypeName.ScatterPlot) return undefined
+ const chartType = chartTypes?.[0] ?? ChartTypeName.LineChart
+ if (chartType === ChartTypeName.ScatterPlot) return undefined
if (!dimensions) return undefined
const yVariableIds = dimensions
diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts
index eacf00917fa..75f99afdd16 100644
--- a/packages/@ourworldindata/utils/src/index.ts
+++ b/packages/@ourworldindata/utils/src/index.ts
@@ -62,6 +62,7 @@ export {
intersectionOfSets,
unionOfSets,
differenceOfSets,
+ areSetsEqual,
isSubsetOf,
intersection,
sortByUndefinedLast,
diff --git a/site/gdocs/components/Chart.tsx b/site/gdocs/components/Chart.tsx
index dbc57a3f973..b4a239370c6 100644
--- a/site/gdocs/components/Chart.tsx
+++ b/site/gdocs/components/Chart.tsx
@@ -1,17 +1,17 @@
import React, { useRef } from "react"
import { useEmbedChart } from "../../hooks.js"
import {
- grapherInterfaceWithHiddenControlsOnly,
- grapherInterfaceWithHiddenTabsOnly,
+ grapherInterfaceWithHiddenControls,
+ grapherInterfaceWithHiddenTabs,
GrapherProgrammaticInterface,
} from "@ourworldindata/grapher"
import {
ChartControlKeyword,
ChartTabKeyword,
EnrichedBlockChart,
- identity,
Url,
merge,
+ excludeUndefined,
} from "@ourworldindata/utils"
import { ChartConfigType } from "@ourworldindata/types"
import { renderSpans, useLinkedChart } from "../utils.js"
@@ -54,17 +54,26 @@ export default function Chart({
if (!isExplorer && isCustomized) {
const controls: ChartControlKeyword[] = d.controls || []
const tabs: ChartTabKeyword[] = d.tabs || []
+
const showAllControls = controls.includes(ChartControlKeyword.all)
const showAllTabs = tabs.includes(ChartTabKeyword.all)
- const listOfPartialGrapherConfigs = [...controls, ...tabs]
- .map(mapKeywordToGrapherConfig)
- .filter(identity) as GrapherProgrammaticInterface[]
+
+ const allControlsHidden = grapherInterfaceWithHiddenControls
+ const allTabsHidden = grapherInterfaceWithHiddenTabs
+
+ const enabledControls = excludeUndefined(
+ controls.map(mapControlKeywordToGrapherConfig)
+ )
+ const enabledTabs = excludeUndefined(
+ tabs.map(mapTabKeywordToGrapherConfig)
+ )
customizedChartConfig = merge(
{},
- !showAllControls ? grapherInterfaceWithHiddenControlsOnly : {},
- !showAllTabs ? grapherInterfaceWithHiddenTabsOnly : {},
- ...listOfPartialGrapherConfigs,
+ !showAllControls ? allControlsHidden : {},
+ !showAllTabs ? allTabsHidden : {},
+ ...enabledControls,
+ ...enabledTabs,
{
hideRelatedQuestion: true,
hideShareButton: true, // always hidden since the original chart would be shared, not the customized one
@@ -134,12 +143,10 @@ export default function Chart({
)
}
-const mapKeywordToGrapherConfig = (
- keyword: ChartControlKeyword | ChartTabKeyword
-): GrapherProgrammaticInterface | null => {
+const mapControlKeywordToGrapherConfig = (
+ keyword: ChartControlKeyword
+): GrapherProgrammaticInterface | undefined => {
switch (keyword) {
- // controls
-
case ChartControlKeyword.relativeToggle:
return { hideRelativeToggle: false }
@@ -173,18 +180,25 @@ const mapKeywordToGrapherConfig = (
case ChartControlKeyword.tableFilterToggle:
return { hideTableFilterToggle: false }
- // tabs
+ default:
+ return undefined
+ }
+}
- case ChartTabKeyword.chart:
- return { hasChartTab: true }
+const mapTabKeywordToGrapherConfig = (
+ keyword: ChartTabKeyword
+): GrapherProgrammaticInterface | undefined => {
+ switch (keyword) {
+ case ChartTabKeyword.table:
+ return { hasTableTab: true }
case ChartTabKeyword.map:
return { hasMapTab: true }
- case ChartTabKeyword.table:
- return { hasTableTab: true }
+ case ChartTabKeyword.chart:
+ return { hideChartTabs: false }
default:
- return null
+ return undefined
}
}
diff --git a/site/multiembedder/MultiEmbedder.tsx b/site/multiembedder/MultiEmbedder.tsx
index 7e1639f85e9..b9c6d085164 100644
--- a/site/multiembedder/MultiEmbedder.tsx
+++ b/site/multiembedder/MultiEmbedder.tsx
@@ -211,16 +211,10 @@ class MultiEmbedder {
// make sure the tab of the active pane is visible
if (figureConfigAttr) {
- const activeTab =
- queryParams.tab ||
- grapherPageConfig.tab ||
+ localConfig.tab =
+ 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(