Skip to content

Commit

Permalink
Merge pull request #4082 from owid/mobile-tooltips-fixed-bottom
Browse files Browse the repository at this point in the history
✨ (grapher) pin tooltips to the bottom on mobile / TAS-664
  • Loading branch information
sophiamersmann authored Nov 14, 2024
2 parents 040e8c2 + 613742d commit d1926da
Show file tree
Hide file tree
Showing 34 changed files with 1,133 additions and 843 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -71,8 +72,6 @@ export interface CaptionedChartManager
// layout & style
isSmall?: boolean
isMedium?: boolean
framePaddingHorizontal?: number
framePaddingVertical?: number
fontSize?: number
backgroundColor?: string

Expand Down Expand Up @@ -109,6 +108,9 @@ const CONTROLS_ROW_HEIGHT = 32

@observer
export class CaptionedChart extends React.Component<CaptionedChartProps> {
protected framePaddingHorizontal = GRAPHER_FRAME_PADDING_HORIZONTAL
protected framePaddingVertical = GRAPHER_FRAME_PADDING_VERTICAL

@computed protected get manager(): CaptionedChartManager {
return this.props.manager
}
Expand All @@ -124,18 +126,6 @@ export class CaptionedChart extends React.Component<CaptionedChartProps> {
)
}

@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
}
Expand Down Expand Up @@ -357,7 +347,6 @@ export class CaptionedChart extends React.Component<CaptionedChartProps> {
<TimelineComponent
timelineController={this.manager.timelineController!}
maxWidth={this.maxWidth}
framePaddingHorizontal={this.framePaddingHorizontal}
/>
)
}
Expand Down Expand Up @@ -495,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)
Expand Down
3 changes: 2 additions & 1 deletion packages/@ourworldindata/grapher/src/chart/ChartManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -92,6 +92,7 @@ export interface ChartManager {
isExportingForSocialMedia?: boolean
secondaryColorInStaticCharts?: string
backgroundColor?: Color
shouldPinTooltipToBottom?: boolean

detailsOrderedByReference?: string[]
detailsMarkerInSvg?: DetailsMarker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { MarimekkoChart } from "../stackedCharts/MarimekkoChart"
interface ChartComponentProps {
manager: ChartManager
bounds?: Bounds
containerElement?: any // todo: remove?
containerElement?: HTMLDivElement
}

interface ChartComponentClass extends ComponentClass<ChartComponentProps> {
Expand Down
13 changes: 13 additions & 0 deletions packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -24,8 +27,6 @@ export interface ControlsRowManager
sidePanelBounds?: Bounds
availableTabs?: GrapherTabOption[]
showEntitySelectionToggle?: boolean
framePaddingHorizontal?: number
framePaddingVertical?: number
}

@observer
Expand All @@ -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
Expand All @@ -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
}
Expand Down
93 changes: 64 additions & 29 deletions packages/@ourworldindata/grapher/src/core/Grapher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
sortBy,
extractDetailsFromSyntax,
omit,
isTouchDevice,
} from "@ourworldindata/utils"
import {
MarkdownTextWrap,
Expand Down Expand Up @@ -105,6 +106,7 @@ import {
GrapherWindowType,
Color,
GRAPHER_QUERY_PARAM_KEYS,
GrapherTooltipAnchor,
} from "@ourworldindata/types"
import {
BlankOwidTable,
Expand All @@ -118,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,
Expand All @@ -129,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"
Expand Down Expand Up @@ -210,6 +213,7 @@ import {
type EntitySelectorState,
} from "../entitySelector/EntitySelector"
import { SlideInDrawer } from "../slideInDrawer/SlideInDrawer"
import { BodyDiv } from "../bodyDiv/BodyDiv"

declare global {
interface Window {
Expand Down Expand Up @@ -462,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<LegacyGrapherInterface> = {}
Expand Down Expand Up @@ -862,7 +870,9 @@ export class Grapher
@observable.ref isExportingToSvgOrPng = false
@observable.ref isSocialMediaExport = false

tooltips?: TooltipManager["tooltips"] = observable.map({}, { deep: false })
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
Expand Down Expand Up @@ -2184,6 +2194,10 @@ export class Grapher
return isMobile()
}

@computed get isTouchDevice(): boolean {
return isTouchDevice()
}

@computed private get externalBounds(): Bounds {
return this.props.bounds ?? DEFAULT_BOUNDS
}
Expand Down Expand Up @@ -2820,33 +2834,53 @@ export class Grapher
<EntitySelector manager={this} autoFocus={true} />
</SlideInDrawer>

{/* tooltip */}
<TooltipContainer
containerWidth={this.captionedChartBounds.width}
containerHeight={this.captionedChartBounds.height}
tooltipProvider={this}
/>
{/* tooltip: either pin to the bottom or render into the chart area */}
{this.shouldPinTooltipToBottom ? (
<BodyDiv>
<TooltipContainer
tooltipProvider={this}
anchor={GrapherTooltipAnchor.bottom}
/>
</BodyDiv>
) : (
<TooltipContainer
tooltipProvider={this}
containerWidth={this.captionedChartBounds.width}
containerHeight={this.captionedChartBounds.height}
/>
)}
</>
)
}

// 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 {
Expand Down Expand Up @@ -2897,17 +2931,9 @@ 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 <= 400
return this.frameBounds.width <= 420
}

// SemiNarrow charts shorten their button labels to fit within the controls row
Expand Down Expand Up @@ -2960,6 +2986,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 {
Expand Down Expand Up @@ -3294,6 +3324,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading

0 comments on commit d1926da

Please sign in to comment.