Skip to content

Commit

Permalink
🎉 (grapher) make charts inherit indicator-level settings (#3793)
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann authored Sep 5, 2024
1 parent c33fc73 commit 0a61d87
Show file tree
Hide file tree
Showing 58 changed files with 4,148 additions and 1,364 deletions.
148 changes: 148 additions & 0 deletions adminSiteClient/AbstractChartEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import {
isEqual,
omit,
GrapherInterface,
diffGrapherConfigs,
mergeGrapherConfigs,
} from "@ourworldindata/utils"
import { action, computed, observable, when } from "mobx"
import { EditorFeatures } from "./EditorFeatures.js"
import { Admin } from "./Admin.js"
import { defaultGrapherConfig, Grapher } from "@ourworldindata/grapher"

export type EditorTab =
| "basic"
| "data"
| "text"
| "customize"
| "map"
| "scatter"
| "marimekko"
| "revisions"
| "refs"
| "export"
| "inheritance"
| "debug"

export interface AbstractChartEditorManager {
admin: Admin
patchConfig: GrapherInterface
parentConfig?: GrapherInterface
isInheritanceEnabled?: boolean
}

export abstract class AbstractChartEditor<
Manager extends AbstractChartEditorManager = AbstractChartEditorManager,
> {
manager: Manager

@observable.ref grapher = new Grapher()
@observable.ref currentRequest: Promise<any> | undefined // Whether the current chart state is saved or not
@observable.ref tab: EditorTab = "basic"
@observable.ref errorMessage?: { title: string; content: string }
@observable.ref previewMode: "mobile" | "desktop"
@observable.ref showStaticPreview = false
@observable.ref savedPatchConfig: GrapherInterface = {}

// parent config derived from the current chart config
@observable.ref parentConfig: GrapherInterface | undefined = undefined
// if inheritance is enabled, the parent config is applied to grapher
@observable.ref isInheritanceEnabled: boolean | undefined = undefined

constructor(props: { manager: Manager }) {
this.manager = props.manager
this.previewMode =
localStorage.getItem("editorPreviewMode") === "mobile"
? "mobile"
: "desktop"

when(
() => this.manager.parentConfig !== undefined,
() => (this.parentConfig = this.manager.parentConfig)
)

when(
() => this.manager.isInheritanceEnabled !== undefined,
() =>
(this.isInheritanceEnabled = this.manager.isInheritanceEnabled)
)

when(
() => this.grapher.hasData && this.grapher.isReady,
() => (this.savedPatchConfig = this.patchConfig)
)
}

/** original grapher config used to init the grapher instance */
@computed get originalGrapherConfig(): GrapherInterface {
const { patchConfig, parentConfig, isInheritanceEnabled } = this.manager
if (!isInheritanceEnabled) return patchConfig
return mergeGrapherConfigs(parentConfig ?? {}, patchConfig)
}

/** live-updating full config */
@computed get fullConfig(): GrapherInterface {
return mergeGrapherConfigs(defaultGrapherConfig, this.grapher.object)
}

/** parent config currently applied to grapher */
@computed get activeParentConfig(): GrapherInterface | undefined {
return this.isInheritanceEnabled ? this.parentConfig : undefined
}

@computed get activeParentConfigWithDefaults():
| GrapherInterface
| undefined {
if (!this.activeParentConfig) return undefined
return mergeGrapherConfigs(
defaultGrapherConfig,
this.activeParentConfig
)
}

/** patch config of the chart that is written to the db on save */
@computed get patchConfig(): GrapherInterface {
return diffGrapherConfigs(
this.fullConfig,
this.activeParentConfigWithDefaults ?? defaultGrapherConfig
)
}

@computed get isModified(): boolean {
return !isEqual(
omit(this.patchConfig, "version"),
omit(this.savedPatchConfig, "version")
)
}

@computed get features(): EditorFeatures {
return new EditorFeatures(this)
}

@action.bound updateLiveGrapher(config: GrapherInterface): void {
this.grapher.reset()
this.grapher.updateFromObject(config)
this.grapher.updateAuthoredVersion(config)
}

// only works for top-level properties
isPropertyInherited(property: keyof GrapherInterface): boolean {
if (!this.isInheritanceEnabled || !this.activeParentConfigWithDefaults)
return false
return (
!Object.hasOwn(this.patchConfig, property) &&
Object.hasOwn(this.activeParentConfigWithDefaults, property)
)
}

// only works for top-level properties
couldPropertyBeInherited(property: keyof GrapherInterface): boolean {
if (!this.isInheritanceEnabled || !this.activeParentConfig) return false
return Object.hasOwn(this.activeParentConfig, property)
}

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

abstract saveGrapher(props?: { onError?: () => void }): Promise<void>
}
12 changes: 12 additions & 0 deletions adminSiteClient/AdminApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { BulkGrapherConfigEditorPage } from "./BulkGrapherConfigEditor.js"
import { GdocsIndexPage, GdocsMatchProps } from "./GdocsIndexPage.js"
import { GdocsPreviewPage } from "./GdocsPreviewPage.js"
import { GdocsStoreProvider } from "./GdocsStore.js"
import { IndicatorChartEditorPage } from "./IndicatorChartEditorPage.js"

@observer
class AdminErrorMessage extends React.Component<{ admin: Admin }> {
Expand Down Expand Up @@ -200,6 +201,17 @@ export class AdminApp extends React.Component<{
path="/users"
component={UsersIndexPage}
/>
<Route
exact
path="/variables/:variableId/config"
render={({ match }) => (
<IndicatorChartEditorPage
variableId={parseInt(
match.params.variableId
)}
/>
)}
/>
<Route
exact
path="/variables/:variableId"
Expand Down
Loading

0 comments on commit 0a61d87

Please sign in to comment.