diff --git a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts index 8b24bdd70a..83009ae205 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts +++ b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts @@ -17,6 +17,7 @@ import { TooltipManager } from "../tooltip/TooltipProps" import { OwidTable, CoreColumn } from "@ourworldindata/core-table" import { SelectionArray } from "../selection/SelectionArray" +import { InteractionArray } from "../selection/InteractionArray" import { ColumnSlug, SortConfig, TimeBound } from "@ourworldindata/utils" import { ColorScaleBin } from "../color/ColorScaleBin" import { ColorScale } from "../color/ColorScale" @@ -63,6 +64,7 @@ export interface ChartManager { sizeColumnSlug?: ColumnSlug colorColumnSlug?: ColumnSlug + interactionArray?: InteractionArray selection?: SelectionArray | EntityName[] entityType?: string diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index c9edeaa86d..d970b43aef 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -111,6 +111,7 @@ import { GRAPHER_TAB_NAMES, GRAPHER_TAB_QUERY_PARAMS, GrapherTabOption, + SeriesName, } from "@ourworldindata/types" import { BlankOwidTable, @@ -222,6 +223,7 @@ import { } from "../entitySelector/EntitySelector" import { SlideInDrawer } from "../slideInDrawer/SlideInDrawer" import { BodyDiv } from "../bodyDiv/BodyDiv" +import { InteractionArray } from "../selection/InteractionArray" declare global { interface Window { @@ -433,6 +435,7 @@ export class Grapher // Initializing arrays with `undefined` ensures that empty arrays get serialised @observable selectedEntityNames?: EntityName[] = undefined + @observable focusedSeriesNames?: SeriesName[] = undefined @observable excludedEntities?: number[] = undefined /** IncludedEntities are usually empty which means use all available entities. When includedEntities is set it means "only use these entities". excludedEntities @@ -565,6 +568,7 @@ export class Grapher ) obj.selectedEntityNames = this.selection.selectedEntityNames + obj.focusedSeriesNames = this.interactionArray.focusedEntityNames deleteRuntimeAndUnchangedProps(obj, defaultObject) @@ -606,6 +610,9 @@ export class Grapher if (obj.selectedEntityNames) this.selection.setSelectedEntities(obj.selectedEntityNames) + if (obj.focusedSeriesNames) + this.interactionArray.setFocusedEntities(obj.focusedSeriesNames) + // JSON doesn't support Infinity, so we use strings instead. this.minTime = minTimeBoundFromJSONOrNegativeInfinity(obj.minTime) this.maxTime = maxTimeBoundFromJSONOrPositiveInfinity(obj.maxTime) @@ -2537,6 +2544,8 @@ export class Grapher this.props.table?.availableEntities ?? [] ) + interactionArray = new InteractionArray() + @computed get availableEntities(): Entity[] { return this.tableForSelection.availableEntities } diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index a36ecb2180..8dc6cfe25a 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -107,6 +107,7 @@ import { getColorKey, getSeriesName, } from "./LineChartHelpers" +import { InteractionArray } from "../selection/InteractionArray" const LINE_CHART_CLASS_NAME = "LineChart" @@ -187,8 +188,12 @@ export class LineChart return makeSelectionArray(this.manager.selection) } + @computed get interactionArray(): InteractionArray { + return this.manager.interactionArray ?? new InteractionArray() + } + @computed private get focusedSeriesNameSet(): Set { - return this.selectionArray.focusedEntityNameSet + return this.interactionArray.focusedEntityNameSet } @computed private get missingDataStrategy(): MissingDataStrategy { @@ -540,7 +545,7 @@ export class LineChart } @action.bound onLineLegendClick(seriesName: SeriesName): void { - this.selectionArray.toggleFocus(seriesName) + this.interactionArray.toggleFocus(seriesName) } // TODO diff --git a/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml b/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml index 67ae88895c..72aeb7cbac 100644 --- a/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml +++ b/packages/@ourworldindata/grapher/src/schema/grapher-schema.006.yaml @@ -104,6 +104,15 @@ properties: items: type: - string + focusedSeriesNames: + type: array + description: | + The initially focused chart elements (e.g. line or bar). + Is either a list of entity or variable names. + Only works to line and slope charts for now. + items: + type: + - string baseColorScheme: type: string description: | diff --git a/packages/@ourworldindata/grapher/src/selection/InteractionArray.ts b/packages/@ourworldindata/grapher/src/selection/InteractionArray.ts new file mode 100644 index 0000000000..9ae3c8f374 --- /dev/null +++ b/packages/@ourworldindata/grapher/src/selection/InteractionArray.ts @@ -0,0 +1,48 @@ +import { EntityName } from "@ourworldindata/types" +import { action, computed, observable } from "mobx" + +export class InteractionArray { + constructor(focusedEntityNames: EntityName[] = []) { + this.focusedEntityNames = focusedEntityNames.slice() + } + + @observable focusedEntityNames: EntityName[] + + @computed get focusedEntityNameSet(): Set { + return new Set(this.focusedEntityNames) + } + + @action.bound focusEntity(entityName: EntityName): this { + if (!this.focusedEntityNameSet.has(entityName)) + this.focusedEntityNames.push(entityName) + return this + } + + @action.bound unfocusEntity(entityName: EntityName): this { + this.focusedEntityNames = this.focusedEntityNames.filter( + (name) => name !== entityName + ) + return this + } + + @action.bound toggleFocus(entityName: EntityName): this { + return this.focusedEntityNameSet.has(entityName) + ? this.unfocusEntity(entityName) + : this.focusEntity(entityName) + } + + @action.bound clear(): void { + this.focusedEntityNames = [] + } + + @action.bound addToFocusedEntities(entityNames: EntityName[]): this { + this.focusedEntityNames = this.focusedEntityNames.concat(entityNames) + return this + } + + // Clears and sets focused entities + @action.bound setFocusedEntities(entityNames: EntityName[]): this { + this.clear() + return this.addToFocusedEntities(entityNames) + } +} diff --git a/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts b/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts index 10405feb45..8f1c06a013 100644 --- a/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts +++ b/packages/@ourworldindata/grapher/src/selection/SelectionArray.ts @@ -15,15 +15,11 @@ export class SelectionArray { ) { this.selectedEntityNames = selectedEntityNames.slice() this.availableEntities = availableEntities.slice() - - this.focusedEntityNames = [] } @observable selectedEntityNames: EntityName[] @observable private availableEntities: Entity[] - @observable focusedEntityNames: EntityName[] - @computed get availableEntityNames(): string[] { return this.availableEntities.map((entity) => entity.entityName) } @@ -107,33 +103,6 @@ export class SelectionArray { return this } - @computed get focusedEntityNameSet(): Set { - return new Set(this.focusedEntityNames) - } - - @computed get hoveredEntityNameSet(): Set { - return new Set(this.focusedEntityNames) - } - - @action.bound focusEntity(entityName: EntityName): this { - if (!this.focusedEntityNameSet.has(entityName)) - this.focusedEntityNames.push(entityName) - return this - } - - @action.bound unfocusEntity(entityName: EntityName): this { - this.focusedEntityNames = this.focusedEntityNames.filter( - (name) => name !== entityName - ) - return this - } - - @action.bound toggleFocus(entityName: EntityName): this { - return this.focusedEntityNameSet.has(entityName) - ? this.unfocusEntity(entityName) - : this.focusEntity(entityName) - } - // Mainly for testing @action.bound selectSample(howMany = 1): this { return this.setSelectedEntities( diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 7b23a461d3..5c29720ccd 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -80,6 +80,7 @@ import { } from "../lineCharts/LineChartHelpers" import { HorizontalColorLegendManager } from "../horizontalColorLegend/HorizontalColorLegends" import { CategoricalBin } from "../color/ColorScaleBin" +import { InteractionArray } from "../selection/InteractionArray" type SVGMouseOrTouchEvent = | React.MouseEvent @@ -212,8 +213,12 @@ export class SlopeChart return makeSelectionArray(this.manager.selection) } + @computed get interactionArray(): InteractionArray { + return this.manager.interactionArray ?? new InteractionArray() + } + @computed private get focusedSeriesNameSet(): Set { - return this.selectionArray.focusedEntityNameSet + return this.interactionArray.focusedEntityNameSet } @computed private get formatColumn() { @@ -844,7 +849,7 @@ export class SlopeChart } @action.bound onLineLegendClick(seriesName: SeriesName): void { - this.selectionArray.toggleFocus(seriesName) + this.interactionArray.toggleFocus(seriesName) } private hoverTimer?: NodeJS.Timeout diff --git a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts index 1d73e6a647..cb146f77cc 100644 --- a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts +++ b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts @@ -580,6 +580,7 @@ export interface GrapherInterface extends SortConfig { includedEntities?: number[] selectedEntityNames?: EntityName[] selectedEntityColors?: { [entityName: string]: string | undefined } + focusedSeriesNames?: SeriesName[] missingDataStrategy?: MissingDataStrategy hideFacetControl?: boolean facettingLabelByYVariables?: string @@ -698,6 +699,7 @@ export const grapherKeysToSerialize = [ "dimensions", "selectedEntityNames", "selectedEntityColors", + "focusedSeriesNames", "sortBy", "sortOrder", "sortColumnSlug",