Skip to content

Commit

Permalink
✨ (grapher) show tooltip at the bottom on mobile
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Oct 30, 2024
1 parent 8b07d32 commit bae4058
Show file tree
Hide file tree
Showing 24 changed files with 1,081 additions and 751 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export class CaptionedChart extends React.Component<CaptionedChartProps> {
bounds={bounds}
manager={manager}
containerElement={this.containerElement}
framePaddingHorizontal={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 @@ -39,7 +39,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 @@ -99,6 +99,7 @@ export interface ChartManager {
isExportingForSocialMedia?: boolean
secondaryColorInStaticCharts?: string
backgroundColor?: Color
shouldPinTooltipToBottom?: boolean

detailsOrderedByReference?: string[]
detailsMarkerInSvg?: DetailsMarker
Expand Down
3 changes: 2 additions & 1 deletion packages/@ourworldindata/grapher/src/chart/ChartTypeMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChartComponentProps> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface EntitySelectionManager {
isEntitySelectorModalOrDrawerOpen?: boolean
isOnChartTab?: boolean
hideEntityControls?: boolean
onEntitySelectorOpen?: () => void
}

interface EntitySelectionLabel {
Expand Down Expand Up @@ -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"
Expand Down
74 changes: 58 additions & 16 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,
MultiDimDataPageProps,
Color,
GrapherTooltipAnchor,
} from "@ourworldindata/types"
import {
BlankOwidTable,
Expand Down Expand Up @@ -210,6 +212,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 @@ -854,7 +857,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
Expand Down Expand Up @@ -2177,6 +2180,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 @@ -2812,27 +2819,48 @@ 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
observer.disconnect()
}
})
})
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.hasBeenVisible = true
}

// 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 @@ -2893,7 +2921,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
Expand Down Expand Up @@ -2946,6 +2974,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 @@ -3277,6 +3309,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 Expand Up @@ -3417,6 +3454,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
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
32 changes: 29 additions & 3 deletions packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SVGElement>): 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()
}

Expand Down Expand Up @@ -600,6 +613,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
Expand Down Expand Up @@ -669,7 +690,7 @@ export class LineChart

return (
<Tooltip
id={this.renderUid}
id={this.tooltipId}
tooltipManager={this.manager}
x={position.x}
y={position.y}
Expand All @@ -682,6 +703,10 @@ export class LineChart
subtitleFormat={subtitleFormat}
footer={footer}
dissolve={fading}
dismiss={this.dismissTooltip}
shouldDismissOnClickOutside={
this.manager.shouldPinTooltipToBottom
}
>
<TooltipTable
columns={columns}
Expand Down Expand Up @@ -930,6 +955,7 @@ export class LineChart
<g
ref={this.base}
className="LineChart"
onClick={this.onClick}
onMouseLeave={this.onCursorLeave}
onTouchEnd={this.onCursorLeave}
onTouchCancel={this.onCursorLeave}
Expand All @@ -951,7 +977,7 @@ export class LineChart
{this.renderDualAxis()}
<g clipPath={this.clipPath.id}>{this.renderChartElements()}</g>

{this.activeXVerticalLine}
{this.isTooltipActive && this.activeXVerticalLine}
{this.tooltip}
</g>
)
Expand Down
18 changes: 17 additions & 1 deletion packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,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,
Expand Down Expand Up @@ -76,6 +80,7 @@ interface MapChartProps {
bounds?: Bounds
manager: MapChartManager
containerElement?: HTMLDivElement
framePaddingHorizontal?: number
}

// Get the underlying geographical topology elements we're going to display
Expand Down Expand Up @@ -229,6 +234,12 @@ export class MapChart
return this.seriesMap
}

@computed private get framePaddingHorizontal(): number {
return (
this.props.framePaddingHorizontal ?? DEFAULT_GRAPHER_FRAME_PADDING
)
}

base: React.RefObject<SVGGElement> = React.createRef()
@action.bound onMapMouseOver(feature: GeoFeature): void {
const series =
Expand Down Expand Up @@ -601,6 +612,10 @@ export class MapChart

const { tooltipState } = this

const sparklineWidth = this.manager.shouldPinTooltipToBottom
? this.bounds.width + (this.framePaddingHorizontal - 1) * 2
: undefined

return (
<g
ref={this.base}
Expand All @@ -617,6 +632,7 @@ export class MapChart
manager={this.manager}
colorScaleManager={this}
targetTime={this.targetTime}
sparklineWidth={sparklineWidth}
/>
)}
</g>
Expand Down
Loading

0 comments on commit bae4058

Please sign in to comment.