Skip to content

Commit

Permalink
🎉 (line+slope) add focus state
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Dec 9, 2024
1 parent 3ead566 commit 1921177
Show file tree
Hide file tree
Showing 16 changed files with 239 additions and 75 deletions.
3 changes: 3 additions & 0 deletions packages/@ourworldindata/grapher/src/chart/ChartManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { SelectionArray } from "../selection/SelectionArray"
import { ColumnSlug, SortConfig, TimeBound } from "@ourworldindata/utils"
import { ColorScale } from "../color/ColorScale"
import { ColorScaleBin } from "../color/ColorScaleBin"
import { InteractionArray } from "../selection/InteractionArray"

// The possible options common across our chart types. Not all of these apply to every chart type, so there is room to create a better type hierarchy.

Expand Down Expand Up @@ -97,4 +98,6 @@ export interface ChartManager {

detailsOrderedByReference?: string[]
detailsMarkerInSvg?: DetailsMarker

focusArray?: InteractionArray
}
26 changes: 17 additions & 9 deletions packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,6 @@ export function findValidChartTypeCombination(
return undefined
}

/** Useful for sorting series by their interaction state */
export function byInteractionState(series: {
hover: InteractionState
}): number {
if (series.hover.background) return 1
if (series.hover.active) return 3
return 2
}

export function findSeriesNamesContainedInBin(
series: readonly ChartSeries[],
bin: ColorScaleBin
Expand All @@ -210,3 +201,20 @@ export function findSeriesNamesContainedInBin(
.map(({ seriesName }) => seriesName)
.filter((seriesName) => bin.contains(seriesName))
}

/** Useful for sorting series by their interaction state */
export function byInteractionState(state: InteractionState): number {
if (state.background) return 1
if (state.active) return 3
return 2
}

/** Useful for sorting series by their interaction state */
export function byHoverThenFocusState(series: {
hover: InteractionState
focus: InteractionState
}): number {
const hoverScore = byInteractionState(series.hover)
const focusScore = byInteractionState(series.focus)
return hoverScore * 10 + focusScore
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,17 @@ export const migrateSelectedEntityNamesParam: UrlMigration = (
* Accessors
*/

export const getSelectedEntityNamesFromQueryParam = (
queryParam: string
): EntityName[] => entityNamesFromV2Param(queryParam).map(codeToEntityName)

export const getSelectedEntityNamesParam = (
url: Url
): EntityName[] | undefined => {
// Expects an already-migrated URL as input
const { country } = url.queryParams
return country !== undefined
? entityNamesFromV2Param(country).map(codeToEntityName)
? getSelectedEntityNamesFromQueryParam(country)
: undefined
}

Expand Down
49 changes: 48 additions & 1 deletion packages/@ourworldindata/grapher/src/core/Grapher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ import {
GRAPHER_TAB_NAMES,
GRAPHER_TAB_QUERY_PARAMS,
GrapherTabOption,
SeriesName,
} from "@ourworldindata/types"
import {
BlankOwidTable,
Expand Down Expand Up @@ -144,7 +145,10 @@ import {
import { TooltipManager } from "../tooltip/TooltipProps"

import { DimensionSlot } from "../chart/DimensionSlot"
import { getSelectedEntityNamesParam } from "./EntityUrlBuilder"
import {
getSelectedEntityNamesParam,
getSelectedEntityNamesFromQueryParam,
} from "./EntityUrlBuilder"
import { AxisConfig, AxisManager } from "../axis/AxisConfig"
import { ColorScaleConfig } from "../color/ColorScaleConfig"
import { MapConfig } from "../mapCharts/MapConfig"
Expand Down Expand Up @@ -440,6 +444,7 @@ export class Grapher
are evaluated afterwards and can still remove entities even if they were included before.
*/
@observable includedEntities?: number[] = undefined
@observable focusedSeriesNames?: SeriesName[] = undefined
@observable comparisonLines?: ComparisonLineConfig[] = undefined // todo: Persistables?
@observable relatedQuestions?: RelatedQuestionsConfig[] = undefined // todo: Persistables?

Expand Down Expand Up @@ -566,6 +571,7 @@ export class Grapher
)

obj.selectedEntityNames = this.selection.selectedEntityNames
obj.focusedSeriesNames = this.focusArray.activeSeriesNames

deleteRuntimeAndUnchangedProps(obj, defaultObject)

Expand Down Expand Up @@ -607,6 +613,9 @@ export class Grapher
if (obj.selectedEntityNames)
this.selection.setSelectedEntities(obj.selectedEntityNames)

if (obj.focusedSeriesNames)
this.focusArray.clearAllAndActivate(...obj.focusedSeriesNames)

// JSON doesn't support Infinity, so we use strings instead.
this.minTime = minTimeBoundFromJSONOrNegativeInfinity(obj.minTime)
this.maxTime = maxTimeBoundFromJSONOrPositiveInfinity(obj.maxTime)
Expand Down Expand Up @@ -683,6 +692,14 @@ export class Grapher
if (this.addCountryMode !== EntitySelectionMode.Disabled && selection)
this.selection.setSelectedEntities(selection)

const focusedSeriesNames =
params.focus !== undefined
? getSelectedEntityNamesFromQueryParam(params.focus)
: undefined
if (focusedSeriesNames) {
this.focusArray.clearAllAndActivate(...focusedSeriesNames)
}

// faceting
if (params.facet && params.facet in FacetStrategy) {
this.selectedFacetStrategy = params.facet as FacetStrategy
Expand Down Expand Up @@ -2538,6 +2555,8 @@ export class Grapher
this.props.table?.availableEntities ?? []
)

focusArray = new InteractionArray()

@computed get availableEntities(): Entity[] {
return this.tableForSelection.availableEntities
}
Expand Down Expand Up @@ -3181,6 +3200,10 @@ export class Grapher
)
}
}
),
reaction(
() => this.facetStrategy,
() => this.focusArray.clear()
)
)
if (this.props.bindUrlToWindow) this.bindToWindow()
Expand Down Expand Up @@ -3385,6 +3408,30 @@ export class Grapher
return undefined
}

