Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ee213a9
🎉 (line+slope) add focus state
sophiamersmann Dec 9, 2024
a6271a3
🎉 (admin) edit focused entities/columns
sophiamersmann Dec 9, 2024
3ad39ba
✨ (line) make labels of focused entities bold
sophiamersmann Dec 10, 2024
4fa747a
🐛 dismiss focused entities when selection is cleared
sophiamersmann Dec 10, 2024
6ac77bf
🔨 rename InteractionArray to FocusArray
sophiamersmann Dec 11, 2024
ea5337e
🐛 ensure series names are shared between line and slope charts
sophiamersmann Dec 12, 2024
add2888
🔨 move focus array to its own folder
sophiamersmann Dec 12, 2024
74d35bb
🧪 add focus array tests
sophiamersmann Dec 12, 2024
dbd72b2
🧪 fix Grapher tests
sophiamersmann Dec 12, 2024
2433cd8
✨ ensure the focused series is labelled
sophiamersmann Dec 12, 2024
42e4423
✨ (line+slope) hide outlines for background series
sophiamersmann Dec 12, 2024
52305dd
🐛 gray out non-focused multi-color lines
sophiamersmann Dec 12, 2024
9166f1d
🔨 refactor url parsing/serialising
sophiamersmann Dec 12, 2024
1c4c1ce
✨ persist focused entities for chart views
sophiamersmann Dec 12, 2024
6fd141b
🔨 clean up
sophiamersmann Dec 13, 2024
46c1df3
🐛 fix order of render series
sophiamersmann Dec 13, 2024
a417323
✨ (admin) improve support for focusing series
sophiamersmann Dec 13, 2024
aa78cc9
✨ make unfocused lines a little thinner
sophiamersmann Dec 13, 2024
24bc734
🔨 clean up
sophiamersmann Dec 13, 2024
d76793f
💄 add comment
sophiamersmann Dec 17, 2024
cf0c5c2
🔨 exclude tables from faceting
sophiamersmann Dec 17, 2024
0ba95df
✨ (slope) focus series by clicking on labels on the left
sophiamersmann Dec 17, 2024
dc9a69a
🐛 (admin) only evaluate chart series names once Grapher is ready
sophiamersmann Dec 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions adminSiteClient/AbstractChartEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
diffGrapherConfigs,
mergeGrapherConfigs,
PostReference,
SeriesName,
difference,
} from "@ourworldindata/utils"
import { action, computed, observable, when } from "mobx"
import { EditorFeatures } from "./EditorFeatures.js"
Expand Down Expand Up @@ -163,6 +165,24 @@ export abstract class AbstractChartEditor<
return Object.hasOwn(this.activeParentConfig, property)
}

@computed get invalidFocusedSeriesNames(): SeriesName[] {
const { grapher } = this

// if focusing is not supported, then all focused series are invalid
if (!this.features.canHighlightSeries) {
return grapher.focusArray.seriesNames
}

// find invalid focused series
const availableSeriesNames = grapher.chartSeriesNames
const focusedSeriesNames = grapher.focusArray.seriesNames
return difference(focusedSeriesNames, availableSeriesNames)
}

@action.bound removeInvalidFocusedSeriesNames(): void {
this.grapher.focusArray.remove(...this.invalidFocusedSeriesNames)
}

abstract get isNewGrapher(): boolean
abstract get availableTabs(): EditorTab[]

Expand Down
11 changes: 3 additions & 8 deletions adminSiteClient/ChartEditorTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,8 @@ export type FieldWithDetailReferences =
| "axisLabelX"
| "axisLabelY"

export interface DimensionErrorMessage {
displayName?: string
}
type ErrorMessageFieldName = FieldWithDetailReferences | "focusedSeriesNames"

export type ErrorMessages = Partial<Record<FieldWithDetailReferences, string>>
export type ErrorMessages = Partial<Record<ErrorMessageFieldName, string>>

export type ErrorMessagesForDimensions = Record<
DimensionProperty,
DimensionErrorMessage[]
>
export type ErrorMessagesForDimensions = Record<DimensionProperty, string[]>
13 changes: 10 additions & 3 deletions adminSiteClient/ChartEditorView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,14 @@ export class ChartEditorView<
}
)

// add an error message if any focused series names are invalid
const { invalidFocusedSeriesNames = [] } = this.editor ?? {}
if (invalidFocusedSeriesNames.length > 0) {
const invalidNames = invalidFocusedSeriesNames.join(", ")
const message = `Invalid focus state. The following entities/indicators are not plotted: ${invalidNames}`
errorMessages.focusedSeriesNames = message
}

return errorMessages
}

Expand All @@ -287,9 +295,8 @@ export class ChartEditorView<

