Skip to content

Commit

Permalink
🎉 (grapher) add significance rounding
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Jun 19, 2024
1 parent 32302d7 commit 4a01890
Show file tree
Hide file tree
Showing 31 changed files with 830 additions and 123 deletions.
55 changes: 53 additions & 2 deletions adminSiteClient/DimensionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@ import React from "react"
import { observable, computed, action } from "mobx"
import { observer } from "mobx-react"
import { ChartDimension } from "@ourworldindata/grapher"
import { OwidVariableRoundingMode } from "@ourworldindata/types"
import { startCase } from "@ourworldindata/utils"
import { ChartEditor, DimensionErrorMessage } from "./ChartEditor.js"
import { Toggle, BindAutoString, BindAutoFloat, ColorBox } from "./Forms.js"
import {
Toggle,
BindAutoString,
BindAutoFloat,
ColorBox,
SelectField,
} from "./Forms.js"
import { Link } from "./Link.js"
import {
faChevronDown,
Expand Down Expand Up @@ -49,6 +57,13 @@ export class DimensionCard extends React.Component<{
return this.props.dimension.column.def.color
}

@computed get roundingMode(): OwidVariableRoundingMode {
return (
this.props.dimension.display.roundingMode ??
OwidVariableRoundingMode.decimalPlaces
)
}

private get tableDisplaySettings() {
const { tableDisplay = {} } = this.props.dimension.display
return (
Expand Down Expand Up @@ -166,13 +181,49 @@ export class DimensionCard extends React.Component<{
auto={column.shortUnit ?? ""}
onBlur={this.onChange}
/>
<SelectField
label="Rounding mode"
value={dimension.display.roundingMode}
onValue={(value) => {
const roundingMode =
value as OwidVariableRoundingMode
this.props.dimension.display.roundingMode =
roundingMode !==
OwidVariableRoundingMode.decimalPlaces
? roundingMode
: undefined

this.onChange()
}}
options={Object.keys(OwidVariableRoundingMode).map(
(key) => ({
value: key,
label: startCase(key),
})
)}
/>
{this.roundingMode ===
OwidVariableRoundingMode.significantFigures && (
<BindAutoFloat
label="Number of significant figures"
field="numSignificantFigures"
store={dimension.display}
auto={column.numSignificantFigures}
onBlur={this.onChange}
/>
)}
<BindAutoFloat
label="Number of decimal places"
field="numDecimalPlaces"
store={dimension.display}
auto={column.numDecimalPlaces}
helpText={`A negative number here will round integers`}
onBlur={this.onChange}
helpText={
this.roundingMode ===
OwidVariableRoundingMode.significantFigures
? "Used in Grapher's table where values are always rounded to a fixed number of decimal places"
: undefined
}
/>
<BindAutoFloat
label="Unit conversion factor"
Expand Down
39 changes: 34 additions & 5 deletions adminSiteClient/VariableEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
FieldsRow,
BindDropdown,
Toggle,
SelectField,
} from "./Forms.js"
import {
OwidVariableWithDataAndSource,
Expand All @@ -31,14 +32,19 @@ import {
OwidOrigin,
OwidSource,
stringifyUnknownError,
startCase,
} from "@ourworldindata/utils"
import { GrapherFigureView } from "../site/GrapherFigureView.js"
import { ChartList, ChartListItem } from "./ChartList.js"
import { OriginList } from "./OriginList.js"
import { SourceList } from "./SourceList.js"
import { AdminAppContext, AdminAppContextType } from "./AdminAppContext.js"
import { Base64 } from "js-base64"
import { GrapherTabOption, GrapherInterface } from "@ourworldindata/types"
import {
GrapherTabOption,
GrapherInterface,
OwidVariableRoundingMode,
} from "@ourworldindata/types"
import { Grapher } from "@ourworldindata/grapher"
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js"
Expand Down Expand Up @@ -250,19 +256,42 @@ class VariableEditor extends React.Component<{ variable: VariablePageData }> {
store={newVariable.display}
placeholder={newVariable.shortUnit}
/>
<BindFloat
label="Unit conversion factor"
field="conversionFactor"
store={newVariable.display}
helpText={`Multiply all values by this amount`}
/>
</FieldsRow>
<FieldsRow>
<SelectField
label="Rounding mode"
value={variable.display?.roundingMode}
onValue={(value) => {
const roundingMode =
value as OwidVariableRoundingMode
newVariable.display.roundingMode =
roundingMode !==
OwidVariableRoundingMode.decimalPlaces
? roundingMode
: undefined
}}
options={Object.keys(
OwidVariableRoundingMode
).map((key) => ({
value: key,
label: startCase(key),
}))}
/>
<BindFloat
label="Number of decimal places"
field="numDecimalPlaces"
store={newVariable.display}
helpText={`A negative number here will round integers`}
/>
<BindFloat
label="Unit conversion factor"
field="conversionFactor"
label="Number of significant figures"
field="numSignificantFigures"
store={newVariable.display}
helpText={`Multiply all values by this amount`}
/>
</FieldsRow>
<FieldsRow>
Expand Down
11 changes: 11 additions & 0 deletions devTools/schemaProcessor/columns.json
Original file line number Diff line number Diff line change
Expand Up @@ -578,11 +578,22 @@
"pointer": "/dimensions/0/display/unit",
"editor": "textfield"
},
{
"type": "string",
"pointer": "/dimensions/0/display/roundingMode",
"editor": "dropdown",
"enumOptions": ["decimalPlaces", "significantFigures"]
},
{
"type": "integer",
"pointer": "/dimensions/0/display/numDecimalPlaces",
"editor": "numeric"
},
{
"type": "integer",
"pointer": "/dimensions/0/display/numSignificantFigures",
"editor": "numeric"
},
{
"type": "string",
"pointer": "/dimensions/0/display/zeroDay",
Expand Down
22 changes: 22 additions & 0 deletions packages/@ourworldindata/core-table/src/CoreTableColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
EntityName,
OwidVariableRow,
ErrorValue,
OwidVariableRoundingMode,
} from "@ourworldindata/types"
import { ErrorValueTypes, isNotErrorValue } from "./ErrorValues.js"
import {
Expand Down Expand Up @@ -223,10 +224,28 @@ export abstract class AbstractCoreColumn<JS_TYPE extends PrimitiveType> {
return this.originalTimeColumn.formatValue(time)
}

@imemo get roundingMode(): OwidVariableRoundingMode {
return (
this.display?.roundingMode ?? OwidVariableRoundingMode.decimalPlaces
)
}

@imemo get roundsToFixedDecimals(): boolean {
return this.roundingMode === OwidVariableRoundingMode.decimalPlaces
}

@imemo get roundsToSignificantFigures(): boolean {
return this.roundingMode === OwidVariableRoundingMode.significantFigures
}

@imemo get numDecimalPlaces(): number {
return this.display?.numDecimalPlaces ?? 2
}

@imemo get numSignificantFigures(): number {
return this.display?.numSignificantFigures ?? 3
}

@imemo get unit(): string | undefined {
return this.display?.unit ?? this.def.unit
}
Expand Down Expand Up @@ -612,7 +631,9 @@ abstract class AbstractColumnWithNumberFormatting<
formatValue(value: unknown, options?: TickFormattingOptions): string {
if (isNumber(value)) {
return formatValue(value, {
roundingMode: this.roundingMode,
numDecimalPlaces: this.numDecimalPlaces,
numSignificantFigures: this.numSignificantFigures,
...options,
})
}
Expand Down Expand Up @@ -731,6 +752,7 @@ class IntegerColumn extends NumericColumn {
class CurrencyColumn extends NumericColumn {
formatValue(value: unknown, options?: TickFormattingOptions): string {
return super.formatValue(value, {
roundingMode: OwidVariableRoundingMode.decimalPlaces,
numDecimalPlaces: 0,
unit: this.shortUnit,
...options,
Expand Down
2 changes: 2 additions & 0 deletions packages/@ourworldindata/grapher/src/axis/Axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
Tickmark,
ValueRange,
cloneDeep,
OwidVariableRoundingMode,
} from "@ourworldindata/utils"
import { AxisConfig, AxisManager } from "./AxisConfig"
import { MarkdownTextWrap } from "@ourworldindata/components"
Expand Down Expand Up @@ -323,6 +324,7 @@ abstract class AbstractAxis {
private getTickFormattingOptions(): TickFormattingOptions {
const options: TickFormattingOptions = {
...this.config.tickFormattingOptions,
roundingMode: OwidVariableRoundingMode.decimalPlaces,
}

// The chart's tick formatting function is used by default to format axis ticks. This means
Expand Down
10 changes: 8 additions & 2 deletions packages/@ourworldindata/grapher/src/color/ColorScale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
BinningStrategy,
Color,
CoreValueType,
OwidVariableRoundingMode,
} from "@ourworldindata/types"
import { CoreColumn } from "@ourworldindata/core-table"

Expand Down Expand Up @@ -271,10 +272,15 @@ export class ColorScale {
const color = customNumericColors[index] ?? baseColor
const label = customNumericLabels[index]

const roundingOptions = {
roundingMode: OwidVariableRoundingMode.decimalPlaces,
}
const displayMin =
this.colorScaleColumn?.formatValueShort(min) ?? min.toString()
this.colorScaleColumn?.formatValueShort(min, roundingOptions) ??
min.toString()
const displayMax =
this.colorScaleColumn?.formatValueShort(max) ?? max.toString()
this.colorScaleColumn?.formatValueShort(max, roundingOptions) ??
max.toString()

const currentMin = min
const isFirst = index === 0
Expand Down
2 changes: 2 additions & 0 deletions packages/@ourworldindata/grapher/src/dataTable/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Time,
EntityName,
OwidTableSlugs,
OwidVariableRoundingMode,
} from "@ourworldindata/types"
import {
BlankOwidTable,
Expand Down Expand Up @@ -724,6 +725,7 @@ export class DataTable extends React.Component<{
return value === undefined
? value
: column.formatValueShort(value, {
roundingMode: OwidVariableRoundingMode.decimalPlaces,
numberAbbreviation: false,
trailingZeroes: true,
useNoBreakSpace: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,8 @@ export class EntitySelector extends React.Component<{
if (!isFiniteWithGuard(value)) return { formattedValue: "No data" }

return {
formattedValue: displayColumn.formatValueShort(value),
formattedValue:
displayColumn.formatValueShortWithAbbreviations(value),
width: clamp(barScale(value), 0, 1),
}
}
Expand Down
25 changes: 21 additions & 4 deletions packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ import {
LineLegendManager,
} from "../lineLegend/LineLegend"
import { ComparisonLine } from "../scatterCharts/ComparisonLine"
import { Tooltip, TooltipState, TooltipTable } from "../tooltip/Tooltip"
import { TooltipFooterIcon } from "../tooltip/TooltipProps.js"
import {
Tooltip,
TooltipState,
TooltipTable,
makeTooltipRoundingNotice,
} from "../tooltip/Tooltip"
import { NoDataModal } from "../noDataModal/NoDataModal"
import { extent } from "d3-array"
import {
Expand Down Expand Up @@ -564,9 +570,21 @@ export class LineChart
? `% change since ${formatColumn.formatTime(startTime)}`
: unitLabel
const subtitleFormat = subtitle === unitLabel ? "unit" : undefined
const footer = sortedData.some((series) => series.isProjection)
? `Projected data`

const projectionNotice = sortedData.some(
(series) => series.isProjection
)
? { icon: TooltipFooterIcon.stripes, text: "Projected data" }
: undefined
const roundingNotice = formatColumn.roundsToSignificantFigures
? {
icon: TooltipFooterIcon.none,
text: makeTooltipRoundingNotice([
formatColumn.numSignificantFigures,
]),
}
: undefined
const footer = excludeUndefined([projectionNotice, roundingNotice])

return (
<Tooltip
Expand All @@ -582,7 +600,6 @@ export class LineChart
subtitle={subtitle}
subtitleFormat={subtitleFormat}
footer={footer}
footerFormat="stripes"
dissolve={fading}
>
<TooltipTable
Expand Down
27 changes: 12 additions & 15 deletions packages/@ourworldindata/grapher/src/mapCharts/MapChart.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React from "react"
import {
anyToString,
isNumber,
Bounds,
DEFAULT_BOUNDS,
flatten,
Expand Down Expand Up @@ -325,19 +323,18 @@ export class MapChart
this.mapConfig.projection = value
}

@computed private get formatTooltipValue(): (d: PrimitiveType) => string {
const { mapConfig, mapColumn, colorScale } = this
@computed private get formatTooltipValueIfCustom(): (
d: PrimitiveType
) => string | undefined {
const { mapConfig, colorScale } = this

return (d: PrimitiveType): string => {
if (mapConfig.tooltipUseCustomLabels) {
// Find the bin (and its label) that this value belongs to
const bin = colorScale.getBinForValue(d)
const label = bin?.label
if (label !== undefined && label !== "") return label
}
return isNumber(d)
? mapColumn?.formatValueShort(d) ?? ""
: anyToString(d)
return (d: PrimitiveType): string | undefined => {
if (!mapConfig.tooltipUseCustomLabels) return undefined
// Find the bin (and its label) that this value belongs to
const bin = colorScale.getBinForValue(d)
const label = bin?.label
if (label !== undefined && label !== "") return label
else return undefined
}
}

Expand Down Expand Up @@ -620,7 +617,7 @@ export class MapChart
<MapTooltip
tooltipState={tooltipState}
timeSeriesTable={this.inputTable}
formatValue={this.formatTooltipValue}
formatValueIfCustom={this.formatTooltipValueIfCustom}
manager={this.manager}
colorScaleManager={this}
targetTime={this.targetTime}
Expand Down
Loading

0 comments on commit 4a01890

Please sign in to comment.