@computed get focusedSeriesNamesIfDifferentThanAuthors():
| SeriesName[]
| undefined {
const authoredConfig = this.legacyConfigAsAuthored

const originalFocusedSeriesNames =
authoredConfig.focusedSeriesNames ?? []
const currentFocusedSeriesNames = this.focusArray.activeSeriesNames

const seriesNamesThatTheUserDeselected = difference(
currentFocusedSeriesNames,
originalFocusedSeriesNames
)

if (
currentFocusedSeriesNames.length !==
originalFocusedSeriesNames.length ||
seriesNamesThatTheUserDeselected.length
)
return this.focusArray.activeSeriesNames

return undefined
}

// Autocomputed url params to reflect difference between current grapher state
// and original config state
@computed.struct get changedParams(): Partial<GrapherQueryParams> {
Expand Down
8 changes: 8 additions & 0 deletions packages/@ourworldindata/grapher/src/core/GrapherUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export const grapherConfigToQueryParams = (
country: config.selectedEntityNames
? generateSelectedEntityNamesParam(config.selectedEntityNames)
: undefined,
focus: config.focusedSeriesNames
? generateSelectedEntityNamesParam(config.focusedSeriesNames)
: undefined,

// These cannot be specified in config, so we always set them to undefined
showSelectionOnlyInTable: undefined,
Expand Down Expand Up @@ -97,6 +100,11 @@ export const grapherObjectToQueryParams = (
grapher.selectedEntitiesIfDifferentThanAuthors
)
: undefined,
focus: grapher.focusedSeriesNamesIfDifferentThanAuthors
? generateSelectedEntityNamesParam(
grapher.focusedSeriesNamesIfDifferentThanAuthors
)
: undefined,
}
return params
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { ColumnSlug, OwidColumnDef } from "@ourworldindata/types"
import { buildVariableTable } from "../core/LegacyToOwidTable"
import { loadVariableDataAndMetadata } from "../core/loadVariable"
import { DrawerContext } from "../slideInDrawer/SlideInDrawer.js"
import { InteractionArray } from "../selection/InteractionArray"

export interface EntitySelectorState {
searchInput: string
Expand All @@ -70,6 +71,7 @@ export interface EntitySelectorManager {
isEntitySelectorModalOrDrawerOpen?: boolean
canChangeEntity?: boolean
canHighlightEntities?: boolean
focusArray?: InteractionArray
}

interface SortConfig {
Expand Down Expand Up @@ -603,7 +605,10 @@ export class EntitySelector extends React.Component<{
this.selectionArray.setSelectedEntities([entityName])
}

this.clearSearchInput()
// remove focus from an entity that has been removed from the selection
if (!this.selectionArray.isSelected(entityName)) {
this.manager.focusArray?.deactivate(entityName)
}

// close the modal or drawer automatically after selection if in single mode
if (
Expand Down
12 changes: 12 additions & 0 deletions packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ export class FacetChart
const manager = {
...series.manager,
useValueBasedColorScheme,
focusArray: this.manager.focusArray,
externalLegendHoverBin: this.legendHoverBin,
xAxisConfig: {
// For now, sharing an x axis means hiding the tick labels of inner facets.
Expand Down Expand Up @@ -775,6 +776,17 @@ export class FacetChart
this.legendHoverBin = undefined
}

@action.bound onLegendClick(bin: ColorScaleBin): void {
const seriesNames = uniq(
this.intermediateChartInstances.flatMap((chartInstance) =>
chartInstance.series.map((s) => s.seriesName)
)
)
this.manager.focusArray?.toggle(
...seriesNames.filter((seriesName) => bin.contains(seriesName))
)
}

// end of legend props

@computed private get legend(): HorizontalColorLegend {
Expand Down
Loading

0 comments on commit 1921177

Please sign in to comment.