From 5fa630a2d127222f8cecb91c18cdb971a18227fb Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Wed, 18 Sep 2024 18:13:17 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/ChartEditorPage.tsx | 4 + adminSiteClient/EditorCustomizeTab.tsx | 170 ++++++++++++------ adminSiteClient/EditorTextTab.tsx | 26 ++- adminSiteClient/Forms.tsx | 86 ++++++++- adminSiteClient/admin.scss | 29 +++ .../grapher/src/core/Grapher.tsx | 11 ++ .../src/schema/defaultGrapherConfig.ts | 5 + .../src/schema/grapher-schema.005.yaml | 14 +- .../types/src/grapherTypes/GrapherTypes.ts | 4 +- vite.config-common.mts | 3 + 10 files changed, 273 insertions(+), 79 deletions(-) diff --git a/adminSiteClient/ChartEditorPage.tsx b/adminSiteClient/ChartEditorPage.tsx index b0c1baa3331..b9d972cf64a 100644 --- a/adminSiteClient/ChartEditorPage.tsx +++ b/adminSiteClient/ChartEditorPage.tsx @@ -56,6 +56,10 @@ export class ChartEditorPage `/api/charts/${grapherId}.parent.json` ) this.parentConfig = parent?.config + if (this.parentConfig) { + this.parentConfig.timelineMinTime = 2000 + this.parentConfig.subtitle = "parent subtitle" + } this.isInheritanceEnabled = parent?.isActive ?? true } else if (grapherConfig) { const parentIndicatorId = diff --git a/adminSiteClient/EditorCustomizeTab.tsx b/adminSiteClient/EditorCustomizeTab.tsx index 454255fb1ea..2e2c6d218db 100644 --- a/adminSiteClient/EditorCustomizeTab.tsx +++ b/adminSiteClient/EditorCustomizeTab.tsx @@ -6,6 +6,7 @@ import { ColorSchemeName, FacetAxisDomain, FacetStrategy, + GrapherInterface, } from "@ourworldindata/types" import { Grapher } from "@ourworldindata/grapher" import { @@ -17,6 +18,9 @@ import { TextField, Button, RadioGroup, + BindAutoFloat, + InputWithAction, + BindAutoFloatExt, } from "./Forms.js" import { debounce, @@ -39,6 +43,84 @@ import Select from "react-select" import { AbstractChartEditor } from "./AbstractChartEditor.js" import { ErrorMessages } from "./ChartEditorTypes.js" +@observer +class TimeField< + T extends { [field: string]: any }, + K extends Extract, +> extends React.Component<{ + editor: AbstractChartEditor + field: K + store: T + label: string + defaultValue: number + defaultTextValue: TimeBoundValue +}> { + @action.bound onChange(value: number | undefined) { + this.props.store[this.props.field] = + value ?? (this.props.defaultValue as any) + } + + render() { + const { editor, field, label, defaultTextValue, defaultValue } = + this.props + + const inheritedValue = editor.activeParentConfig?.[ + field as keyof GrapherInterface + ] as number + + console.log("inherited", inheritedValue) + + console.log( + "auto", + inheritedValue === defaultTextValue + ? defaultValue + : inheritedValue ?? defaultValue + ) + + return ( + store[field]} + writeFn={(store, newVal) => + (store[field] = newVal as any) + } + readAutoFn={() => + inheritedValue === defaultTextValue + ? defaultValue + : inheritedValue ?? defaultValue + } + isAuto={editor.isPropertyInherited( + field as keyof GrapherInterface + )} + store={this.props.store} + /> + ) : ( + + ) + } + text={{ + default: "Bind to data", + disabled: "Bound to data", + }} + onClick={() => { + this.props.store[this.props.field] = defaultValue as any + }} + isDisabled={this.props.store[this.props.field] === defaultValue} + /> + ) + } +} + @observer export class ColorSchemeSelector extends React.Component<{ grapher: Grapher @@ -318,13 +400,6 @@ class TimelineSection< return this.grapher.maxTime } - @computed get timelineMinTime() { - return this.grapher.timelineMinTime - } - @computed get timelineMaxTime() { - return this.grapher.timelineMaxTime - } - @action.bound onMinTime(value: number | undefined) { this.grapher.minTime = value ?? TimeBoundValue.negativeInfinity } @@ -333,28 +408,6 @@ class TimelineSection< this.grapher.maxTime = value ?? TimeBoundValue.positiveInfinity } - @action.bound onTimelineMinTime(value: number | undefined) { - this.grapher.timelineMinTime = value - } - - @action.bound onBlurTimelineMinTime() { - if (this.grapher.timelineMinTime === undefined) { - this.grapher.timelineMinTime = - this.activeParentConfig?.timelineMinTime - } - } - - @action.bound onTimelineMaxTime(value: number | undefined) { - this.grapher.timelineMaxTime = value - } - - @action.bound onBlurTimelineMaxTime() { - if (this.grapher.timelineMaxTime === undefined) { - this.grapher.timelineMaxTime = - this.activeParentConfig?.timelineMaxTime - } - } - @action.bound onToggleHideTimeline(value: boolean) { this.grapher.hideTimeline = value || undefined } @@ -364,55 +417,54 @@ class TimelineSection< } render() { - const { features } = this.props.editor + const { editor } = this.props + const { features } = editor const { grapher } = this return (
- + {/* {features.timeDomain && ( - )} - - + */} {features.timelineRange && ( - - + defaultValue={Infinity} + defaultTextValue={TimeBoundValue.positiveInfinity} + /> */} )} diff --git a/adminSiteClient/EditorTextTab.tsx b/adminSiteClient/EditorTextTab.tsx index 7112e320ba6..01cc6624296 100644 --- a/adminSiteClient/EditorTextTab.tsx +++ b/adminSiteClient/EditorTextTab.tsx @@ -93,11 +93,9 @@ export class EditorTextTab<
grapher.displayTitle} - writeFn={({ grapher }, newVal) => - (grapher.title = newVal) - } - readAutoFn={({ editor }) => + readFn={(grapher) => grapher.displayTitle} + writeFn={(grapher, newVal) => (grapher.title = newVal)} + readAutoFn={() => editor.couldPropertyBeInherited("title") ? editor.activeParentConfig!.title : undefined @@ -106,7 +104,7 @@ export class EditorTextTab< editor.isPropertyInherited("title") || grapher.title === undefined } - store={{ grapher, editor }} + store={grapher} softCharacterLimit={100} /> {features.showEntityAnnotationInTitleToggle && ( @@ -156,11 +154,11 @@ export class EditorTextTab< /> grapher.currentSubtitle} - writeFn={({ grapher }, newVal) => + readFn={(grapher) => grapher.currentSubtitle} + writeFn={(grapher, newVal) => (grapher.subtitle = newVal) } - readAutoFn={({ editor }) => + readAutoFn={() => editor.couldPropertyBeInherited("subtitle") ? editor.activeParentConfig!.subtitle : undefined @@ -169,7 +167,7 @@ export class EditorTextTab< editor.isPropertyInherited("subtitle") || grapher.subtitle === undefined } - store={{ grapher, editor }} + store={grapher} placeholder="Briefly describe the context of the data. It's best to avoid duplicating any information which can be easily inferred from other visual elements of the chart." textarea softCharacterLimit={280} @@ -192,11 +190,11 @@ export class EditorTextTab<
grapher.sourcesLine} - writeFn={({ grapher }, newVal) => + readFn={(grapher) => grapher.sourcesLine} + writeFn={(grapher, newVal) => (grapher.sourceDesc = newVal) } - readAutoFn={({ editor }) => + readAutoFn={() => editor.couldPropertyBeInherited("sourceDesc") ? editor.activeParentConfig!.sourceDesc : undefined @@ -205,7 +203,7 @@ export class EditorTextTab< editor.isPropertyInherited("sourceDesc") || grapher.sourceDesc === undefined } - store={{ grapher, editor }} + store={grapher} helpText="Short comma-separated list of source names" softCharacterLimit={60} /> diff --git a/adminSiteClient/Forms.tsx b/adminSiteClient/Forms.tsx index c2e575045fe..6bee48c37f4 100644 --- a/adminSiteClient/Forms.tsx +++ b/adminSiteClient/Forms.tsx @@ -46,6 +46,7 @@ interface TextFieldProps extends React.HTMLAttributes { softCharacterLimit?: number errorMessage?: string buttonContent?: React.ReactNode + buttonDisabled?: boolean } export class TextField extends React.Component { @@ -128,6 +129,7 @@ export class TextField extends React.Component { onClick={() => props.onButtonClick && props.onButtonClick() } + disabled={props.buttonDisabled} > {props.buttonContent} @@ -251,6 +253,7 @@ interface NumberFieldProps { helpText?: string buttonContent?: React.ReactNode onButtonClick?: () => void + buttonDisabled?: boolean } interface NumberFieldState { @@ -994,7 +997,9 @@ class AutoFloatField extends React.Component { buttonContent={
{props.isAuto ? ( @@ -1005,6 +1010,7 @@ class AutoFloatField extends React.Component {
} onButtonClick={() => props.onToggleAuto(!props.isAuto)} + buttonDisabled={props.isAuto} /> ) } @@ -1096,6 +1102,76 @@ export class BindAutoFloat< } } +@observer +export class BindAutoFloatExt< + T extends Record, +> extends React.Component< + { + readFn: (x: T) => number + readAutoFn?: (x: T) => number | undefined + writeFn: (x: T, value: number | undefined) => void + store: T + } & Omit +> { + @action.bound onValue(value: number | undefined) { + this.props.writeFn(this.props.store, value) + } + + @action.bound onToggleAuto(value: boolean) { + console.log("value?", value, this.props.readAutoFn?.(this.props.store)) + this.props.writeFn( + this.props.store, + value + ? this.props.readAutoFn?.(this.props.store) + : this.props.readFn(this.props.store) + ) + } + + render() { + const { readFn, readAutoFn, store, ...rest } = this.props + const currentReadValue = this.props.isAuto + ? readAutoFn?.(store) ?? readFn(store) + : readFn(store) + return ( + + ) + } +} + +@observer +export class InputWithAction extends React.Component<{ + input: React.ReactElement + text: string | { default: string; disabled: string } + onClick: () => void + isDisabled: boolean +}> { + render() { + const { input, text, onClick, isDisabled } = this.props + + return ( +
+ {input} + +
+ ) + } +} + @observer export class Modal extends React.Component<{ className?: string @@ -1171,10 +1247,16 @@ export class Timeago extends React.Component<{ export class Button extends React.Component<{ children: any onClick: () => void + className: string + disabled: boolean }> { render() { return ( - ) diff --git a/adminSiteClient/admin.scss b/adminSiteClient/admin.scss index 53c5b0c5636..2dc179bc7a4 100644 --- a/adminSiteClient/admin.scss +++ b/adminSiteClient/admin.scss @@ -647,6 +647,35 @@ $nav-height: 45px; } } +.InputWithAction { + display: flex; + flex-direction: column; + align-items: flex-start; + + .form-group { + margin-bottom: 0; + width: 100%; + } + + .ActionButton { + padding: 0; + font-size: 0.8em; + color: inherit; + + &:not(:disabled) { + text-decoration: underline; + } + + &:disabled { + font-style: italic; + } + + &:hover { + text-decoration: none; + } + } +} + .ColorBox { width: 2em; height: 2em; diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 3a133d616f0..4596e99c959 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -545,6 +545,11 @@ export class Grapher if (obj.minTime) obj.minTime = minTimeToJSON(this.minTime) as any if (obj.maxTime) obj.maxTime = maxTimeToJSON(this.maxTime) as any + if (obj.timelineMinTime) + obj.timelineMinTime = minTimeToJSON(this.timelineMinTime) as any + if (obj.timelineMaxTime) + obj.timelineMaxTime = maxTimeToJSON(this.timelineMaxTime) as any + // todo: remove dimensions concept // if (this.legacyConfigAsAuthored?.dimensions) // obj.dimensions = this.legacyConfigAsAuthored.dimensions @@ -574,6 +579,12 @@ export class Grapher // JSON doesn't support Infinity, so we use strings instead. this.minTime = minTimeBoundFromJSONOrNegativeInfinity(obj.minTime) this.maxTime = maxTimeBoundFromJSONOrPositiveInfinity(obj.maxTime) + this.timelineMinTime = minTimeBoundFromJSONOrNegativeInfinity( + obj.timelineMinTime + ) + this.timelineMaxTime = maxTimeBoundFromJSONOrPositiveInfinity( + obj.timelineMaxTime + ) // Todo: remove once we are more RAII. if (obj?.dimensions?.length) diff --git a/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts b/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts index c45ec0d1224..8406ccb2054 100644 --- a/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts +++ b/packages/@ourworldindata/grapher/src/schema/defaultGrapherConfig.ts @@ -4,6 +4,9 @@ import { GrapherInterface } from "@ourworldindata/types" +export const latestSchemaVersion = "005" as const +export const outdatedSchemaVersions = ["001", "002", "003", "004"] as const + export const defaultGrapherConfig = { $schema: "https://files.ourworldindata.org/schemas/grapher-schema.005.json", map: { @@ -32,6 +35,7 @@ export const defaultGrapherConfig = { hasChartTab: true, hideLegend: false, hideLogo: false, + timelineMinTime: "earliest", hideTimeline: false, colorScale: { equalSizeBins: true, @@ -64,6 +68,7 @@ export const defaultGrapherConfig = { canChangeScaleType: false, facetDomain: "shared", }, + timelineMaxTime: "latest", hideConnectedScatterLines: false, showNoDataArea: true, zoomToSelection: false, diff --git a/packages/@ourworldindata/grapher/src/schema/grapher-schema.005.yaml b/packages/@ourworldindata/grapher/src/schema/grapher-schema.005.yaml index 6f472f2a402..97873b45883 100644 --- a/packages/@ourworldindata/grapher/src/schema/grapher-schema.005.yaml +++ b/packages/@ourworldindata/grapher/src/schema/grapher-schema.005.yaml @@ -134,10 +134,15 @@ properties: type: boolean default: false timelineMinTime: - type: integer description: | The lowest year to show in the timeline. If this is set then the user is not able to see any data before this year. Inferred from data if not provided. + default: earliest + oneOf: + - type: number + - type: string + enum: + - earliest variantName: type: string description: Optional internal variant name for distinguishing charts with the same title @@ -423,10 +428,15 @@ properties: xAxis: $ref: "#/$defs/axis" timelineMaxTime: - type: integer description: | The highest year to show in the timeline. If this is set then the user is not able to see any data after this year. Inferred from data if not provided. + default: latest + oneOf: + - type: number + - type: string + enum: + - latest hideConnectedScatterLines: type: boolean default: false diff --git a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts index 3c72fd1aeb3..bd2971e57e6 100644 --- a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts +++ b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts @@ -535,8 +535,8 @@ export interface GrapherInterface extends SortConfig { hideAnnotationFieldsInTitle?: AnnotationFieldsInTitle minTime?: TimeBound | TimeBoundValueStr maxTime?: TimeBound | TimeBoundValueStr - timelineMinTime?: Time - timelineMaxTime?: Time + timelineMinTime?: Time | TimeBoundValueStr + timelineMaxTime?: Time | TimeBoundValueStr dimensions?: OwidChartDimensionInterface[] addCountryMode?: EntitySelectionMode comparisonLines?: ComparisonLineConfig[] diff --git a/vite.config-common.mts b/vite.config-common.mts index 4fea08c254f..f3d5526b8be 100644 --- a/vite.config-common.mts +++ b/vite.config-common.mts @@ -83,5 +83,8 @@ export const defineViteConfigForEntrypoint = (entrypoint: ViteEntryPoint) => { preview: { port: 8090, }, + optimizeDeps: { + exclude: ["js-base64"], + }, }) }