From 6f968158dd6f12b600a2ddf28b391aceb52df2a6 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 25 Oct 2024 13:11:18 +0200 Subject: [PATCH 1/9] =?UTF-8?q?=E2=9C=A8=20(grapher)=20show=20tooltip=20at?= =?UTF-8?q?=20the=20bottom=20on=20mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/captionedChart/CaptionedChart.tsx | 1 + .../grapher/src/chart/ChartManager.ts | 3 +- .../grapher/src/chart/ChartTypeMap.tsx | 3 +- .../src/controls/EntitySelectionToggle.tsx | 2 + .../grapher/src/core/Grapher.tsx | 81 +- .../grapher/src/facetChart/FacetChart.tsx | 3 +- .../grapher/src/lineCharts/LineChart.tsx | 32 +- .../grapher/src/mapCharts/MapChart.tsx | 18 +- .../grapher/src/mapCharts/MapSparkline.tsx | 255 ++++++ .../grapher/src/mapCharts/MapTooltip.scss | 72 +- .../grapher/src/mapCharts/MapTooltip.tsx | 231 +---- .../src/scatterCharts/ScatterPlotChart.tsx | 1 + .../src/stackedCharts/MarimekkoChart.tsx | 1 + .../src/stackedCharts/StackedAreaChart.tsx | 33 +- .../src/stackedCharts/StackedBarChart.tsx | 1 + .../stackedCharts/StackedDiscreteBarChart.tsx | 1 + .../src/timeline/TimelineComponent.tsx | 2 + .../src/timeline/TimelineController.ts | 1 + .../grapher/src/tooltip/Tooltip.scss | 813 +++++++++--------- .../grapher/src/tooltip/Tooltip.tsx | 178 ++-- .../grapher/src/tooltip/TooltipContents.tsx | 83 +- .../grapher/src/tooltip/TooltipProps.ts | 16 +- .../types/src/grapherTypes/GrapherTypes.ts | 7 + packages/@ourworldindata/types/src/index.ts | 1 + 24 files changed, 1084 insertions(+), 755 deletions(-) create mode 100644 packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx diff --git a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx index 9aaffebffd6..629381a7bb5 100644 --- a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx +++ b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx @@ -229,6 +229,7 @@ export class CaptionedChart extends React.Component { bounds={bounds} manager={manager} containerElement={this.containerElement} + framePaddingHorizontal={this.framePaddingHorizontal} /> ) } diff --git a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts index 4f04a69d072..e4116cf0410 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts +++ b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts @@ -34,7 +34,7 @@ export interface ChartManager { isRelativeMode?: boolean comparisonLines?: ComparisonLineConfig[] showLegend?: boolean - tooltips?: TooltipManager["tooltips"] + tooltip?: TooltipManager["tooltip"] baseColorScheme?: ColorSchemeName invertColorScheme?: boolean compareEndPointsOnly?: boolean @@ -92,6 +92,7 @@ export interface ChartManager { isExportingForSocialMedia?: boolean secondaryColorInStaticCharts?: string backgroundColor?: Color + shouldPinTooltipToBottom?: boolean detailsOrderedByReference?: string[] detailsMarkerInSvg?: DetailsMarker diff --git a/packages/@ourworldindata/grapher/src/chart/ChartTypeMap.tsx b/packages/@ourworldindata/grapher/src/chart/ChartTypeMap.tsx index 45a0eef3b5f..2fbb5f8576c 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartTypeMap.tsx +++ b/packages/@ourworldindata/grapher/src/chart/ChartTypeMap.tsx @@ -16,7 +16,8 @@ import { MarimekkoChart } from "../stackedCharts/MarimekkoChart" interface ChartComponentProps { manager: ChartManager bounds?: Bounds - containerElement?: any // todo: remove? + containerElement?: HTMLDivElement + framePaddingHorizontal?: number } interface ChartComponentClass extends ComponentClass { diff --git a/packages/@ourworldindata/grapher/src/controls/EntitySelectionToggle.tsx b/packages/@ourworldindata/grapher/src/controls/EntitySelectionToggle.tsx index 3df6fa62294..80201e0eef0 100644 --- a/packages/@ourworldindata/grapher/src/controls/EntitySelectionToggle.tsx +++ b/packages/@ourworldindata/grapher/src/controls/EntitySelectionToggle.tsx @@ -18,6 +18,7 @@ export interface EntitySelectionManager { isEntitySelectorModalOrDrawerOpen?: boolean isOnChartTab?: boolean hideEntityControls?: boolean + onEntitySelectorOpen?: () => void } interface EntitySelectionLabel { @@ -82,6 +83,7 @@ export class EntitySelectionToggle extends React.Component<{ onClick={(e): void => { this.props.manager.isEntitySelectorModalOrDrawerOpen = !active + this.props.manager.onEntitySelectorOpen?.() e.stopPropagation() }} type="button" diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index cf38c20e544..6b52d454a4b 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -66,6 +66,7 @@ import { sortBy, extractDetailsFromSyntax, omit, + isTouchDevice, } from "@ourworldindata/utils" import { MarkdownTextWrap, @@ -105,6 +106,7 @@ import { GrapherWindowType, Color, GRAPHER_QUERY_PARAM_KEYS, + GrapherTooltipAnchor, } from "@ourworldindata/types" import { BlankOwidTable, @@ -210,6 +212,7 @@ import { type EntitySelectorState, } from "../entitySelector/EntitySelector" import { SlideInDrawer } from "../slideInDrawer/SlideInDrawer" +import { BodyDiv } from "../bodyDiv/BodyDiv" declare global { interface Window { @@ -862,7 +865,7 @@ export class Grapher @observable.ref isExportingToSvgOrPng = false @observable.ref isSocialMediaExport = false - tooltips?: TooltipManager["tooltips"] = observable.map({}, { deep: false }) + tooltip?: TooltipManager["tooltip"] = observable.box(undefined) @observable.ref isPlaying = false @observable.ref isTimelineAnimationActive = false // true if the timeline animation is either playing or paused but not finished @@ -2184,6 +2187,10 @@ export class Grapher return isMobile() } + @computed get isTouchDevice(): boolean { + return isTouchDevice() + } + @computed private get externalBounds(): Bounds { return this.props.bounds ?? DEFAULT_BOUNDS } @@ -2820,12 +2827,21 @@ export class Grapher - {/* tooltip */} - + {/* tooltip: either pin to the bottom or render into the chart area */} + {this.shouldPinTooltipToBottom ? ( + + + + ) : ( + + )} ) } @@ -2833,20 +2849,31 @@ export class Grapher // Chart should only render SVG when it's on the screen @action.bound private setUpIntersectionObserver(): void { if (typeof window !== "undefined" && "IntersectionObserver" in window) { - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - this.hasBeenVisible = true - - if (this.slug && !this.hasLoggedGAViewEvent) { - this.analytics.logGrapherView(this.slug) - this.hasLoggedGAViewEvent = true + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + this.hasBeenVisible = true + + if (this.slug && !this.hasLoggedGAViewEvent) { + this.analytics.logGrapherView(this.slug) + this.hasLoggedGAViewEvent = true + } } - observer.disconnect() - } - }) - }) + // dismiss tooltip when less than 2/3 of the chart is visible + const tooltip = this.tooltip?.get() + const isNotVisible = !entry.isIntersecting + const isPartiallyVisible = + entry.isIntersecting && + entry.intersectionRatio < 0.66 + if (tooltip && (isNotVisible || isPartiallyVisible)) { + tooltip.dismiss?.() + } + }) + }, + { threshold: [0, 0.66] } + ) observer.observe(this.containerElement!) this.disposers.push(() => observer.disconnect()) } else { @@ -2907,7 +2934,7 @@ export class Grapher @computed get isNarrow(): boolean { if (this.isStatic) return false - return this.frameBounds.width <= 400 + return this.frameBounds.width <= 420 } // SemiNarrow charts shorten their button labels to fit within the controls row @@ -2960,6 +2987,10 @@ export class Grapher : GRAPHER_BACKGROUND_DEFAULT } + @computed get shouldPinTooltipToBottom(): boolean { + return this.isNarrow && this.isTouchDevice + } + // Binds chart properties to global window title and URL. This should only // ever be invoked from top-level JavaScript. private bindToWindow(): void { @@ -3294,6 +3325,11 @@ export class Grapher timelineController = new TimelineController(this) + @action.bound onTimelineClick(): void { + const tooltip = this.tooltip?.get() + if (tooltip) tooltip.dismiss?.() + } + // todo: restore this behavior?? onStartPlayOrDrag(): void { this.debounceMode = true @@ -3434,6 +3470,11 @@ export class Grapher ) } + @action.bound onEntitySelectorOpen(): void { + const tooltip = this.tooltip?.get() + if (tooltip) tooltip.dismiss?.() + } + // This is just a helper method to return the correct table for providing entity choices. We want to // provide the root table, not the transformed table. // A user may have added time or other filters that would filter out all rows from certain entities, but diff --git a/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx b/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx index 956ee9d711a..8fd30e2bed9 100644 --- a/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx +++ b/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx @@ -502,7 +502,8 @@ export class FacetChart ...series.manager.yAxisConfig, ...axes.y.config, }, - tooltips: this.manager.tooltips, + tooltip: this.manager.tooltip, + shouldPinTooltipToBottom: this.manager.shouldPinTooltipToBottom, base: this.manager.base, } const contentBounds = getContentBounds( diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index 48e585aaa10..ee76ad5eb09 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -443,8 +443,21 @@ export class LineChart return table } - @action.bound private onCursorLeave(): void { + @action.bound private dismissTooltip(): void { this.tooltipState.target = null + } + + @action.bound onClick(e: React.MouseEvent): void { + // don't fire document event handler that dismisses the tooltip + if (this.manager.shouldPinTooltipToBottom) { + e.stopPropagation() + } + } + + @action.bound private onCursorLeave(): void { + if (!this.manager.shouldPinTooltipToBottom) { + this.dismissTooltip() + } this.clearHighlightedSeries() } @@ -601,6 +614,14 @@ export class LineChart ) } + @computed private get tooltipId(): number { + return this.renderUid + } + + @computed private get isTooltipActive(): boolean { + return this.manager.tooltip?.get()?.id === this.tooltipId + } + @computed private get tooltip(): React.ReactElement | undefined { const { formatColumn, colorColumn, hasColorScale } = this const { target, position, fading } = this.tooltipState @@ -670,7 +691,7 @@ export class LineChart return ( {this.renderChartElements()} - {this.activeXVerticalLine} + {this.isTooltipActive && this.activeXVerticalLine} {this.tooltip} ) diff --git a/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx b/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx index b8d9343a8fc..0a3deb9f85d 100644 --- a/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx +++ b/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx @@ -42,7 +42,11 @@ import { } from "./MapChartConstants" import { MapConfig } from "./MapConfig" import { ColorScale, ColorScaleManager } from "../color/ColorScale" -import { BASE_FONT_SIZE, Patterns } from "../core/GrapherConstants" +import { + BASE_FONT_SIZE, + DEFAULT_GRAPHER_FRAME_PADDING, + Patterns, +} from "../core/GrapherConstants" import { ChartInterface } from "../chart/ChartInterface" import { CategoricalBin, @@ -77,6 +81,7 @@ interface MapChartProps { bounds?: Bounds manager: MapChartManager containerElement?: HTMLDivElement + framePaddingHorizontal?: number } // Get the underlying geographical topology elements we're going to display @@ -230,6 +235,12 @@ export class MapChart return this.seriesMap } + @computed private get framePaddingHorizontal(): number { + return ( + this.props.framePaddingHorizontal ?? DEFAULT_GRAPHER_FRAME_PADDING + ) + } + base: React.RefObject = React.createRef() @action.bound onMapMouseOver(feature: GeoFeature): void { const series = @@ -622,6 +633,10 @@ export class MapChart renderInteractive(): React.ReactElement { const { tooltipState } = this + const sparklineWidth = this.manager.shouldPinTooltipToBottom + ? this.bounds.width + (this.framePaddingHorizontal - 1) * 2 + : undefined + return ( )} diff --git a/packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx b/packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx new file mode 100644 index 00000000000..509b6c23213 --- /dev/null +++ b/packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx @@ -0,0 +1,255 @@ +import React from "react" +import { computed } from "mobx" +import { observer } from "mobx-react" +import { + AxisConfigInterface, + ColumnSlug, + EntityName, + OwidVariableRoundingMode, + OwidVariableRow, + Time, +} from "@ourworldindata/types" +import { OwidTable } from "@ourworldindata/core-table" +import { LineChart } from "../lineCharts/LineChart" +import { + Bounds, + isNumber, + checkIsVeryShortUnit, + isString, + first, + last, + min, + max, +} from "@ourworldindata/utils" +import { LineChartManager } from "../lineCharts/LineChartConstants" +import { ColorScale } from "../color/ColorScale.js" + +const DEFAULT_SPARKLINE_WIDTH = 250 +const DEFAULT_SPARKLINE_HEIGHT = 87 + +const SPARKLINE_PADDING = 15 +const SPARKLINE_NUDGE = 3 // step away from the axis + +export interface MapSparklineManager { + mapColumnSlug?: ColumnSlug + timeSeriesTable: OwidTable + targetTime?: Time + entityName: EntityName + lineColorScale?: ColorScale + datum?: OwidVariableRow + mapAndYColumnAreTheSame?: boolean + yAxisConfig?: AxisConfigInterface +} + +@observer +export class MapSparkline extends React.Component<{ + manager: MapSparklineManager + sparklineWidth?: number + sparklineHeight?: number +}> { + static shouldShow(manager: MapSparklineManager): boolean { + const test = new MapSparkline({ manager }) + return test.showSparkline + } + + @computed private get manager(): MapSparklineManager { + return this.props.manager + } + + @computed private get mapColumnSlug(): ColumnSlug | undefined { + return this.manager.mapColumnSlug + } + + @computed private get sparklineTable(): OwidTable { + if (this.mapColumnSlug === undefined) return new OwidTable() + + return this.manager.timeSeriesTable + .filterByEntityNames([this.manager.entityName]) + .columnFilter( + this.mapColumnSlug, + isNumber, + "Drop rows with non-number values in Y column" + ) + .sortBy([this.manager.timeSeriesTable.timeColumn.slug]) + } + + @computed private get hasTimeSeriesData(): boolean { + return this.sparklineTable !== undefined + ? this.sparklineTable.numRows > 1 + : false + } + + @computed private get showSparkline(): boolean { + return this.hasTimeSeriesData + } + + @computed private get sparklineManager(): LineChartManager { + // use the whole time range for the sparkline, not just the range where this series has data + let { minTime, maxTime } = this.manager.timeSeriesTable ?? {} + if (this.mapColumnSlug) { + const times = + this.manager.timeSeriesTable.getTimesUniqSortedAscForColumns([ + this.mapColumnSlug, + ]) + minTime = first(times) ?? minTime + maxTime = last(times) ?? maxTime + } + + // Pass down short units, while omitting long or undefined ones. + const unit = this.sparklineTable.get(this.mapColumnSlug).shortUnit + const yAxisUnit = + typeof unit === "string" + ? checkIsVeryShortUnit(unit) + ? unit + : "" + : "" + + return { + table: this.sparklineTable, + transformedTable: this.sparklineTable, + yColumnSlug: this.mapColumnSlug, + colorColumnSlug: this.mapColumnSlug, + selection: [this.manager.entityName], + colorScaleOverride: this.manager.lineColorScale, + showLegend: false, + hidePoints: true, + fontSize: 11, + disableIntroAnimation: true, + lineStrokeWidth: 2, + annotation: { + entityName: this.manager.entityName, + year: this.manager.datum?.time, + }, + yAxisConfig: { + hideAxis: true, + hideGridlines: false, + tickFormattingOptions: { + unit: yAxisUnit, + numberAbbreviation: "short", + }, + // Copy min/max from top-level Grapher config if Y column == Map column + min: this.manager.mapAndYColumnAreTheSame + ? this.manager.yAxisConfig?.min + : undefined, + max: this.manager.mapAndYColumnAreTheSame + ? this.manager.yAxisConfig?.max + : undefined, + ticks: [ + // Show minimum and zero (maximum is added by hand in render so it's never omitted) + { value: -Infinity, priority: 2 }, + { value: 0, priority: 1 }, + ], + }, + xAxisConfig: { + hideAxis: false, + hideGridlines: true, + tickFormattingOptions: {}, + min: minTime ?? this.manager.targetTime, + max: maxTime ?? this.manager.targetTime, + ticks: [ + // Show minimum and maximum + { value: -Infinity, priority: 1 }, + { value: Infinity, priority: 1 }, + ], + }, + } + } + + @computed private get sparklineWidth(): number { + return this.props.sparklineWidth ?? DEFAULT_SPARKLINE_WIDTH + } + + @computed private get sparklineHeight(): number { + return this.props.sparklineHeight ?? DEFAULT_SPARKLINE_HEIGHT + } + + @computed private get sparklineBounds(): Bounds { + // Add padding so that the edges of the plot doesn't get clipped. + // The plot can go out of boundaries due to line stroke thickness & labels. + return new Bounds(0, 0, this.sparklineWidth, this.sparklineHeight).pad({ + top: 9, + left: SPARKLINE_PADDING, + right: SPARKLINE_PADDING, + bottom: 3, + }) + } + + @computed private get sparklineChart(): LineChart { + return new LineChart({ + manager: this.sparklineManager, + bounds: this.sparklineBounds, + }) + } + + render(): React.ReactElement | null { + if (!this.showSparkline) return null + + const { yAxisConfig } = this.sparklineManager, + yColumn = this.sparklineTable.get(this.mapColumnSlug), + minVal = min([yColumn.min, yAxisConfig?.min]), + maxVal = max([yColumn.max, yAxisConfig?.max]), + minCustom = + isNumber(minVal) && + this.manager.lineColorScale?.getBinForValue(minVal)?.label, + maxCustom = + isNumber(maxVal) && + this.manager.lineColorScale?.getBinForValue(maxVal)?.label, + useCustom = isString(minCustom) && isString(maxCustom), + minLabel = useCustom + ? minCustom + : yColumn.formatValueShort(minVal ?? 0, { + roundingMode: OwidVariableRoundingMode.decimalPlaces, + }), + maxLabel = useCustom + ? maxCustom + : yColumn.formatValueShort(maxVal ?? 0, { + roundingMode: OwidVariableRoundingMode.decimalPlaces, + }) + const { innerBounds: axisBounds } = this.sparklineChart.dualAxis + + const labelX = axisBounds.right - SPARKLINE_NUDGE + const labelTop = axisBounds.top - SPARKLINE_NUDGE + const labelBottom = axisBounds.bottom - SPARKLINE_NUDGE + + return ( +
+ + + + {maxLabel !== minLabel && ( + + + {maxLabel} + + + )} + + + {minLabel} + + + {minLabel} + + + +
+ ) + } +} diff --git a/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.scss b/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.scss index 4ddaad6d2c5..f8b7731c558 100644 --- a/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.scss +++ b/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.scss @@ -1,46 +1,48 @@ -.sparkline { - padding-bottom: 4px; +@at-root { + .sparkline { + padding-bottom: 4px; - .HorizontalAxis text { - fill: #787878; - font-weight: 700; - font-size: 10px; - transform: translate(2px, 2px); + .HorizontalAxis text { + fill: #787878; + font-weight: 700; + font-size: 10px; + transform: translate(2px, 2px); - &:last-child { - transform: translate(-2px, 2px); + &:last-child { + transform: translate(-2px, 2px); + } } - } - // min-line + zero-line - .AxisGridLines.horizontalLines { - line { - stroke-dasharray: 0; - stroke-linecap: square; - stroke: $gray-30; - } + // min-line + zero-line + .AxisGridLines.horizontalLines { + line { + stroke-dasharray: 0; + stroke-linecap: square; + stroke: $gray-30; + } - // the zero line - line:nth-child(2) { - stroke: #787878; + // the zero line + line:nth-child(2) { + stroke: #787878; + } } - } - line.max-line { - stroke-linecap: square; - stroke: $gray-30; - } + line.max-line { + stroke-linecap: square; + stroke: $gray-30; + } - .axis-label { - text { - font: 400 10px Lato; - letter-spacing: 0.01em; - font-style: italic; - text-anchor: end; - fill: #787878; - &.outline { - stroke: rgba(255, 255, 255, 0.85); - stroke-width: 2px; + .axis-label { + text { + font: 400 10px Lato; + letter-spacing: 0.01em; + font-style: italic; + text-anchor: end; + fill: #787878; + &.outline { + stroke: rgba(255, 255, 255, 0.85); + stroke-width: 2px; + } } } } diff --git a/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.tsx b/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.tsx index 59a4bee155e..93433eb8b93 100644 --- a/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.tsx +++ b/packages/@ourworldindata/grapher/src/mapCharts/MapTooltip.tsx @@ -15,26 +15,18 @@ import { Time, EntityName, OwidVariableRow, - OwidVariableRoundingMode, + AxisConfigInterface, } from "@ourworldindata/types" import { CoreColumn, OwidTable } from "@ourworldindata/core-table" -import { LineChart } from "../lineCharts/LineChart" import { - Bounds, isNumber, AllKeysRequired, - checkIsVeryShortUnit, PrimitiveType, - isString, - first, - last, - min, - max, excludeUndefined, anyToString, } from "@ourworldindata/utils" -import { LineChartManager } from "../lineCharts/LineChartConstants" import { darkenColorForHighContrastText } from "../color/ColorUtils" +import { MapSparkline, MapSparklineManager } from "./MapSparkline.js" interface MapTooltipProps { tooltipState: TooltipState<{ featureId: string; clickable: boolean }> @@ -43,16 +35,16 @@ interface MapTooltipProps { formatValueIfCustom: (d: PrimitiveType) => string | undefined timeSeriesTable: OwidTable targetTime?: Time + sparklineWidth?: number + sparklineHeight?: number } -const SPARKLINE_WIDTH = 250 -const SPARKLINE_HEIGHT = 87 -const SPARKLINE_PADDING = 15 -const SPARKLINE_NUDGE = 3 // step away from the axis - @observer -export class MapTooltip extends React.Component { - @computed private get mapColumnSlug(): string | undefined { +export class MapTooltip + extends React.Component + implements MapSparklineManager +{ + @computed get mapColumnSlug(): string | undefined { return this.props.manager.mapColumnSlug } @@ -60,49 +52,37 @@ export class MapTooltip extends React.Component { return this.mapTable.get(this.mapColumnSlug) } - @computed private get mapAndYColumnAreTheSame(): boolean { + @computed get mapAndYColumnAreTheSame(): boolean { const { yColumnSlug, yColumnSlugs, mapColumnSlug } = this.props.manager return yColumnSlugs && mapColumnSlug !== undefined ? yColumnSlugs.includes(mapColumnSlug) : yColumnSlug === mapColumnSlug } - @computed private get entityName(): EntityName { + @computed get entityName(): EntityName { return this.props.tooltipState.target?.featureId ?? "" } - // Table pre-filtered by targetTime, exlcudes time series + @computed get targetTime(): Time | undefined { + return this.props.targetTime + } + + // Table pre-filtered by targetTime, excludes time series @computed private get mapTable(): OwidTable { const table = this.props.manager.transformedTable ?? this.props.manager.table return table.filterByEntityNames([this.entityName]) } - @computed private get timeSeriesTable(): OwidTable | undefined { - if (this.mapColumnSlug === undefined) return undefined + @computed get timeSeriesTable(): OwidTable { return this.props.timeSeriesTable - .filterByEntityNames([this.entityName]) - .columnFilter( - this.mapColumnSlug, - isNumber, - "Drop rows with non-number values in Y column" - ) - .sortBy([this.props.timeSeriesTable.timeColumn.slug]) } - @computed private get datum(): - | OwidVariableRow - | undefined { + @computed get datum(): OwidVariableRow | undefined { return this.mapColumn.owidRows[0] } - @computed private get hasTimeSeriesData(): boolean { - return this.timeSeriesTable !== undefined - ? this.timeSeriesTable.numRows > 1 - : false - } - - @computed private get lineColorScale(): ColorScale { + @computed get lineColorScale(): ColorScale { const oldManager = this.props.colorScaleManager // Make sure all ColorScaleManager props are included. // We can't ...rest here because I think mobx computeds aren't @@ -118,101 +98,11 @@ export class MapTooltip extends React.Component { } @computed private get showSparkline(): boolean { - return this.hasTimeSeriesData - } - - // Line chart fields - @computed private get sparklineTable(): OwidTable { - return this.timeSeriesTable ?? new OwidTable() - } - @computed private get sparklineManager(): LineChartManager { - // use the whole time range for the sparkline, not just the range where this series has data - let { minTime, maxTime } = this.props.timeSeriesTable ?? {} - if (this.mapColumnSlug) { - const times = - this.props.timeSeriesTable.getTimesUniqSortedAscForColumns([ - this.mapColumnSlug, - ]) - minTime = first(times) ?? minTime - maxTime = last(times) ?? maxTime - } - - // Pass down short units, while omitting long or undefined ones. - const unit = this.sparklineTable.get(this.mapColumnSlug).shortUnit - const yAxisUnit = - typeof unit === "string" - ? checkIsVeryShortUnit(unit) - ? unit - : "" - : "" - - return { - table: this.sparklineTable, - transformedTable: this.sparklineTable, - yColumnSlug: this.mapColumnSlug, - colorColumnSlug: this.mapColumnSlug, - selection: [this.entityName], - colorScaleOverride: this.lineColorScale, - showLegend: false, - hidePoints: true, - fontSize: 11, - disableIntroAnimation: true, - lineStrokeWidth: 2, - entityYearHighlight: { - entityName: this.entityName, - year: this.datum?.time, - }, - yAxisConfig: { - hideAxis: true, - hideGridlines: false, - tickFormattingOptions: { - unit: yAxisUnit, - numberAbbreviation: "short", - }, - // Copy min/max from top-level Grapher config if Y column == Map column - min: this.mapAndYColumnAreTheSame - ? this.props.manager.yAxisConfig?.min - : undefined, - max: this.mapAndYColumnAreTheSame - ? this.props.manager.yAxisConfig?.max - : undefined, - ticks: [ - // Show minimum and zero (maximum is added by hand in render so it's never omitted) - { value: -Infinity, priority: 2 }, - { value: 0, priority: 1 }, - ], - }, - xAxisConfig: { - hideAxis: false, - hideGridlines: true, - tickFormattingOptions: {}, - min: minTime ?? this.props.targetTime, - max: maxTime ?? this.props.targetTime, - ticks: [ - // Show minimum and maximum - { value: -Infinity, priority: 1 }, - { value: Infinity, priority: 1 }, - ], - }, - } + return MapSparkline.shouldShow(this) } - @computed private get sparklineBounds(): Bounds { - // Add padding so that the edges of the plot doesn't get clipped. - // The plot can go out of boundaries due to line stroke thickness & labels. - return new Bounds(0, 0, SPARKLINE_WIDTH, SPARKLINE_HEIGHT).pad({ - top: 9, - left: SPARKLINE_PADDING, - right: SPARKLINE_PADDING, - bottom: 3, - }) - } - - @computed private get sparklineChart(): LineChart { - return new LineChart({ - manager: this.sparklineManager, - bounds: this.sparklineBounds, - }) + @computed get yAxisConfig(): AxisConfigInterface | undefined { + return this.props.manager.yAxisConfig } render(): React.ReactElement { @@ -250,28 +140,7 @@ export class MapTooltip extends React.Component { } } - const { yAxisConfig } = this.sparklineManager, - yColumn = this.sparklineTable.get(this.mapColumnSlug), - minVal = min([yColumn.min, yAxisConfig?.min]), - maxVal = max([yColumn.max, yAxisConfig?.max]), - minCustom = - isNumber(minVal) && - this.lineColorScale.getBinForValue(minVal)?.label, - maxCustom = - isNumber(maxVal) && - this.lineColorScale.getBinForValue(maxVal)?.label, - useCustom = isString(minCustom) && isString(maxCustom), - minLabel = useCustom - ? minCustom - : yColumn.formatValueShort(minVal ?? 0, { - roundingMode: OwidVariableRoundingMode.decimalPlaces, - }), - maxLabel = useCustom - ? maxCustom - : yColumn.formatValueShort(maxVal ?? 0, { - roundingMode: OwidVariableRoundingMode.decimalPlaces, - }) - const { innerBounds: axisBounds } = this.sparklineChart.dualAxis + const yColumn = this.mapTable.get(this.mapColumnSlug) const targetNotice = datum && datum.time !== targetTime ? displayTime : undefined @@ -295,10 +164,6 @@ export class MapTooltip extends React.Component { : undefined const footer = excludeUndefined([toleranceNotice, roundingNotice]) - const labelX = axisBounds.right - SPARKLINE_NUDGE - const labelTop = axisBounds.top - SPARKLINE_NUDGE - const labelBottom = axisBounds.bottom - SPARKLINE_NUDGE - return ( { subtitleFormat={targetNotice ? "notice" : undefined} footer={footer} dissolve={fading} + dismiss={() => (this.props.tooltipState.target = null)} > { roundingNotice.icon !== TooltipFooterIcon.none } /> - {this.showSparkline && ( -
- - - - {maxLabel !== minLabel && ( - - - {maxLabel} - - - )} - - - {minLabel} - - - {minLabel} - - - -
- )} +
) } diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx index 1f70d50a869..12ad62c83c6 100644 --- a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx +++ b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPlotChart.tsx @@ -983,6 +983,7 @@ export class ScatterPlotChart subtitle={timeLabel} dissolve={fading} footer={footer} + dismiss={() => (this.tooltipState.target = null)} > (this.tooltipState.target = null)} > {yValues.map(({ name, value, notice }) => ( ): void { + // don't fire document event handler that dismisses the tooltip + if (this.manager.shouldPinTooltipToBottom) { + e.stopPropagation() + } + } + @action.bound private onCursorMove( ev: React.MouseEvent | React.TouchEvent ): void { @@ -454,9 +461,15 @@ export class StackedAreaChart } } + @action.bound private dismissTooltip(): void { + this.tooltipState.target = null + } + @action.bound private onCursorLeave(): void { + if (!this.manager.shouldPinTooltipToBottom) { + this.dismissTooltip() + } this.hoverSeriesName = undefined - this.tooltipState.target = null } @computed private get activeXVerticalLine(): @@ -465,7 +478,6 @@ export class StackedAreaChart const { dualAxis, series } = this const { horizontalAxis, verticalAxis } = dualAxis const hoveredPointIndex = this.tooltipState.target?.index - if (hoveredPointIndex === undefined) return undefined return ( @@ -502,6 +514,14 @@ export class StackedAreaChart ) } + @computed private get tooltipId(): number { + return this.renderUid + } + + @computed private get isTooltipActive(): boolean { + return this.manager.tooltip?.get()?.id === this.tooltipId + } + @computed private get tooltip(): React.ReactElement | undefined { const { target, position, fading } = this.tooltipState if (!target) return undefined @@ -530,7 +550,7 @@ export class StackedAreaChart return ( - {this.activeXVerticalLine} + {this.isTooltipActive && this.activeXVerticalLine} {this.tooltip} ) diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx index 8c6f70c009a..cf881398051 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedBarChart.tsx @@ -392,6 +392,7 @@ export class StackedBarChart subtitleFormat="unit" footer={footer} dissolve={fading} + dismiss={() => (this.tooltipState.target = null)} > (this.tooltipState.target = null)} > void + onTimelineClick?: () => void } const delay = (ms: number): Promise => diff --git a/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss b/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss index e414ea9b8ce..0359f682f95 100644 --- a/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss +++ b/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss @@ -1,467 +1,496 @@ -.tooltip-container > .Tooltip { - $background-fill: #f0f0f0; - $background-stroke: $gray-20; - $dark-grey: #2d2d2d; - $light-grey: $gray-30; - $grey: #787878; - $red: #cc3b55; - $green: #2c8465; - $fade-time: 200ms; - $fade-delay: 200ms; - $medium: 400; - $bold: 700; - - border-radius: 4px; - border: 1px solid $background-stroke; - box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.2); - background: white; - text-align: left; - position: absolute; - pointer-events: none; - font-family: $sans-serif-font-stack; - font-size: 16px; - - @mixin diagonal-background($background, $color) { - background: repeating-linear-gradient( - -45deg, - $background, - $background 16%, - $color 16%, - $color 25% - ); - } - - .frontmatter { - background: $background-fill; - color: black; - padding: 8px 12px; - border-radius: 3px 3px 0 0; - - .title, - .subtitle { - margin: 0; - padding: 0; - line-height: 1.125em; +@at-root { + .tooltip-container { + $background-fill: #f0f0f0; + $background-stroke: $gray-20; + $dark-grey: #2d2d2d; + $light-grey: $gray-30; + $grey: #787878; + $red: #cc3b55; + $green: #2c8465; + $fade-time: 200ms; + $fade-delay: 200ms; + $medium: 400; + $bold: 700; + + &.fixed-bottom { + position: fixed; + left: 4px; + bottom: 4px; + width: calc(100% - 8px); + z-index: $zindex-tooltip; + + > .Tooltip { + pointer-events: auto; + + .content-and-endmatter { + max-height: min(166px, 25vh); + overflow-y: auto; + overflow-x: hidden; + } + } } - .title { - font-size: 14px; - font-weight: $bold; - letter-spacing: 0; - } - .subtitle { - margin: 4px 0 2px 0; - font-size: 12px; - font-weight: $medium; - letter-spacing: 0.01em; + > .Tooltip { + border-radius: 4px; + border: 1px solid $background-stroke; + box-shadow: 0px 4px 40px rgba(0, 0, 0, 0.2); + background: white; + text-align: left; + pointer-events: none; + font-family: $sans-serif-font-stack; + font-size: 16px; + + @mixin diagonal-background($background, $color) { + background: repeating-linear-gradient( + -45deg, + $background, + $background 16%, + $color 16%, + $color 25% + ); + } - svg.fa-circle-info { - color: $grey; - margin-right: 0.5em; + .frontmatter { + background: $background-fill; + color: black; + padding: 8px 12px; + border-radius: 3px 3px 0 0; + + .title, + .subtitle { + margin: 0; + padding: 0; + line-height: 1.125em; + } + + .title { + font-size: 14px; + font-weight: $bold; + letter-spacing: 0; + } + .subtitle { + margin: 4px 0 2px 0; + font-size: 12px; + font-weight: $medium; + letter-spacing: 0.01em; + + svg.fa-circle-info { + color: $grey; + margin-right: 0.5em; + } + } } - } - } - &.plain header { - border-radius: 3px; - background: white; - } + &.plain header { + border-radius: 3px; + background: white; + } - .content { - padding: 8px 12px; + .content { + padding: 8px 12px; - > p { - margin: 0; - padding: 0; - } + > p { + margin: 0; + padding: 0; + } + + .variable { + .definition { + color: $gray-70; + font-size: 12px; + line-height: 15px; + letter-spacing: 0.01em; + font-weight: $bold; + + .name { + margin-right: 0.25em; + } - .variable { - .definition { - color: $gray-70; - font-size: 12px; - line-height: 15px; - letter-spacing: 0.01em; - font-weight: $bold; + .unit { + font-weight: $medium; + font-style: normal; - .name { - margin-right: 0.25em; + &::before { + content: "("; + } + + &::after { + content: ")"; + } + } + } + + .values { + display: flex; + align-items: baseline; + justify-content: space-between; + color: $dark-grey; + padding: 2px 0; + line-height: 21px; + font-size: 18px; + font-weight: $bold; + + .range { + display: flex; + flex-wrap: wrap; + align-items: baseline; + column-gap: 0.2em; + flex-grow: 1; + + .term { + overflow-wrap: anywhere; + } + + svg.arrow { + height: 14px; + padding-right: 0.15em; + &.up path { + fill: $green; + } + &.down path { + fill: $red; + } + &.right path { + fill: $grey; + } + } + } + + .time-notice { + position: relative; + font-weight: $medium; + font-size: 14px; + line-height: 21px; + padding-left: 20px; + svg.fa-circle-info { + position: absolute; + top: 3px; + left: 0; + } + color: $grey; + } + } + } + + .variable + .variable { + margin-top: 4px; + padding-top: 8px; + border-top: 1px solid $light-grey; } - .unit { + table.series-list { + color: $dark-grey; + font-size: 14px; + line-height: 22px; font-weight: $medium; - font-style: normal; + white-space: normal; + border-collapse: collapse; + width: 100%; + + // only used if rows have ≥2 values + thead { + font-size: 12px; + letter-spacing: 0.01em; - &::before { - content: "("; + tr td.series-value { + font-weight: $medium; + } } - &::after { - content: ")"; + // -- standard columns -- + td { + vertical-align: baseline; } - } - } - .values { - display: flex; - align-items: baseline; - justify-content: space-between; - color: $dark-grey; - padding: 2px 0; - line-height: 21px; - font-size: 18px; - font-weight: $bold; - - .range { - display: flex; - flex-wrap: wrap; - align-items: baseline; - column-gap: 0.2em; - flex-grow: 1; - - .term { - overflow-wrap: anywhere; + td.series-color { + padding-left: 0; + .swatch { + width: 12px; + height: 12px; + display: inline-block; + margin-right: 0.3em; + text-align: left; + position: relative; + } } - svg.arrow { - height: 14px; - padding-right: 0.15em; - &.up path { - fill: $green; + td.series-name { + padding-right: 0.9em; + line-height: 16px; + width: 100%; + .parenthetical { + color: $grey; } - &.down path { - fill: $red; + + .annotation { + display: block; + color: $grey; + font-size: 12px; + letter-spacing: 0.01em; } - &.right path { - fill: $grey; + } + + td.series-value { + font-weight: $bold; + text-align: right; + white-space: nowrap; + + &.missing::before { + content: "No data"; + color: $light-grey; + } + + & + .series-value { + padding-left: 0.5em; } } - } - .time-notice { - position: relative; - font-weight: $medium; - font-size: 14px; - line-height: 21px; - padding-left: 20px; - svg.fa-circle-info { - position: absolute; - top: 3px; - left: 0; + td.time-notice { + white-space: nowrap; + font-weight: $medium; + text-indent: 20px; + text-align: right; + padding-right: 0; + color: $grey; } - color: $grey; - } - } - } - .variable + .variable { - margin-top: 4px; - padding-top: 8px; - border-top: 1px solid $light-grey; - } + // -- special row types -- - table.series-list { - color: $dark-grey; - font-size: 14px; - line-height: 22px; - font-weight: $medium; - white-space: normal; - border-collapse: collapse; - width: 100%; - - // only used if rows have ≥2 values - thead { - font-size: 12px; - letter-spacing: 0.01em; - - tr td.series-value { - font-weight: $medium; - } - } + tr.blurred { + color: $light-grey; + .series-color .swatch { + opacity: 0.25; + } + .series-name span { + color: inherit; + } + } - // -- standard columns -- - td { - vertical-align: baseline; - } + tr.spacer { + line-height: 2px; + font-size: 2px; + &::before { + content: "\00a0"; + } + } - td.series-color { - padding-left: 0; - .swatch { - width: 12px; - height: 12px; - display: inline-block; - margin-right: 0.3em; - text-align: left; - position: relative; - } - } + tr.total { + td { + line-height: 14px; + } + td:nth-child(2), + td:nth-child(3) { + border-top: 1px solid $background-stroke; + vertical-align: bottom; + } + td:last-child::before { + content: "\200a"; + height: 5px; + display: block; + } + } - td.series-name { - padding-right: 0.9em; - line-height: 16px; - width: 100%; - .parenthetical { - color: $grey; - } + tr.total--top { + td:nth-child(2), + td:nth-child(3) { + border-bottom: 1px solid $background-stroke; + vertical-align: top; + } + } - .annotation { - display: block; - color: $grey; - font-size: 12px; - letter-spacing: 0.01em; - } - } + // highlight hovered row + &.focal { + tr:not(.focused, .total) td { + opacity: 0.6; + } - td.series-value { - font-weight: $bold; - text-align: right; - white-space: nowrap; + td.series-value { + font-weight: $medium; + } - &.missing::before { - content: "No data"; - color: $light-grey; - } + tr.focused td { + font-weight: $bold; - & + .series-value { - padding-left: 0.5em; - } - } + .parenthetical { + font-weight: $medium; + } + &.time-notice { + font-weight: $medium; + } + } + } - td.time-notice { - white-space: nowrap; - font-weight: $medium; - text-indent: 20px; - text-align: right; - padding-right: 0; - color: $grey; - } + // hide unused color swatch column + &:not(.swatched) { + td.series-color { + display: none; + } + } - // -- special row types -- + // overlay a diagonal line pattern on striped swatches + tr.striped .series-color .swatch::before { + content: " "; + position: absolute; + width: 100%; + height: 100%; + @include diagonal-background(transparent, white); + } - tr.blurred { - color: $light-grey; - .series-color .swatch { - opacity: 0.25; - } - .series-name span { - color: inherit; + tr.striped.blurred .series-color .swatch::before { + @include diagonal-background($grey, white); + } } - } - tr.spacer { - line-height: 2px; - font-size: 2px; - &::before { - content: "\00a0"; + .hoverIndicator circle { + stroke-width: 1; + r: 5px; } } - tr.total { - td { - line-height: 14px; - } - td:nth-child(2), - td:nth-child(3) { - border-top: 1px solid $background-stroke; - vertical-align: bottom; - } - td:last-child::before { - content: "\200a"; - height: 5px; - display: block; + // tolerance captions w/ circle-i icon or projection warning with striped swatch + .endmatter { + position: relative; + color: $grey; + padding: 4px 12px; + border-radius: 0 0 3px 3px; + border-top: 1px solid $light-grey; + + .line { + margin: 4px 0; } - } - // highlight hovered row - &.focal { - tr:not(.focused, .total) td { - opacity: 0.6; + .line.no-icon { + font-style: italic; } - td.series-value { - font-weight: $medium; + // add a top border to the last line if it doesn't have an icon and there are multiple lines + &.multiple-lines .line.no-icon:last-of-type { + border-top: 1px solid $background-stroke; + padding-top: 5px; + margin-top: 6px; } - tr.focused td { - font-weight: $bold; + .icon { + position: absolute; + width: 12px; - .parenthetical { - font-weight: $medium; - } - &.time-notice { - font-weight: $medium; + &.stripes { + content: " "; + height: 12px; + @include diagonal-background($grey, white); } } - } - // hide unused color swatch column - &:not(.swatched) { - td.series-color { - display: none; + p { + font-size: 12px; + letter-spacing: 0.01em; + line-height: 15px; + margin: 0; + max-width: 260px; } - } - - // overlay a diagonal line pattern on striped swatches - tr.striped .series-color .swatch::before { - content: " "; - position: absolute; - width: 100%; - height: 100%; - @include diagonal-background(transparent, white); - } - tr.striped.blurred .series-color .swatch::before { - @include diagonal-background($grey, white); + .icon ~ p { + padding-left: 19px; + } } - } - .hoverIndicator circle { - stroke-width: 1; - r: 5px; - } - } + .icon-circled-s { + --size: 12px; - // tolerance captions w/ circle-i icon or projection warning with striped swatch - .endmatter { - position: relative; - color: $grey; - padding: 4px 12px; - border-radius: 0 0 3px 3px; - border-top: 1px solid $light-grey; + position: relative; + width: var(--size); + height: var(--size); + font-size: var(--size); + bottom: -1px; // small visual correction - .line { - margin: 4px 0; - } - - .line.no-icon { - font-style: italic; - } + &.as-superscript { + --size: 10px; - // add a top border to the last line if it doesn't have an icon and there are multiple lines - &.multiple-lines .line.no-icon:last-of-type { - border-top: 1px solid $background-stroke; - padding-top: 5px; - margin-top: 6px; - } + display: inline-block; + bottom: 0.7em; + margin-left: 2px; + } - .icon { - position: absolute; - width: 12px; + .circle { + position: absolute; + width: 100%; + height: 100%; + border: 1px solid $grey; + border-radius: 50%; + } - &.stripes { - content: " "; - height: 12px; - @include diagonal-background($grey, white); + svg { + font-size: 0.6em; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: $grey; + } } - } - - p { - font-size: 12px; - letter-spacing: 0.01em; - line-height: 15px; - margin: 0; - max-width: 260px; - } - - .icon ~ p { - padding-left: 19px; - } - } - - .icon-circled-s { - --size: 12px; - - position: relative; - width: var(--size); - height: var(--size); - font-size: var(--size); - bottom: -1px; // small visual correction - - &.as-superscript { - --size: 10px; - - display: inline-block; - bottom: 0.7em; - margin-left: 2px; - } - - .circle { - position: absolute; - width: 100%; - height: 100%; - border: 1px solid $grey; - border-radius: 50%; - } - - svg { - font-size: 0.6em; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: $grey; - } - } - // class names passed to the `dissolve` prop control fade-out timing - &.dissolve { - opacity: 0; - transition: opacity $fade-time $fade-delay; - &.immediate { - transition: opacity $fade-time; - } - } - - // - // ‘TEMPORARY’ FIX: - // Until the map variable displayNames can be copy-edited, hide the variable name entirely - // and use just the unit (if present) as a label - // - &#mapTooltip { - .variable .definition { - .name { - display: none; + // class names passed to the `dissolve` prop control fade-out timing + &.dissolve { + opacity: 0; + transition: opacity $fade-time $fade-delay; + &.immediate { + transition: opacity $fade-time; + } } - .unit::after, - .unit::before { - content: none; - } - } - } -} + // + // ‘TEMPORARY’ FIX: + // Until the map variable displayNames can be copy-edited, hide the variable name entirely + // and use just the unit (if present) as a label + // + &#mapTooltip { + .variable .definition { + .name { + display: none; + } -// adapt to smaller display areas -@container grapher (max-width:900px) { - .Tooltip { - .content { - padding: 4px 12px; - .variable .values { - font-size: 16px; - line-height: 18px; - svg.arrow { - height: 12px; - padding-right: 2px; + .unit::after, + .unit::before { + content: none; + } } } + } - .variable + .variable { - padding-top: 4px; - } - - table.series-list { - font-size: 12px; - line-height: 18px; - letter-spacing: 0.01em; + // adapt to smaller display areas + @container grapher (max-width:900px) { + .Tooltip { + .content { + padding: 4px 12px; + .variable .values { + font-size: 16px; + line-height: 18px; + svg.arrow { + height: 12px; + padding-right: 2px; + } + } - tr.total { - td:last-child::before { - height: 4px; + .variable + .variable { + padding-top: 4px; } - } - td.series-name .annotation { - font-size: 10px; - line-height: 12px; + table.series-list { + font-size: 12px; + line-height: 18px; + letter-spacing: 0.01em; + + tr.total { + td:last-child::before { + height: 4px; + } + } + + td.series-name .annotation { + font-size: 10px; + line-height: 12px; + } + } } } } diff --git a/packages/@ourworldindata/grapher/src/tooltip/Tooltip.tsx b/packages/@ourworldindata/grapher/src/tooltip/Tooltip.tsx index 4ad1ce0de63..226fa4fad36 100644 --- a/packages/@ourworldindata/grapher/src/tooltip/Tooltip.tsx +++ b/packages/@ourworldindata/grapher/src/tooltip/Tooltip.tsx @@ -4,12 +4,17 @@ import { observable, computed, action } from "mobx" import { observer } from "mobx-react" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" import { faInfoCircle } from "@fortawesome/free-solid-svg-icons" -import { Bounds, PointVector } from "@ourworldindata/utils" +import { + Bounds, + PointVector, + GrapherTooltipAnchor, +} from "@ourworldindata/utils" import { TooltipProps, TooltipManager, TooltipFadeMode, TooltipFooterIcon, + TooltipContext, } from "./TooltipProps" import { IconCircledS } from "./TooltipContents.js" export * from "./TooltipContents.js" @@ -71,11 +76,13 @@ export class TooltipState { @observer class TooltipCard extends React.Component< TooltipProps & { - containerWidth: number - containerHeight: number bounds?: Bounds + containerWidth?: number + containerHeight?: number } > { + static contextType = TooltipContext + private base: React.RefObject = React.createRef() @observable.struct private bounds?: Bounds @@ -105,32 +112,44 @@ class TooltipCard extends React.Component< offsetY = 0, } = this.props - if (this.props.offsetYDirection === "upward") { - offsetY = -offsetY - (this.bounds?.height ?? 0) - } + const style = { ...this.props.style } - if ( - this.props.offsetXDirection === "left" && - this.props.x > (this.bounds?.width ?? 0) - ) { - offsetX = -offsetX - (this.bounds?.width ?? 0) - } + // if container dimensions are given, we make sure the tooltip + // is positioned within the container bounds + if (this.props.containerWidth && this.props.containerHeight) { + if (this.props.offsetYDirection === "upward") { + offsetY = -offsetY - (this.bounds?.height ?? 0) + } + + if ( + this.props.offsetXDirection === "left" && + this.props.x > (this.bounds?.width ?? 0) + ) { + offsetX = -offsetX - (this.bounds?.width ?? 0) + } + + // Ensure tooltip remains inside chart + let left = this.props.x + offsetX + let top = this.props.y + offsetY + if (this.bounds) { + if (left + this.bounds.width > this.props.containerWidth) + left -= this.bounds.width + 2 * offsetX // flip left + if ( + top + this.bounds.height * 0.75 > + this.props.containerHeight + ) + top -= this.bounds.height + 2 * offsetY // flip upwards eventually... + if (top + this.bounds.height > this.props.containerHeight) + top = this.props.containerHeight - this.bounds.height // ...but first pin at bottom + + if (left < 0) left = 0 // pin on left + if (top < 0) top = 0 // pin at top + } - // Ensure tooltip remains inside chart - let left = this.props.x + offsetX - let top = this.props.y + offsetY - if (this.bounds) { - if (left + this.bounds.width > this.props.containerWidth) - left -= this.bounds.width + 2 * offsetX // flip left - if (top + this.bounds.height * 0.75 > this.props.containerHeight) - top -= this.bounds.height + 2 * offsetY // flip upwards eventually... - if (top + this.bounds.height > this.props.containerHeight) - top = this.props.containerHeight - this.bounds.height // ...but first pin at bottom - - if (left < 0) left = 0 // pin on left - if (top < 0) top = 0 // pin at top + style.position = "absolute" + style.left = left + style.top = top } - const style = { left, top, ...this.props.style } // add a preposition to unit-based subtitles const hasHeader = title !== undefined || subtitle !== undefined @@ -149,6 +168,14 @@ class TooltipCard extends React.Component< // skip transition delay if requested const immediate = dissolve === "immediate" + // ignore the given width and max-width if the tooltip position is fixed + // since we want to use the full width of the screen in that case + const isPinnedToBottom = + this.context.anchor === GrapherTooltipAnchor.bottom + if (isPinnedToBottom && (style.width || style.maxWidth)) { + style.width = style.maxWidth = undefined + } + return (
)} - {children &&
{children}
} - {footer && footer.length > 0 && ( -
1, - })} - > - {footer?.map(({ icon, text }) => ( -
- {TOOLTIP_ICON[icon]} -

{text}

-
- ))} -
- )} +
+ {children &&
{children}
} + {footer && footer.length > 0 && ( +
1, + })} + > + {footer?.map(({ icon, text }) => ( +
+ {TOOLTIP_ICON[icon]} +

{text}

+
+ ))} +
+ )} +
) } @@ -201,24 +232,51 @@ class TooltipCard extends React.Component< @observer export class TooltipContainer extends React.Component<{ tooltipProvider: TooltipManager - containerWidth: number - containerHeight: number + anchor?: GrapherTooltipAnchor + // if container dimensions are given, the tooltip will be positioned within its bounds + containerWidth?: number + containerHeight?: number }> { + @computed private get tooltip(): TooltipProps | undefined { + const { tooltip } = this.props.tooltipProvider + return tooltip?.get() + } + + @computed private get anchor(): GrapherTooltipAnchor { + return this.props.anchor ?? GrapherTooltipAnchor.mouse + } + + componentDidMount(): void { + document.addEventListener("click", this.onDocumentClick) + } + + componentWillUnmount(): void { + document.removeEventListener("click", this.onDocumentClick) + } + + @action.bound private onDocumentClick(): void { + const { tooltip } = this + if (tooltip?.shouldDismissOnClickOutside) tooltip?.dismiss?.() + } + @computed private get rendered(): React.ReactElement | null { - const tooltipsMap = this.props.tooltipProvider.tooltips - if (!tooltipsMap) return null - const tooltips = Object.entries(tooltipsMap.toJSON()) + const { tooltip } = this + if (!tooltip) return null + const isFixedToBottom = this.anchor === GrapherTooltipAnchor.bottom return ( -
- {tooltips.map(([id, tooltip]) => ( + +
- ))} -
+
+ ) } @@ -234,11 +292,11 @@ export class Tooltip extends React.Component { } @action.bound private connectTooltipToContainer(): void { - this.props.tooltipManager.tooltips?.set(this.props.id, this.props) + this.props.tooltipManager.tooltip?.set(this.props) } @action.bound private removeToolTipFromContainer(): void { - this.props.tooltipManager.tooltips?.delete(this.props.id) + this.props.tooltipManager.tooltip?.set(undefined) } componentDidUpdate(): void { diff --git a/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx b/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx index c83f3132235..7b10a798b3f 100644 --- a/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx +++ b/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx @@ -11,11 +11,14 @@ import { isNumber, sortBy, formatInlineList, + GrapherTooltipAnchor, + excludeUndefined, } from "@ourworldindata/utils" import { TooltipTableProps, TooltipValueProps, TooltipValueRangeProps, + TooltipContext, } from "./TooltipProps" export const NO_DATA_COLOR = "#999" @@ -167,6 +170,8 @@ class Variable extends React.Component<{ } export class TooltipTable extends React.Component { + static contextType = TooltipContext + render(): React.ReactElement | null { const { columns, totals, rows } = this.props, focal = rows.some((row) => row.focused), @@ -181,7 +186,21 @@ export class TooltipTable extends React.Component { tooTrivial = zip(columns, totals ?? []).every( ([column, total]) => !!column?.formatValueShort(total).match(/^100(\.0+)?%/) - ) + ), + showTotals = totals && !(tooEmpty || tooTrivial) + + // if the tooltip is pinned to the bottom, show the total at the top, + // so that it's always visible even if the tooltip is scrollable + const showTotalsAtTop = + this.context?.anchor === GrapherTooltipAnchor.bottom + + const totalsCells = zip(columns, totals!).map(([column, total]) => ( + + {column && total !== undefined + ? column.formatValueShort(total, format) + : null} + + )) return ( @@ -199,6 +218,16 @@ export class TooltipTable extends React.Component { )} + {showTotals && showTotalsAtTop && ( + <> + + + + {totalsCells} + + + + )} {rows.map((row) => { const { name, @@ -241,24 +270,26 @@ export class TooltipTable extends React.Component { )} - {zip(columns, values).map(([column, value]) => { - const missing = value === undefined - return column ? ( - + ) : null + } + )} {notice && ( ) })} - {totals && !(tooEmpty || tooTrivial) && ( + {showTotals && !showTotalsAtTop && ( <> - {zip(columns, totals).map(([column, total]) => ( - - ))} + {totalsCells} )} diff --git a/packages/@ourworldindata/grapher/src/tooltip/TooltipProps.ts b/packages/@ourworldindata/grapher/src/tooltip/TooltipProps.ts index fdbd269246c..9cdf38009a2 100644 --- a/packages/@ourworldindata/grapher/src/tooltip/TooltipProps.ts +++ b/packages/@ourworldindata/grapher/src/tooltip/TooltipProps.ts @@ -1,10 +1,14 @@ -import { ObservableMap } from "mobx" +import React from "react" import { CoreColumn } from "@ourworldindata/core-table" -import { TickFormattingOptions } from "@ourworldindata/utils" +import { + GrapherTooltipAnchor, + TickFormattingOptions, +} from "@ourworldindata/utils" +import { IObservableValue } from "mobx" // We can't pass the property directly because we need it to be observable. export interface TooltipManager { - tooltips?: ObservableMap + tooltip?: IObservableValue } export type TooltipFadeMode = "delayed" | "immediate" | "none" @@ -31,6 +35,8 @@ export interface TooltipProps { dissolve?: TooltipFadeMode // flag that the tooltip should begin fading out tooltipManager: TooltipManager children?: React.ReactNode + dismiss?: () => void + shouldDismissOnClickOutside?: boolean } export interface TooltipValueProps { @@ -71,3 +77,7 @@ export interface TooltipTableData { value: number fake?: boolean } + +export const TooltipContext = React.createContext<{ + anchor?: GrapherTooltipAnchor +}>({}) diff --git a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts index 72a958c57c8..05c80c40fb0 100644 --- a/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts +++ b/packages/@ourworldindata/types/src/grapherTypes/GrapherTypes.ts @@ -198,6 +198,13 @@ export enum ScatterPointLabelStrategy { y = "y", } +export enum GrapherTooltipAnchor { + // the tooltip is positioned relative to the mouse cursor + mouse = "mouse", + // the tooltip is pinned to the bottom of the screen + bottom = "bottom", +} + export interface AnnotationFieldsInTitle { entity?: boolean time?: boolean diff --git a/packages/@ourworldindata/types/src/index.ts b/packages/@ourworldindata/types/src/index.ts index 47793dae7df..914e651385b 100644 --- a/packages/@ourworldindata/types/src/index.ts +++ b/packages/@ourworldindata/types/src/index.ts @@ -103,6 +103,7 @@ export { type DetailsMarker, GrapherWindowType, AxisMinMaxValueStr, + GrapherTooltipAnchor, } from "./grapherTypes/GrapherTypes.js" export { From 9c53da7c436ec5b2f83a02d8e7ccc500fd95f22d Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 31 Oct 2024 09:43:49 +0100 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=94=A8=20(grapher)=20remove=20compute?= =?UTF-8?q?d=20props=20for=20frame=20padding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/captionedChart/CaptionedChart.tsx | 30 ++++--------------- .../grapher/src/chart/ChartTypeMap.tsx | 1 - .../src/controls/controlsRow/ControlsRow.tsx | 22 +++++--------- .../grapher/src/core/Grapher.tsx | 15 ++++------ .../grapher/src/core/GrapherConstants.ts | 4 ++- .../grapher/src/footer/Footer.tsx | 10 ++----- .../grapher/src/footer/FooterManager.ts | 1 - .../grapher/src/header/Header.tsx | 17 ++--------- .../grapher/src/header/HeaderManager.ts | 2 -- .../grapher/src/mapCharts/MapChart.tsx | 11 ++----- .../src/timeline/TimelineComponent.tsx | 11 ++----- 11 files changed, 30 insertions(+), 94 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx index 629381a7bb5..502c56a156c 100644 --- a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx +++ b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx @@ -19,8 +19,9 @@ import { BASE_FONT_SIZE, Patterns, STATIC_EXPORT_DETAIL_SPACING, - DEFAULT_GRAPHER_FRAME_PADDING, GRAPHER_BACKGROUND_DEFAULT, + GRAPHER_FRAME_PADDING_VERTICAL, + GRAPHER_FRAME_PADDING_HORIZONTAL, } from "../core/GrapherConstants" import { MapChartManager } from "../mapCharts/MapChartConstants" import { ChartManager } from "../chart/ChartManager" @@ -71,8 +72,6 @@ export interface CaptionedChartManager // layout & style isSmall?: boolean isMedium?: boolean - framePaddingHorizontal?: number - framePaddingVertical?: number fontSize?: number backgroundColor?: string @@ -109,6 +108,9 @@ const CONTROLS_ROW_HEIGHT = 32 @observer export class CaptionedChart extends React.Component { + protected framePaddingHorizontal = GRAPHER_FRAME_PADDING_HORIZONTAL + protected framePaddingVertical = GRAPHER_FRAME_PADDING_VERTICAL + @computed protected get manager(): CaptionedChartManager { return this.props.manager } @@ -124,18 +126,6 @@ export class CaptionedChart extends React.Component { ) } - @computed protected get framePaddingVertical(): number { - return ( - this.manager.framePaddingVertical ?? DEFAULT_GRAPHER_FRAME_PADDING - ) - } - - @computed protected get framePaddingHorizontal(): number { - return ( - this.manager.framePaddingHorizontal ?? DEFAULT_GRAPHER_FRAME_PADDING - ) - } - @computed protected get verticalPadding(): number { return this.manager.isSmall ? 8 : this.manager.isMedium ? 12 : 16 } @@ -229,7 +219,6 @@ export class CaptionedChart extends React.Component { bounds={bounds} manager={manager} containerElement={this.containerElement} - framePaddingHorizontal={this.framePaddingHorizontal} /> ) } @@ -358,7 +347,6 @@ export class CaptionedChart extends React.Component { ) } @@ -496,14 +484,6 @@ export class StaticCaptionedChart extends CaptionedChart { }) } - @computed protected get framePaddingVertical(): number { - return DEFAULT_GRAPHER_FRAME_PADDING - } - - @computed protected get framePaddingHorizontal(): number { - return DEFAULT_GRAPHER_FRAME_PADDING - } - @computed private get paddedBounds(): Bounds { return this.bounds .padWidth(this.framePaddingHorizontal) diff --git a/packages/@ourworldindata/grapher/src/chart/ChartTypeMap.tsx b/packages/@ourworldindata/grapher/src/chart/ChartTypeMap.tsx index 2fbb5f8576c..5f8fa7bd974 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartTypeMap.tsx +++ b/packages/@ourworldindata/grapher/src/chart/ChartTypeMap.tsx @@ -17,7 +17,6 @@ interface ChartComponentProps { manager: ChartManager bounds?: Bounds containerElement?: HTMLDivElement - framePaddingHorizontal?: number } interface ChartComponentClass extends ComponentClass { diff --git a/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx b/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx index 0ae2db8b681..a2c5f9d38c2 100644 --- a/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx +++ b/packages/@ourworldindata/grapher/src/controls/controlsRow/ControlsRow.tsx @@ -14,7 +14,10 @@ import { MapProjectionMenuManager, } from "../MapProjectionMenu" import { SettingsMenu, SettingsMenuManager } from "../SettingsMenu" -import { DEFAULT_GRAPHER_FRAME_PADDING } from "../../core/GrapherConstants" +import { + GRAPHER_FRAME_PADDING_HORIZONTAL, + GRAPHER_FRAME_PADDING_VERTICAL, +} from "../../core/GrapherConstants" export interface ControlsRowManager extends ContentSwitchersManager, @@ -24,8 +27,6 @@ export interface ControlsRowManager sidePanelBounds?: Bounds availableTabs?: GrapherTabOption[] showEntitySelectionToggle?: boolean - framePaddingHorizontal?: number - framePaddingVertical?: number } @observer @@ -34,6 +35,9 @@ export class ControlsRow extends React.Component<{ maxWidth?: number settingsMenuTop?: number }> { + private framePaddingHorizontal = GRAPHER_FRAME_PADDING_HORIZONTAL + private framePaddingVertical = GRAPHER_FRAME_PADDING_VERTICAL + static shouldShow(manager: ControlsRowManager): boolean { const test = new ControlsRow({ manager }) return test.showControlsRow @@ -47,18 +51,6 @@ export class ControlsRow extends React.Component<{ return this.props.maxWidth ?? DEFAULT_BOUNDS.width } - @computed private get framePaddingHorizontal(): number { - return ( - this.manager.framePaddingHorizontal ?? DEFAULT_GRAPHER_FRAME_PADDING - ) - } - - @computed private get framePaddingVertical(): number { - return ( - this.manager.framePaddingVertical ?? DEFAULT_GRAPHER_FRAME_PADDING - ) - } - @computed private get sidePanelWidth(): number { return this.manager.sidePanelBounds?.width ?? 0 } diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 6b52d454a4b..9b8b692e2c1 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -120,7 +120,6 @@ import { ThereWasAProblemLoadingThisChart, DEFAULT_GRAPHER_WIDTH, DEFAULT_GRAPHER_HEIGHT, - DEFAULT_GRAPHER_FRAME_PADDING, DEFAULT_GRAPHER_ENTITY_TYPE, DEFAULT_GRAPHER_ENTITY_TYPE_PLURAL, GRAPHER_DARK_TEXT, @@ -131,6 +130,8 @@ import { isPopulationVariableETLPath, GRAPHER_BACKGROUND_BEIGE, GRAPHER_BACKGROUND_DEFAULT, + GRAPHER_FRAME_PADDING_HORIZONTAL, + GRAPHER_FRAME_PADDING_VERTICAL, } from "../core/GrapherConstants" import { defaultGrapherConfig } from "../schema/defaultGrapherConfig" import { loadVariableDataAndMetadata } from "./loadVariable" @@ -465,6 +466,10 @@ export class Grapher this.props.dataApiUrl ?? "https://api.ourworldindata.org/v1/indicators/" @observable.ref externalQueryParams: QueryParams + + private framePaddingHorizontal = GRAPHER_FRAME_PADDING_HORIZONTAL + private framePaddingVertical = GRAPHER_FRAME_PADDING_VERTICAL + @observable.ref inputTable: OwidTable @observable.ref legacyConfigAsAuthored: Partial = {} @@ -2924,14 +2929,6 @@ export class Grapher return this.props.baseFontSize ?? this.baseFontSize } - @computed get framePaddingHorizontal(): number { - return DEFAULT_GRAPHER_FRAME_PADDING - } - - @computed get framePaddingVertical(): number { - return DEFAULT_GRAPHER_FRAME_PADDING - } - @computed get isNarrow(): boolean { if (this.isStatic) return false return this.frameBounds.width <= 420 diff --git a/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts b/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts index 3fabd87cb2a..86042194b1b 100644 --- a/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts +++ b/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts @@ -17,7 +17,9 @@ export const GRAPHER_LOADED_EVENT_NAME = "grapherLoaded" export const DEFAULT_GRAPHER_WIDTH = 850 export const DEFAULT_GRAPHER_HEIGHT = 600 -export const DEFAULT_GRAPHER_FRAME_PADDING = 16 +export const GRAPHER_FRAME_PADDING_VERTICAL = 16 +export const GRAPHER_FRAME_PADDING_HORIZONTAL = 16 + export const STATIC_EXPORT_DETAIL_SPACING = 8 export const GRAPHER_BACKGROUND_DEFAULT = "#ffffff" diff --git a/packages/@ourworldindata/grapher/src/footer/Footer.tsx b/packages/@ourworldindata/grapher/src/footer/Footer.tsx index f731f5978bf..275486dc7e5 100644 --- a/packages/@ourworldindata/grapher/src/footer/Footer.tsx +++ b/packages/@ourworldindata/grapher/src/footer/Footer.tsx @@ -18,7 +18,7 @@ import { FooterManager } from "./FooterManager" import { ActionButtons } from "../controls/ActionButtons" import { BASE_FONT_SIZE, - DEFAULT_GRAPHER_FRAME_PADDING, + GRAPHER_FRAME_PADDING_HORIZONTAL, GRAPHER_DARK_TEXT, } from "../core/GrapherConstants" @@ -82,12 +82,6 @@ export class Footer< return this.props.maxWidth ?? DEFAULT_BOUNDS.width } - @computed private get framePaddingHorizontal(): number { - return ( - this.manager.framePaddingHorizontal ?? DEFAULT_GRAPHER_FRAME_PADDING - ) - } - @computed protected get useBaseFontSize(): boolean { return !!this.manager.useBaseFontSize } @@ -592,7 +586,7 @@ export class Footer<
diff --git a/packages/@ourworldindata/grapher/src/footer/FooterManager.ts b/packages/@ourworldindata/grapher/src/footer/FooterManager.ts index ca0e777f992..e8d81bb0da8 100644 --- a/packages/@ourworldindata/grapher/src/footer/FooterManager.ts +++ b/packages/@ourworldindata/grapher/src/footer/FooterManager.ts @@ -12,7 +12,6 @@ export interface FooterManager extends TooltipManager, ActionButtonsManager { isSourcesModalOpen?: boolean isSmall?: boolean isMedium?: boolean - framePaddingHorizontal?: number useBaseFontSize?: boolean fontSize?: number isInFullScreenMode?: boolean diff --git a/packages/@ourworldindata/grapher/src/header/Header.tsx b/packages/@ourworldindata/grapher/src/header/Header.tsx index 386faa68595..0ff25e6ed1c 100644 --- a/packages/@ourworldindata/grapher/src/header/Header.tsx +++ b/packages/@ourworldindata/grapher/src/header/Header.tsx @@ -14,7 +14,8 @@ import { Logo } from "../captionedChart/Logos" import { HeaderManager } from "./HeaderManager" import { BASE_FONT_SIZE, - DEFAULT_GRAPHER_FRAME_PADDING, + GRAPHER_FRAME_PADDING_HORIZONTAL, + GRAPHER_FRAME_PADDING_VERTICAL, GRAPHER_DARK_TEXT, } from "../core/GrapherConstants" @@ -35,18 +36,6 @@ export class Header< return this.props.maxWidth ?? DEFAULT_BOUNDS.width } - @computed private get framePaddingHorizontal(): number { - return ( - this.manager.framePaddingHorizontal ?? DEFAULT_GRAPHER_FRAME_PADDING - ) - } - - @computed private get framePaddingVertical(): number { - return ( - this.manager.framePaddingVertical ?? DEFAULT_GRAPHER_FRAME_PADDING - ) - } - @computed protected get useBaseFontSize(): boolean { return !!this.manager.useBaseFontSize } @@ -276,7 +265,7 @@ export class Header<
diff --git a/packages/@ourworldindata/grapher/src/header/HeaderManager.ts b/packages/@ourworldindata/grapher/src/header/HeaderManager.ts index 3d0c9a1dca0..d9b4c598c0c 100644 --- a/packages/@ourworldindata/grapher/src/header/HeaderManager.ts +++ b/packages/@ourworldindata/grapher/src/header/HeaderManager.ts @@ -16,8 +16,6 @@ export interface HeaderManager { isSmall?: boolean isMedium?: boolean isSemiNarrow?: boolean - framePaddingHorizontal?: number - framePaddingVertical?: number isOnCanonicalUrl?: boolean isInIFrame?: boolean useBaseFontSize?: boolean diff --git a/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx b/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx index 0a3deb9f85d..77d944ae8d7 100644 --- a/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx +++ b/packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx @@ -44,7 +44,7 @@ import { MapConfig } from "./MapConfig" import { ColorScale, ColorScaleManager } from "../color/ColorScale" import { BASE_FONT_SIZE, - DEFAULT_GRAPHER_FRAME_PADDING, + GRAPHER_FRAME_PADDING_HORIZONTAL, Patterns, } from "../core/GrapherConstants" import { ChartInterface } from "../chart/ChartInterface" @@ -81,7 +81,6 @@ interface MapChartProps { bounds?: Bounds manager: MapChartManager containerElement?: HTMLDivElement - framePaddingHorizontal?: number } // Get the underlying geographical topology elements we're going to display @@ -235,12 +234,6 @@ export class MapChart return this.seriesMap } - @computed private get framePaddingHorizontal(): number { - return ( - this.props.framePaddingHorizontal ?? DEFAULT_GRAPHER_FRAME_PADDING - ) - } - base: React.RefObject = React.createRef() @action.bound onMapMouseOver(feature: GeoFeature): void { const series = @@ -634,7 +627,7 @@ export class MapChart const { tooltipState } = this const sparklineWidth = this.manager.shouldPinTooltipToBottom - ? this.bounds.width + (this.framePaddingHorizontal - 1) * 2 + ? this.bounds.width + (GRAPHER_FRAME_PADDING_HORIZONTAL - 1) * 2 : undefined return ( diff --git a/packages/@ourworldindata/grapher/src/timeline/TimelineComponent.tsx b/packages/@ourworldindata/grapher/src/timeline/TimelineComponent.tsx index 49e1e295bae..f78014f1a06 100644 --- a/packages/@ourworldindata/grapher/src/timeline/TimelineComponent.tsx +++ b/packages/@ourworldindata/grapher/src/timeline/TimelineComponent.tsx @@ -14,7 +14,7 @@ import { faPlay, faPause } from "@fortawesome/free-solid-svg-icons" import { TimelineController, TimelineManager } from "./TimelineController" import { ActionButton } from "../controls/ActionButtons" import { - DEFAULT_GRAPHER_FRAME_PADDING, + GRAPHER_FRAME_PADDING_HORIZONTAL, GRAPHER_TIMELINE_CLASS, } from "../core/GrapherConstants.js" @@ -26,7 +26,6 @@ const HANDLE_TOOLTIP_FADE_TIME_MS = 2000 export class TimelineComponent extends React.Component<{ timelineController: TimelineController maxWidth?: number - framePaddingHorizontal?: number }> { base: React.RefObject = React.createRef() @@ -36,12 +35,6 @@ export class TimelineComponent extends React.Component<{ return this.props.maxWidth ?? DEFAULT_BOUNDS.width } - @computed private get framePaddingHorizontal(): number { - return ( - this.props.framePaddingHorizontal ?? DEFAULT_GRAPHER_FRAME_PADDING - ) - } - @computed private get isDragging(): boolean { return !!this.dragTarget } @@ -341,7 +334,7 @@ export class TimelineComponent extends React.Component<{ hover: this.mouseHoveringOverTimeline, })} style={{ - padding: `0 ${this.framePaddingHorizontal}px`, + padding: `0 ${GRAPHER_FRAME_PADDING_HORIZONTAL}px`, }} onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} From b144c5e4b7f55ea1074ca1dad1d4c33981f8b1ee Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 7 Nov 2024 13:52:33 +0100 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=90=9B=20(grapher)=20dismiss=20toolti?= =?UTF-8?q?ps=20more=20reliably?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/chart/ChartUtils.tsx | 13 +++++++ .../src/controls/EntitySelectionToggle.tsx | 2 -- .../grapher/src/controls/SettingsMenu.tsx | 8 +++-- .../grapher/src/core/Grapher.tsx | 5 --- .../grapher/src/fullScreen/FullScreen.tsx | 6 ++-- .../grapher/src/lineCharts/LineChart.tsx | 28 +++++++++------ .../grapher/src/modal/Modal.tsx | 12 +++---- .../src/slideInDrawer/SlideInDrawer.tsx | 4 +-- .../src/stackedCharts/StackedAreaChart.tsx | 35 ++++++++++++------- .../grapher/src/tooltip/Tooltip.tsx | 13 ------- .../grapher/src/tooltip/TooltipProps.ts | 1 - 11 files changed, 68 insertions(+), 59 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx index 7a17f9980dd..c6285eabed2 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx +++ b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx @@ -107,3 +107,16 @@ export function getShortNameForEntity(entityName: string): string | undefined { const country = getCountryByName(entityName) return country?.shortName } + +export function isTargetOutsideElement( + target: EventTarget, + element: Node +): boolean { + const targetNode = target as Node + return ( + !element.contains(targetNode) && + // check that the target is still mounted to the document (we also get + // click events on nodes that have since been removed by React) + document.contains(targetNode) + ) +} diff --git a/packages/@ourworldindata/grapher/src/controls/EntitySelectionToggle.tsx b/packages/@ourworldindata/grapher/src/controls/EntitySelectionToggle.tsx index 80201e0eef0..3df6fa62294 100644 --- a/packages/@ourworldindata/grapher/src/controls/EntitySelectionToggle.tsx +++ b/packages/@ourworldindata/grapher/src/controls/EntitySelectionToggle.tsx @@ -18,7 +18,6 @@ export interface EntitySelectionManager { isEntitySelectorModalOrDrawerOpen?: boolean isOnChartTab?: boolean hideEntityControls?: boolean - onEntitySelectorOpen?: () => void } interface EntitySelectionLabel { @@ -83,7 +82,6 @@ export class EntitySelectionToggle extends React.Component<{ onClick={(e): void => { this.props.manager.isEntitySelectorModalOrDrawerOpen = !active - this.props.manager.onEntitySelectorOpen?.() e.stopPropagation() }} type="button" diff --git a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx index 53cfcae4414..daedbd24493 100644 --- a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx +++ b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx @@ -8,7 +8,10 @@ import { EntityName, ChartTypeName, FacetStrategy } from "@ourworldindata/types" import { DEFAULT_BOUNDS } from "@ourworldindata/utils" import { SelectionArray } from "../selection/SelectionArray" import { ChartDimension } from "../chart/ChartDimension" -import { makeSelectionArray } from "../chart/ChartUtils.js" +import { + isTargetOutsideElement, + makeSelectionArray, +} from "../chart/ChartUtils.js" import { AxisConfig } from "../axis/AxisConfig" import { AxisScaleToggle } from "./settings/AxisScaleToggle" @@ -245,8 +248,7 @@ export class SettingsMenu extends React.Component<{ if ( this.active && this.contentRef?.current && - !this.contentRef.current.contains(e.target as Node) && - document.contains(e.target as Node) + isTargetOutsideElement(e.target!, this.contentRef.current) ) this.toggleVisibility() } diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 9b8b692e2c1..622c6479d20 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -3467,11 +3467,6 @@ export class Grapher ) } - @action.bound onEntitySelectorOpen(): void { - const tooltip = this.tooltip?.get() - if (tooltip) tooltip.dismiss?.() - } - // This is just a helper method to return the correct table for providing entity choices. We want to // provide the root table, not the transformed table. // A user may have added time or other filters that would filter out all rows from certain entities, but diff --git a/packages/@ourworldindata/grapher/src/fullScreen/FullScreen.tsx b/packages/@ourworldindata/grapher/src/fullScreen/FullScreen.tsx index f6ab10ba05d..341d94474df 100644 --- a/packages/@ourworldindata/grapher/src/fullScreen/FullScreen.tsx +++ b/packages/@ourworldindata/grapher/src/fullScreen/FullScreen.tsx @@ -2,6 +2,7 @@ import React from "react" import { action } from "mobx" import { observer } from "mobx-react" import { BodyDiv } from "../bodyDiv/BodyDiv" +import { isTargetOutsideElement } from "../chart/ChartUtils" @observer export class FullScreen extends React.Component<{ @@ -14,10 +15,7 @@ export class FullScreen extends React.Component<{ @action.bound onDocumentClick(e: React.MouseEvent): void { if ( this.content?.current && - // check if the click was outside of the modal - !this.content.current.contains(e.target as Node) && - // check that the target is still mounted to the document; we also get click events on nodes that have since been removed by React - document.contains(e.target as Node) + isTargetOutsideElement(e.target, this.content.current) ) this.props.onDismiss() } diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index ee76ad5eb09..f0dd074b7af 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -85,6 +85,7 @@ import { autoDetectYColumnSlugs, getDefaultFailMessage, getSeriesKey, + isTargetOutsideElement, makeClipPath, makeSelectionArray, } from "../chart/ChartUtils" @@ -447,13 +448,6 @@ export class LineChart this.tooltipState.target = null } - @action.bound onClick(e: React.MouseEvent): void { - // don't fire document event handler that dismisses the tooltip - if (this.manager.shouldPinTooltipToBottom) { - e.stopPropagation() - } - } - @action.bound private onCursorLeave(): void { if (!this.manager.shouldPinTooltipToBottom) { this.dismissTooltip() @@ -705,9 +699,6 @@ export class LineChart footer={footer} dissolve={fading} dismiss={this.dismissTooltip} - shouldDismissOnClickOutside={ - this.manager.shouldPinTooltipToBottom - } > 0 } + @action.bound onDocumentClick(e: MouseEvent): void { + // only dismiss the tooltip if the click is outside of the chart area + if ( + !this.base.current || + isTargetOutsideElement(e.target as Node, this.base.current) + ) { + this.dismissTooltip() + } + } + animSelection?: d3.Selection< d3.BaseType, unknown, @@ -804,10 +805,16 @@ export class LineChart this.runFancyIntroAnimation() } exposeInstanceOnWindow(this) + document.addEventListener("click", this.onDocumentClick, { + capture: true, + }) } componentWillUnmount(): void { if (this.animSelection) this.animSelection.interrupt() + document.removeEventListener("click", this.onDocumentClick, { + capture: true, + }) } @computed get renderUid(): number { @@ -956,7 +963,6 @@ export class LineChart void @@ -48,8 +49,7 @@ export class SlideInDrawer extends React.Component<{ if ( this.active && this.drawerRef?.current && - !this.drawerRef.current.contains(e.target as Node) && - document.contains(e.target as Node) + isTargetOutsideElement(e.target!, this.drawerRef.current) ) this.toggleVisibility() } diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx index 8134d05842d..0621b1bb817 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx @@ -52,7 +52,7 @@ import { StackedSeries, } from "./StackedConstants" import { stackSeries, withMissingValuesAsZeroes } from "./StackedUtils" -import { makeClipPath } from "../chart/ChartUtils" +import { makeClipPath, isTargetOutsideElement } from "../chart/ChartUtils" import { bind } from "decko" import { AxisConfig } from "../axis/AxisConfig.js" @@ -411,13 +411,6 @@ export class StackedAreaChart ) } - @action.bound onClick(e: React.MouseEvent): void { - // don't fire document event handler that dismisses the tooltip - if (this.manager.shouldPinTooltipToBottom) { - e.stopPropagation() - } - } - @action.bound private onCursorMove( ev: React.MouseEvent | React.TouchEvent ): void { @@ -564,9 +557,6 @@ export class StackedAreaChart footer={footer} dissolve={fading} dismiss={this.dismissTooltip} - shouldDismissOnClickOutside={ - this.manager.shouldPinTooltipToBottom - } > void - shouldDismissOnClickOutside?: boolean } export interface TooltipValueProps { From bc2deaf0358cf4c0e679fee2b596630d2e4642d7 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 7 Nov 2024 17:17:45 +0100 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=94=A8=20(tooltip)=20get=20rid=20of?= =?UTF-8?q?=20mobx=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/tooltip/TooltipContents.tsx | 7 +++++-- packages/@ourworldindata/utils/src/Util.ts | 11 +++++++++++ packages/@ourworldindata/utils/src/index.ts | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx b/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx index 7b10a798b3f..5b1c9723024 100644 --- a/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx +++ b/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx @@ -12,7 +12,7 @@ import { sortBy, formatInlineList, GrapherTooltipAnchor, - excludeUndefined, + ensureEqualLength, } from "@ourworldindata/utils" import { TooltipTableProps, @@ -270,7 +270,10 @@ export class TooltipTable extends React.Component { )} - {zip(columns, excludeUndefined(values)).map( + {/* ensure arrays of equal length are zipped to + avoid a mobx warning about accessing elements + that are out of bounds */} + {zip(...ensureEqualLength(columns, values)).map( ([column, value]) => { const missing = value === undefined return column ? ( diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts index 5c93748b3fd..4fa9a6bb66c 100644 --- a/packages/@ourworldindata/utils/src/Util.ts +++ b/packages/@ourworldindata/utils/src/Util.ts @@ -2000,3 +2000,14 @@ export function getPaginationPageNumbers( return pageNumbers } + +export function ensureEqualLength( + arr1: T[], + arr2: U[] +): [(T | undefined)[], (U | undefined)[]] { + const length = Math.max(arr1.length, arr2.length) + return [ + [...arr1, ...new Array(length - arr1.length).fill(undefined)], + [...arr2, ...new Array(length - arr2.length).fill(undefined)], + ] +} diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts index eacf00917fa..c4ae332b7d0 100644 --- a/packages/@ourworldindata/utils/src/index.ts +++ b/packages/@ourworldindata/utils/src/index.ts @@ -124,6 +124,7 @@ export { formatInlineList, lazy, getParentVariableIdFromChartConfig, + ensureEqualLength, } from "./Util.js" export { From 4a6937a10c4970b0d2ea86a90fe932c21f0dfca5 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 7 Nov 2024 17:18:28 +0100 Subject: [PATCH 5/9] =?UTF-8?q?=F0=9F=92=84=20(tooltip)=20align=20total=20?= =?UTF-8?q?row=20in=20mobile=20tooltips?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss b/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss index 0359f682f95..c8e74e151f4 100644 --- a/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss +++ b/packages/@ourworldindata/grapher/src/tooltip/Tooltip.scss @@ -294,7 +294,8 @@ td:nth-child(2), td:nth-child(3) { border-bottom: 1px solid $background-stroke; - vertical-align: top; + position: relative; + top: -2px; } } From d055b0181bcdbd4c02d4eb23f50b89cb49ac744a Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 7 Nov 2024 17:19:41 +0100 Subject: [PATCH 6/9] =?UTF-8?q?=F0=9F=94=A8=20(tooltip)=20dismiss=20mobile?= =?UTF-8?q?=20tooltips=20in=20faceted=20charts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineCharts/LineChart.tsx | 20 +++++++++++++------ .../src/stackedCharts/StackedAreaChart.tsx | 20 +++++++++++++------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index f0dd074b7af..e8e73b53012 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -103,6 +103,8 @@ import { HorizontalNumericColorLegend, } from "../horizontalColorLegend/HorizontalColorLegends" +const LINE_CHART_CLASS_NAME = "LineChart" + // line color const BLUR_LINE_COLOR = "#eee" const DEFAULT_LINE_COLOR = "#000" @@ -786,10 +788,16 @@ export class LineChart @action.bound onDocumentClick(e: MouseEvent): void { // only dismiss the tooltip if the click is outside of the chart area - if ( - !this.base.current || - isTargetOutsideElement(e.target as Node, this.base.current) - ) { + // and outside of the chart areas of neighbouring facets + const chartContainer = this.manager.base?.current + if (!chartContainer) return + const chartAreas = chartContainer.getElementsByClassName( + LINE_CHART_CLASS_NAME + ) + const isTargetOutsideChartAreas = Array.from(chartAreas).every( + (chartArea) => isTargetOutsideElement(e.target!, chartArea) + ) + if (isTargetOutsideChartAreas) { this.dismissTooltip() } } @@ -962,7 +970,7 @@ export class LineChart return ( + {this.renderDualAxis()} { onAreaMouseLeave?: () => void } +const STACKED_AREA_CHART_CLASS_NAME = "StackedArea" + const BLUR_COLOR = "#ddd" @observer @@ -592,10 +594,16 @@ export class StackedAreaChart @action.bound onDocumentClick(e: MouseEvent): void { // only dismiss the tooltip if the click is outside of the chart area - if ( - !this.base.current || - isTargetOutsideElement(e.target as Node, this.base.current) - ) { + // and outside of the chart areas of neighbouring facets + const chartContainer = this.manager.base?.current + if (!chartContainer) return + const chartAreas = chartContainer.getElementsByClassName( + STACKED_AREA_CHART_CLASS_NAME + ) + const isTargetOutsideChartAreas = Array.from(chartAreas).every( + (chartArea) => isTargetOutsideElement(e.target!, chartArea) + ) + if (isTargetOutsideChartAreas) { this.dismissTooltip() } } @@ -661,7 +669,7 @@ export class StackedAreaChart return ( + {this.renderAxis()} Date: Fri, 8 Nov 2024 10:44:42 +0100 Subject: [PATCH 7/9] =?UTF-8?q?=F0=9F=94=A8=20(tooltip)=20get=20rid=20of?= =?UTF-8?q?=20mobx=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../grapher/src/lineCharts/LineChart.tsx | 4 +- .../grapher/src/tooltip/TooltipContents.tsx | 40 ++++++++----------- packages/@ourworldindata/utils/src/Util.ts | 11 ----- packages/@ourworldindata/utils/src/index.ts | 1 - 4 files changed, 19 insertions(+), 37 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index e8e73b53012..2559f9e8a62 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -724,10 +724,10 @@ export class LineChart ) : series.color - const values = [ + const values = excludeUndefined([ point?.y, point?.colorValue as undefined | number, - ] + ]) return { name, diff --git a/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx b/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx index 5b1c9723024..da807f42e70 100644 --- a/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx +++ b/packages/@ourworldindata/grapher/src/tooltip/TooltipContents.tsx @@ -12,7 +12,6 @@ import { sortBy, formatInlineList, GrapherTooltipAnchor, - ensureEqualLength, } from "@ourworldindata/utils" import { TooltipTableProps, @@ -270,29 +269,24 @@ export class TooltipTable extends React.Component { )} - {/* ensure arrays of equal length are zipped to - avoid a mobx warning about accessing elements - that are out of bounds */} - {zip(...ensureEqualLength(columns, values)).map( - ([column, value]) => { - const missing = value === undefined - return column ? ( -
- ) : null - } - )} + + ) : null + })} {notice && (
Total
- {!missing && - column.formatValueShort( - value, - format + {zip(columns, excludeUndefined(values)).map( + ([column, value]) => { + const missing = value === undefined + return column ? ( + - ) : null - })} + > + {!missing && + column.formatValueShort( + value, + format + )} + {" "} @@ -268,25 +299,13 @@ export class TooltipTable extends React.Component {
Total - {column && total !== undefined - ? column.formatValueShort( - total, - format - ) - : null} -
{ + const missing = value === undefined + return column ? ( + + {!missing && + column.formatValueShort( + value, + format )} - > - {!missing && - column.formatValueShort( - value, - format - )} - {" "} diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts index 4fa9a6bb66c..5c93748b3fd 100644 --- a/packages/@ourworldindata/utils/src/Util.ts +++ b/packages/@ourworldindata/utils/src/Util.ts @@ -2000,14 +2000,3 @@ export function getPaginationPageNumbers( return pageNumbers } - -export function ensureEqualLength( - arr1: T[], - arr2: U[] -): [(T | undefined)[], (U | undefined)[]] { - const length = Math.max(arr1.length, arr2.length) - return [ - [...arr1, ...new Array(length - arr1.length).fill(undefined)], - [...arr2, ...new Array(length - arr2.length).fill(undefined)], - ] -} diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts index c4ae332b7d0..eacf00917fa 100644 --- a/packages/@ourworldindata/utils/src/index.ts +++ b/packages/@ourworldindata/utils/src/index.ts @@ -124,7 +124,6 @@ export { formatInlineList, lazy, getParentVariableIdFromChartConfig, - ensureEqualLength, } from "./Util.js" export { From 13d45327ede5a73da7a338f860103fce5ccffed0 Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Fri, 8 Nov 2024 11:15:44 +0100 Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=90=9B=20(tooltip)=20fix=20mobx=20iss?= =?UTF-8?q?ue=20for=20tooltips=20in=20the=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/core/Grapher.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 622c6479d20..77b446373b9 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -870,7 +870,9 @@ export class Grapher @observable.ref isExportingToSvgOrPng = false @observable.ref isSocialMediaExport = false - tooltip?: TooltipManager["tooltip"] = observable.box(undefined) + tooltip?: TooltipManager["tooltip"] = observable.box(undefined, { + deep: false, + }) @observable.ref isPlaying = false @observable.ref isTimelineAnimationActive = false // true if the timeline animation is either playing or paused but not finished From 613742db294498b964138c87362773da059855ed Mon Sep 17 00:00:00 2001 From: sophiamersmann Date: Thu, 14 Nov 2024 15:01:13 +0100 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=94=A8=20fix=20rebase=20gone=20wrong?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx b/packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx index 509b6c23213..7fa2a61dbfb 100644 --- a/packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx +++ b/packages/@ourworldindata/grapher/src/mapCharts/MapSparkline.tsx @@ -116,7 +116,7 @@ export class MapSparkline extends React.Component<{ fontSize: 11, disableIntroAnimation: true, lineStrokeWidth: 2, - annotation: { + entityYearHighlight: { entityName: this.manager.entityName, year: this.manager.datum?.time, },