// add error message if details are referenced in the display name
if (hasDetailsInDisplayName) {
errorMessages[slot.property][dimensionIndex] = {
displayName: "Detail syntax is not supported",
}
errorMessages[slot.property][dimensionIndex] =
`Detail syntax is not supported for display names of indicators: ${dimension.display.name}`
}
})
})
Expand Down
5 changes: 2 additions & 3 deletions adminSiteClient/DimensionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { observer } from "mobx-react"
import { ChartDimension } from "@ourworldindata/grapher"
import { OwidColumnDef, OwidVariableRoundingMode } from "@ourworldindata/types"
import { startCase } from "@ourworldindata/utils"
import { DimensionErrorMessage } from "./ChartEditorTypes.js"
import {
Toggle,
BindAutoString,
Expand Down Expand Up @@ -35,7 +34,7 @@ export class DimensionCard<
onChange: (dimension: ChartDimension) => void
onEdit?: () => void
onRemove?: () => void
errorMessage?: DimensionErrorMessage
errorMessage?: string
}> {
@observable.ref isExpanded: boolean = false

Expand Down Expand Up @@ -171,7 +170,7 @@ export class DimensionCard<
store={dimension.display}
auto={column.displayName}
onBlur={this.onChange}
errorMessage={this.props.errorMessage?.displayName}
errorMessage={this.props.errorMessage}
/>
<BindAutoString
label="Unit of measurement"
Expand Down
16 changes: 13 additions & 3 deletions adminSiteClient/EditorBasicTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ class DimensionSlotView<

@observable.ref isSelectingVariables: boolean = false

private get editor() {
return this.props.editor
}

private get grapher() {
return this.props.editor.grapher
}
Expand Down Expand Up @@ -105,7 +109,7 @@ class DimensionSlotView<
this.updateParentConfig()
}

