diff --git a/adminSiteClient/ChartEditorPage.tsx b/adminSiteClient/ChartEditorPage.tsx index 502df473cd8..4b35d41382d 100644 --- a/adminSiteClient/ChartEditorPage.tsx +++ b/adminSiteClient/ChartEditorPage.tsx @@ -249,7 +249,7 @@ export class ChartEditorPage @computed private get bounds(): Bounds { return this.isMobilePreview ? new Bounds(0, 0, 380, 525) - : this.grapher.idealBounds + : this.grapher.defaultBounds } @computed private get staticFormat(): GrapherStaticFormat { diff --git a/baker/GrapherImageBaker.tsx b/baker/GrapherImageBaker.tsx index a8232510b88..173ae4449df 100644 --- a/baker/GrapherImageBaker.tsx +++ b/baker/GrapherImageBaker.tsx @@ -46,7 +46,7 @@ export async function bakeGraphersToPngs( .then(() => console.log(`${outPath}.svg`)), sharp(Buffer.from(grapher.staticSVG), { density: 144 }) .png() - .resize(grapher.idealBounds.width, grapher.idealBounds.height) + .resize(grapher.defaultBounds.width, grapher.defaultBounds.height) .flatten({ background: "#ffffff" }) .toFile(`${outPath}.png`), ]) @@ -102,7 +102,7 @@ export async function bakeGrapherToSvg( verbose = true ) { const grapher = initGrapherForSvgExport(jsonConfig, queryStr) - const { width, height } = grapher.idealBounds + const { width, height } = grapher.defaultBounds const outPath = buildSvgOutFilepath( outDir, { diff --git a/devTools/svgTester/utils.ts b/devTools/svgTester/utils.ts index 26c43e473c7..83207632ef8 100644 --- a/devTools/svgTester/utils.ts +++ b/devTools/svgTester/utils.ts @@ -401,7 +401,7 @@ export async function renderSvg( }, queryStr ) - const { width, height } = grapher.idealBounds + const { width, height } = grapher.defaultBounds const outFilename = buildSvgOutFilename( { slug: configAndData.config.slug!, diff --git a/packages/@ourworldindata/grapher/src/bodyPortal/.eslintrc.yaml b/packages/@ourworldindata/grapher/src/bodyPortal/.eslintrc.yaml new file mode 100644 index 00000000000..2972fb8a9d9 --- /dev/null +++ b/packages/@ourworldindata/grapher/src/bodyPortal/.eslintrc.yaml @@ -0,0 +1,3 @@ +rules: + "@typescript-eslint/explicit-function-return-type": "warn" + "@typescript-eslint/explicit-module-boundary-types": "warn" diff --git a/packages/@ourworldindata/grapher/src/bodyPortal/BodyPortal.tsx b/packages/@ourworldindata/grapher/src/bodyPortal/BodyPortal.tsx new file mode 100644 index 00000000000..8153c14a07f --- /dev/null +++ b/packages/@ourworldindata/grapher/src/bodyPortal/BodyPortal.tsx @@ -0,0 +1,32 @@ +import React from "react" +import ReactDOM from "react-dom" + +interface BodyPortalProps { + id?: string + tagName?: string // default: "div" + children: React.ReactNode +} + +// Render a component on the Body instead of inside the current Tree. +// https://reactjs.org/docs/portals.html +export class BodyPortal extends React.Component { + el: HTMLElement + + constructor(props: BodyPortalProps) { + super(props) + this.el = document.createElement(props.tagName || "div") + if (props.id) this.el.id = props.id + } + + componentDidMount(): void { + document.body.appendChild(this.el) + } + + componentWillUnmount(): void { + document.body.removeChild(this.el) + } + + render(): any { + return ReactDOM.createPortal(this.props.children, this.el) + } +} diff --git a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.scss b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.scss index b44d634451b..06bd9ea88a6 100644 --- a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.scss +++ b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.scss @@ -1,6 +1,10 @@ // keep in sync with constant values in CaptionedChart.tsx $controlRowHeight: 32px; // keep in sync with CONTROLS_ROW_HEIGHT +.CaptionedChart { + width: 100%; +} + .HeaderHTML, .SourcesFooterHTML { font-family: $sans-serif-font-stack; @@ -22,7 +26,6 @@ $controlRowHeight: 32px; // keep in sync with CONTROLS_ROW_HEIGHT align-items: center; border-top: 1px solid $frame-color; position: absolute; - width: 100%; bottom: 0; color: $dark-text; font-weight: 700; diff --git a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.stories.tsx b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.stories.tsx index c0522eda421..b832d6b9ae3 100644 --- a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.stories.tsx +++ b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.stories.tsx @@ -21,7 +21,7 @@ export default { const table = SynthesizeGDPTable({ entityCount: 5 }) const manager: CaptionedChartManager = { - tabBounds: DEFAULT_BOUNDS, + captionedChartBounds: DEFAULT_BOUNDS, table, selection: table.availableEntityNames, currentTitle: "This is the Title", diff --git a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx index c73851bcb8b..7a05b4a79be 100644 --- a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx +++ b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx @@ -45,7 +45,6 @@ import { GrapherTabOption, RelatedQuestionsConfig, } from "@ourworldindata/types" -import { AxisConfig } from "../axis/AxisConfig" import { DataTable, DataTableManager } from "../dataTable/DataTable" import { ContentSwitchers, @@ -68,33 +67,42 @@ export interface CaptionedChartManager MapProjectionMenuManager, SettingsMenuManager { containerElement?: HTMLDivElement - tabBounds?: Bounds + bakedGrapherURL?: string + isReady?: boolean + whatAreWeWaitingFor?: string + + // bounds + captionedChartBounds?: Bounds + sidePanelBounds?: Bounds staticBounds?: Bounds staticBoundsWithDetails?: Bounds + + // layout + isSmall?: boolean + isMedium?: boolean + framePaddingHorizontal?: number + framePaddingVertical?: number fontSize?: number - bakedGrapherURL?: string + + // state tab?: GrapherTabOption - type: ChartTypeName - yAxis: AxisConfig - xAxis: AxisConfig - typeExceptWhenLineChartAndSingleTimeThenWillBeBarChart?: ChartTypeName - isReady?: boolean - whatAreWeWaitingFor?: string - entityType?: string - entityTypePlural?: string - shouldIncludeDetailsInStaticExport?: boolean - detailRenderers: MarkdownTextWrap[] isOnMapTab?: boolean isOnTableTab?: boolean + type: ChartTypeName + typeExceptWhenLineChartAndSingleTimeThenWillBeBarChart?: ChartTypeName + showEntitySelectionToggle?: boolean + + // timeline hasTimeline?: boolean timelineController?: TimelineController - hasRelatedQuestion?: boolean - isRelatedQuestionTargetDifferentFromCurrentPage?: boolean + + // details on demand + shouldIncludeDetailsInStaticExport?: boolean + detailRenderers: MarkdownTextWrap[] + + // related question relatedQuestions?: RelatedQuestionsConfig[] - isSmall?: boolean - isMedium?: boolean - framePaddingHorizontal?: number - framePaddingVertical?: number + showRelatedQuestion?: boolean } interface CaptionedChartProps { @@ -144,10 +152,6 @@ export class CaptionedChart extends React.Component { return this.manager.isMedium ? 8 : 16 } - @computed protected get relatedQuestionHeight(): number { - return this.manager.isMedium ? 24 : 28 - } - @computed protected get header(): Header { return new Header({ manager: this.manager, @@ -181,7 +185,9 @@ export class CaptionedChart extends React.Component { @computed protected get bounds(): Bounds { const bounds = - this.props.bounds ?? this.manager.tabBounds ?? DEFAULT_BOUNDS + this.props.bounds ?? + this.manager.captionedChartBounds ?? + DEFAULT_BOUNDS // the padding ensures grapher's frame is not cut off return bounds.padRight(2).padBottom(2) } @@ -258,28 +264,36 @@ export class CaptionedChart extends React.Component { return this.manager.selection } - @computed get showRelatedQuestion(): boolean { - return ( - !!this.manager.relatedQuestions && - !!this.manager.hasRelatedQuestion && - !!this.manager.isRelatedQuestionTargetDifferentFromCurrentPage - ) + @computed private get showRelatedQuestion(): boolean { + return !!this.manager.showRelatedQuestion + } + + @computed get relatedQuestionHeight(): number { + if (!this.showRelatedQuestion) return 0 + return this.manager.isMedium ? 24 : 28 + } + + @computed private get sidePanelWidth(): number { + return this.manager.sidePanelBounds?.width ?? 0 } private renderControlsRow(): JSX.Element { - const { showContentSwitchers } = this + const { showEntitySelectionToggle } = this.manager return (