Skip to content

Commit

Permalink
🐛 (slope) add tolerance
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Dec 11, 2024
1 parent 60f2e34 commit 9b1c0c5
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 34 deletions.
17 changes: 17 additions & 0 deletions packages/@ourworldindata/core-table/src/CoreTableColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,23 @@ export abstract class AbstractCoreColumn<JS_TYPE extends PrimitiveType> {
return map
}

// todo: remove? Should not be on CoreTable
@imemo get owidRowByEntityNameAndTime(): Map<
EntityName,
Map<Time, OwidVariableRow<JS_TYPE>>
> {
const valueByEntityNameAndTime = new Map<
EntityName,
Map<Time, OwidVariableRow<JS_TYPE>>
>()
this.owidRows.forEach((row) => {
if (!valueByEntityNameAndTime.has(row.entityName))
valueByEntityNameAndTime.set(row.entityName, new Map())
valueByEntityNameAndTime.get(row.entityName)!.set(row.time, row)
})
return valueByEntityNameAndTime
}

// todo: remove? Should not be on CoreTable
// NOTE: this uses the original times, so any tolerance is effectively unapplied.
@imemo get valueByEntityNameAndOriginalTime(): Map<
Expand Down
5 changes: 4 additions & 1 deletion packages/@ourworldindata/grapher/src/core/Grapher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,10 @@ export class Grapher
)

if (this.isOnSlopeChartTab)
return table.filterByTargetTimes([startTime, endTime])
return table.filterByTargetTimes(
[startTime, endTime],
table.get(this.yColumnSlugs[0]).tolerance
)

return table.filterByTimeRange(startTime, endTime)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ it("filters non-numeric values", () => {
expect(chart.series.length).toEqual(1)
expect(
chart.series.every(
(series) => isNumber(series.startValue) && isNumber(series.endValue)
(series) =>
isNumber(series.start.value) && isNumber(series.end.value)
)
).toBeTruthy()
})
Expand Down
124 changes: 101 additions & 23 deletions packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { ColorSchemes } from "../color/ColorSchemes"
import { LineLabelSeries, LineLegend } from "../lineLegend/LineLegend"
import {
makeTooltipRoundingNotice,
makeTooltipToleranceNotice,
Tooltip,
TooltipState,
TooltipValueRange,
Expand Down Expand Up @@ -114,12 +115,20 @@ export class SlopeChart
if (this.isLogScale)
table = table.replaceNonPositiveCellsForLogScale(this.yColumnSlugs)

this.yColumnSlugs.forEach((slug) => {
table = table.interpolateColumnWithTolerance(slug)
})

return table
}

transformTableForSelection(table: OwidTable): OwidTable {
table = table.replaceNonNumericCellsWithErrorValues(this.yColumnSlugs)

this.yColumnSlugs.forEach((slug) => {
table = table.interpolateColumnWithTolerance(slug)
})

// if time selection is disabled, then filter all entities that
// don't have data for the current time period
if (!this.manager.hasTimeline && this.startTime !== this.endTime) {
Expand Down Expand Up @@ -276,10 +285,9 @@ export class SlopeChart
canSelectMultipleEntities,
})

const valueByTime =
column.valueByEntityNameAndOriginalTime.get(entityName)
const startValue = valueByTime?.get(startTime)
const endValue = valueByTime?.get(endTime)
const owidRowByTime = column.owidRowByEntityNameAndTime.get(entityName)
const start = owidRowByTime?.get(startTime)
const end = owidRowByTime?.get(endTime)

const colorKey = getColorKey({
entityName,
Expand All @@ -299,16 +307,38 @@ export class SlopeChart
seriesName,
entityName,
color,
startValue,
endValue,
start,
end,
annotation,
}
}

private isSeriesValid(
series: RawSlopeChartSeries
): series is SlopeChartSeries {
return series.startValue !== undefined && series.endValue !== undefined
const {
start,
end,
column: { tolerance },
} = series

// if the start or end value is missing, we can't draw the slope
if (start?.value === undefined || end?.value === undefined) return false

// sanity check (might happen if tolerance is enabled)
if (start.originalTime >= end.originalTime) return false

const isToleranceAppliedToStartValue =
start.originalTime !== this.startTime
const isToleranceAppliedToEndValue = end.originalTime !== this.endTime

// if tolerance has been applied to one of the values, then we require
// a minimal distance between the original times
if (isToleranceAppliedToStartValue || isToleranceAppliedToEndValue) {
return end.originalTime - start.originalTime >= 2 * tolerance
}

return true
}

// Usually we drop rows with missing data in the transformTable function.
Expand Down Expand Up @@ -370,8 +400,8 @@ export class SlopeChart
const { yAxis, startX, endX } = this