@action.bound private updateDefaults() {
@action.bound private updateDefaultSelection() {
const { grapher } = this.props.editor
const { selection } = grapher
const { availableEntityNames, availableEntityNameSet } = selection
Expand Down Expand Up @@ -151,11 +155,17 @@ class DimensionSlotView<
this.disposers.push(
reaction(
() => this.grapher.validChartTypes,
this.updateDefaults
() => {
this.updateDefaultSelection()
this.editor.removeInvalidFocusedSeriesNames()
}
),
reaction(
() => this.grapher.yColumnsFromDimensions.length,
this.updateDefaults
() => {
this.updateDefaultSelection()
this.editor.removeInvalidFocusedSeriesNames()
}
)
)
}
Expand Down
162 changes: 155 additions & 7 deletions adminSiteClient/EditorDataTab.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import React from "react"
import { moveArrayItemToIndex, omit } from "@ourworldindata/utils"
import {
differenceOfSets,
moveArrayItemToIndex,
omit,
sortBy,
} from "@ourworldindata/utils"
import { computed, action, observable } from "mobx"
import { observer } from "mobx-react"
import cx from "classnames"
import {
EntitySelectionMode,
MissingDataStrategy,
EntityName,
SeriesName,
} from "@ourworldindata/types"
import { Grapher } from "@ourworldindata/grapher"
import { ColorBox, SelectField, Section, FieldsRow } from "./Forms.js"
Expand All @@ -24,14 +31,20 @@ import {
} from "react-beautiful-dnd"
import { AbstractChartEditor } from "./AbstractChartEditor.js"

interface EntityItemProps extends React.HTMLProps<HTMLDivElement> {
interface EntityListItemProps extends React.HTMLProps<HTMLDivElement> {
grapher: Grapher
entityName: EntityName
onRemove?: () => void
}

interface SeriesListItemProps extends React.HTMLProps<HTMLDivElement> {
seriesName: SeriesName
isValid?: boolean
onRemove?: () => void
}

@observer
class EntityItem extends React.Component<EntityItemProps> {
class EntityListItem extends React.Component<EntityListItemProps> {
@observable.ref isChoosingColor: boolean = false

@computed get table() {
Expand Down Expand Up @@ -89,7 +102,36 @@ class EntityItem extends React.Component<EntityItemProps> {
}

@observer
export class KeysSection extends React.Component<{
class SeriesListItem extends React.Component<SeriesListItemProps> {
@action.bound onRemove() {
this.props.onRemove?.()
}

render() {
const { props } = this
const { seriesName, isValid } = props
const rest = omit(props, ["seriesName", "isValid", "onRemove"])

const className = cx("ListItem", "list-group-item", {
invalid: !isValid,
})
const annotation = !isValid ? "(not plotted)" : ""

return (
<div className={className} key={seriesName} {...rest}>
<div>
{seriesName} {annotation}
</div>
<div className="clickable" onClick={this.onRemove}>
<FontAwesomeIcon icon={faTimes} />
</div>
</div>
)
}
}

@observer
export class EntitySelectionSection extends React.Component<{
editor: AbstractChartEditor
}> {
@observable.ref dragKey?: EntityName
Expand All @@ -100,6 +142,12 @@ export class KeysSection extends React.Component<{

@action.bound onAddKey(entityName: EntityName) {
this.editor.grapher.selection.selectEntity(entityName)
this.editor.removeInvalidFocusedSeriesNames()
}

@action.bound onRemoveKey(entityName: EntityName) {
this.editor.grapher.selection.deselectEntity(entityName)
this.editor.removeInvalidFocusedSeriesNames()
}

@action.bound onDragEnd(result: DropResult) {
Expand All @@ -122,6 +170,7 @@ export class KeysSection extends React.Component<{
grapher.selection.setSelectedEntities(
activeParentConfig.selectedEntityNames
)
this.editor.removeInvalidFocusedSeriesNames()
}

render() {
Expand Down Expand Up @@ -183,12 +232,12 @@ export class KeysSection extends React.Component<{
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<EntityItem
<EntityListItem
key={entityName}
grapher={grapher}
entityName={entityName}
onRemove={() =>
selection.deselectEntity(
this.onRemoveKey(
entityName
)
}
Expand Down Expand Up @@ -216,6 +265,102 @@ export class KeysSection extends React.Component<{
}
}

@observer
export class FocusSection extends React.Component<{
editor: AbstractChartEditor
}> {
@computed get editor() {
return this.props.editor
}

@action.bound addToFocusedSeries(seriesName: SeriesName) {
this.editor.grapher.focusArray.add(seriesName)
}

@action.bound removeFromFocusedSeries(seriesName: SeriesName) {
this.editor.grapher.focusArray.remove(seriesName)
}

@action.bound setFocusedSeriesNamesToParentValue() {
const { grapher, activeParentConfig } = this.editor
if (!activeParentConfig || !activeParentConfig.focusedSeriesNames)
return
grapher.focusArray.clearAllAndAdd(
...activeParentConfig.focusedSeriesNames
)
this.editor.removeInvalidFocusedSeriesNames()
}

render() {
const { editor } = this
const { grapher } = editor

const isFocusInherited =
editor.isPropertyInherited("focusedSeriesNames")

const focusedSeriesNameSet = grapher.focusArray.seriesNameSet
const focusedSeriesNames = grapher.focusArray.seriesNames

// series available to highlight are those that are currently plotted
const seriesNameSet = new Set(grapher.chartSeriesNames)
const availableSeriesNameSet = differenceOfSets([
seriesNameSet,
focusedSeriesNameSet,
])

// focusing only makes sense for two or more plotted series
if (focusedSeriesNameSet.size === 0 && availableSeriesNameSet.size < 2)
return null

const availableSeriesNames: SeriesName[] = sortBy(
Array.from(availableSeriesNameSet)
)

const invalidFocusedSeriesNames = differenceOfSets([
focusedSeriesNameSet,
seriesNameSet,
])

return (
<Section name="Data to highlight">
<FieldsRow>
<SelectField
onValue={this.addToFocusedSeries}
value="Select data"
options={["Select data"]
.concat(availableSeriesNames)
.map((key) => ({ value: key }))}
/>
{editor.couldPropertyBeInherited("focusedSeriesNames") && (
<button
className="btn btn-outline-secondary"
type="button"
style={{ maxWidth: "min-content" }}
title="Reset to parent focus"
onClick={this.setFocusedSeriesNamesToParentValue}
disabled={isFocusInherited}
>
<FontAwesomeIcon
icon={isFocusInherited ? faLink : faUnlink}
/>
</button>
)}
</FieldsRow>
{focusedSeriesNames.map((seriesName) => (
<SeriesListItem
key={seriesName}
seriesName={seriesName}
isValid={!invalidFocusedSeriesNames.has(seriesName)}
onRemove={() =>
this.removeFromFocusedSeries(seriesName)
}
/>
))}
</Section>
)
}
}

@observer
class MissingDataSection<
Editor extends AbstractChartEditor,
Expand Down Expand Up @@ -331,7 +476,10 @@ export class EditorDataTab<
</label>
</div>
</Section>
<KeysSection editor={editor} />
<EntitySelectionSection editor={editor} />
{features.canHighlightSeries && (
<FocusSection editor={editor} />
)}
{features.canSpecifyMissingDataStrategy && (
<MissingDataSection editor={this.props.editor} />
)}
Expand Down
1 change: 1 addition & 0 deletions adminSiteClient/EditorExportTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export class EditorExportTab<
staticFormat: format,
selectedEntityNames:
this.grapher.selection.selectedEntityNames,
focusedSeriesNames: this.grapher.focusedSeriesNames,
isSocialMediaExport,
})
}
Expand Down
7 changes: 7 additions & 0 deletions adminSiteClient/EditorFeatures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,11 @@ export class EditorFeatures {
)
)
}

@computed get canHighlightSeries() {
return (
(this.grapher.hasLineChart || this.grapher.hasSlopeChart) &&
this.grapher.isOnChartTab
)
}
}
Loading
Loading