return this.series.map((series) => {
const startY = yAxis.place(series.startValue)
const endY = yAxis.place(series.endValue)
const startY = yAxis.place(series.start.value)
const endY = yAxis.place(series.end.value)

const startPoint = new PointVector(startX, startY)
const endPoint = new PointVector(endX, endY)
Expand Down Expand Up @@ -406,8 +436,8 @@ export class SlopeChart

@computed get allYValues(): number[] {
return this.series.flatMap((series) => [
series.startValue,
series.endValue,
series.start.value,
series.end.value,
])
}

Expand Down Expand Up @@ -522,13 +552,13 @@ export class SlopeChart
// used in LineLegend
@computed get labelSeries(): LineLabelSeries[] {
return this.series.map((series) => {
const { seriesName, color, endValue, annotation } = series
const { seriesName, color, end, annotation } = series
return {
color,
seriesName,
label: seriesName,
annotation,
yValue: endValue,
yValue: end.value,
}
})
}
Expand Down Expand Up @@ -660,17 +690,45 @@ export class SlopeChart
const { series } = target || {}
if (!series) return

const formatTime = (time: Time) => formatColumn.formatTime(time)

const title = isRelativeMode
? `${series.seriesName}, ${formatColumn.formatTime(endTime)}`
: series.seriesName

const timeRange = [startTime, endTime]
.map((t) => formatColumn.formatTime(t))
.join(" to ")
const isStartValueOriginal = series.start.originalTime === startTime
const isEndValueOriginal = series.end.originalTime === endTime
const actualStartTime = isStartValueOriginal
? startTime
: series.start.originalTime
const actualEndTime = isEndValueOriginal
? endTime
: series.end.originalTime

const timeRange = `${formatTime(actualStartTime)} to ${formatTime(actualEndTime)}`
const timeLabel = isRelativeMode
? `% change since ${formatColumn.formatTime(startTime)}`
: timeRange

const constructTargetYearForToleranceNotice = () => {
if (!isStartValueOriginal && !isEndValueOriginal) {
return `${formatTime(startTime)} and ${formatTime(endTime)}`
} else if (!isStartValueOriginal) {
return formatTime(startTime)
} else if (!isEndValueOriginal) {
return formatTime(endTime)
} else {
return undefined
}
}

const targetYear = constructTargetYearForToleranceNotice()
const toleranceNotice = targetYear
? {
icon: TooltipFooterIcon.notice,
text: makeTooltipToleranceNotice(targetYear),
}
: undefined
const roundingNotice = series.column.roundsToSignificantFigures
? {
icon: TooltipFooterIcon.none,
Expand All @@ -680,11 +738,11 @@ export class SlopeChart
),
}
: undefined
const footer = excludeUndefined([roundingNotice])
const footer = excludeUndefined([toleranceNotice, roundingNotice])

const values = isRelativeMode
? [series.endValue]
: [series.startValue, series.endValue]
? [series.end.value]
: [series.start.value, series.end.value]

return (
<Tooltip
Expand All @@ -697,6 +755,7 @@ export class SlopeChart
style={{ maxWidth: "250px" }}
title={title}
subtitle={timeLabel}
subtitleFormat={targetYear ? "notice" : undefined}
dissolve={fading}
footer={footer}
dismiss={() => (this.tooltipState.target = null)}
Expand All @@ -707,16 +766,35 @@ export class SlopeChart
}

private makeMissingDataLabel(series: RawSlopeChartSeries): string {
const { seriesName } = series
const { seriesName, start, end } = series

const startTime = this.formatColumn.formatTime(this.startTime)
const endTime = this.formatColumn.formatTime(this.endTime)
if (series.startValue === undefined && series.endValue === undefined) {

// mention the start or end value if they're missing
if (start?.value === undefined && end?.value === undefined) {
return `${seriesName} (${startTime} & ${endTime})`
} else if (series.startValue === undefined) {
} else if (start?.value === undefined) {
return `${seriesName} (${startTime})`
} else if (series.endValue === undefined) {
} else if (end?.value === undefined) {
return `${seriesName} (${endTime})`
}

// if both values are given but the series shows up in the No Data
// section, then tolerance has been applied to one of the values
// in such a way that we decided not to render the slope after all
// (e.g. when the original times are too close to each other)
const isToleranceAppliedToStartValue =
start.originalTime !== this.startTime
const isToleranceAppliedToEndValue = end.originalTime !== this.endTime
if (isToleranceAppliedToStartValue && isToleranceAppliedToEndValue) {
return `${seriesName} (${startTime} & ${endTime})`
} else if (isToleranceAppliedToStartValue) {
return `${seriesName} (${startTime})`
} else if (isToleranceAppliedToEndValue) {
return `${seriesName} (${endTime})`
}

return seriesName
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { EntityName, PartialBy, PointVector } from "@ourworldindata/utils"
import { PartialBy, PointVector } from "@ourworldindata/utils"
import { EntityName, OwidVariableRow } from "@ourworldindata/types"
import { ChartSeries } from "../chart/ChartInterface"
import { CoreColumn } from "@ourworldindata/core-table"

export interface SlopeChartSeries extends ChartSeries {
column: CoreColumn
entityName: EntityName
startValue: number
endValue: number
start: Pick<OwidVariableRow<number>, "value" | "originalTime">
end: Pick<OwidVariableRow<number>, "value" | "originalTime">
annotation?: string
}

export type RawSlopeChartSeries = PartialBy<
SlopeChartSeries,
"startValue" | "endValue"
>
export type RawSlopeChartSeries = PartialBy<SlopeChartSeries, "start" | "end">

export interface PlacedSlopeChartSeries extends SlopeChartSeries {
startPoint: PointVector
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,12 @@ export function IconCircledS({
)
}

export function makeTooltipToleranceNotice(targetYear: string): string {
return `Data not available for ${targetYear}. Showing closest available data point instead`
export function makeTooltipToleranceNotice(
targetYear: string,
{ plural }: { plural: boolean } = { plural: false }
): string {
const dataPoint = plural ? "data points" : "data point"
return `Data not available for ${targetYear}. Showing closest available ${dataPoint} instead`
}

export function makeTooltipRoundingNotice(
Expand Down

0 comments on commit 9b1c0c5

Please sign in to comment.