Skip to content

Commit

Permalink
🎉 (stacked bar) add neg/pos color encoding / TAS-547 (#3725)
Browse files Browse the repository at this point in the history
Automatically colours positive and negative values differently (if a single series is plotted)

Implemented as part of an effort to create a set of climate charts, see draft charts on [this staging site](http://staging-site-stacked-bar/admin/charts)

## Summary

- Positive and negative values are coloured differently if a single series is currently plotted and that series actually has positive and negative values
- If one faceted chart uses pos/neg colouring, then we switch them all over for consistency
- The colours for positive and negative values are currently hard-coded, I don't want to add config for this; let's see how far we get with hard-coded values first

## Screenshot

Example on staging: http://staging-site-stacked-bar-colors/admin/charts/7905/edit

<img width="817" alt="Screenshot 2024-06-21 at 17 18 40" src="https://github.com/owid/owid-grapher/assets/12461810/cc9c0954-c794-4d8a-9603-27bcdb95d815">
  • Loading branch information
sophiamersmann authored Jul 2, 2024
1 parent 94f8c52 commit 6cafe62
Show file tree
Hide file tree
Showing 7 changed files with 68 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface ChartInterface {
transformedTable: OwidTable // Points to the OwidTable after the chart has transformed the input table. The chart may add a relative transform, for example. Standardized as part of the interface as a development aid.

colorScale?: ColorScale
shouldUseValueBasedColorScheme?: boolean // Opt-out of assigned colors and use a value-based color scheme instead

seriesStrategy?: SeriesStrategy
series: readonly ChartSeries[] // This points to the marks that the chart will render. They don't have to be placed yet. Standardized as part of the interface as a development aid.
Expand Down
8 changes: 5 additions & 3 deletions packages/@ourworldindata/grapher/src/chart/ChartManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export interface ChartManager {
colorScaleColumnOverride?: CoreColumn
// for passing colorScale to sparkline in map charts
colorScaleOverride?: ColorScale
// If you want to use auto-assigned colors, but then have them preserved across selection and chart changes
seriesColorMap?: SeriesColorMap
// If you want to opt out of assigned colors and use a value-based color scheme instead
// (e.g. stacked bar charts coloring positive/negative values differently)
useValueBasedColorScheme?: boolean

yAxisConfig?: Readonly<AxisConfigInterface>
xAxisConfig?: Readonly<AxisConfigInterface>
Expand All @@ -65,9 +70,6 @@ export interface ChartManager {
selection?: SelectionArray | EntityName[]
entityType?: string

// If you want to use auto-assigned colors, but then have them preserved across selection and chart changes
seriesColorMap?: SeriesColorMap

hidePoints?: boolean // for line options
startHandleTimeBound?: TimeBound // for relative-to-first-year line chart

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,13 @@ export class FacetChart
// Only made public for testing
@computed get placedSeries(): PlacedFacetSeries[] {
const { intermediateChartInstances } = this

// If one of the charts should use a value-based color scheme,
// switch them all over for consistency
const useValueBasedColorScheme = intermediateChartInstances.some(
(instance) => instance.shouldUseValueBasedColorScheme
)

// Define the global axis config, shared between all facets
const sharedAxesSizes: PositionMap<number> = {}

Expand Down Expand Up @@ -437,6 +444,7 @@ export class FacetChart
// We need to preserve most config coming in.
const manager = {
...series.manager,
useValueBasedColorScheme,
externalLegendFocusBin: this.legendFocusBin,
xAxisConfig: {
hideAxis: shouldHideFacetAxis(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,15 @@ import { ColorSchemes } from "../color/ColorSchemes"
import { SelectionArray } from "../selection/SelectionArray"
import { CategoricalBin } from "../color/ColorScaleBin"
import { HorizontalColorLegendManager } from "../horizontalColorLegend/HorizontalColorLegends"
import { CategoricalColorAssigner } from "../color/CategoricalColorAssigner.js"
import {
CategoricalColorAssigner,
CategoricalColorMap,
} from "../color/CategoricalColorAssigner.js"
import { BinaryMapPaletteE } from "../color/CustomSchemes"

// used in StackedBar charts to color negative and positive bars
const POSITIVE_COLOR = BinaryMapPaletteE.colorSets[0][0] // orange
const NEGATIVE_COLOR = BinaryMapPaletteE.colorSets[0][1] // blue

export interface AbstractStackedChartProps {
bounds?: Bounds
Expand Down Expand Up @@ -312,6 +320,12 @@ export class AbstractStackedChart
return ""
}

@computed private get colorMap(): CategoricalColorMap {
return this.isEntitySeries
? this.inputTable.entityNameColorIndex
: this.inputTable.columnDisplayNameToColorMap
}

@computed private get categoricalColorAssigner(): CategoricalColorAssigner {
const seriesCount = this.isEntitySeries
? this.selectionArray.numSelectedEntities
Expand All @@ -323,9 +337,7 @@ export class AbstractStackedChart
: null) ??
ColorSchemes.get(ColorSchemeName.stackedAreaDefault),
invertColorScheme: this.manager.invertColorScheme,
colorMap: this.isEntitySeries
? this.inputTable.entityNameColorIndex
: this.inputTable.columnDisplayNameToColorMap,
colorMap: this.colorMap,
autoColorMapCache: this.manager.seriesColorMap,
numColorsInUse: seriesCount,
})
Expand Down Expand Up @@ -376,6 +388,14 @@ export class AbstractStackedChart
return strategies
}

@computed get shouldUseValueBasedColorScheme(): boolean {
return false
}

@computed get useValueBasedColorScheme(): boolean {
return false
}

@computed get unstackedSeries(): readonly StackedSeries<number>[] {
return this.rawSeries
.filter((series) => series.rows.length > 0)
Expand All @@ -384,6 +404,8 @@ export class AbstractStackedChart
const { isProjection, seriesName, rows } = series

const points = rows.map((row) => {
const pointColor =
row.value > 0 ? POSITIVE_COLOR : NEGATIVE_COLOR
return {
position: row.time,
time: row.time,
Expand All @@ -393,6 +415,10 @@ export class AbstractStackedChart
this.shouldRunLinearInterpolation &&
isNotErrorValueOrEmptyCell(row.value) &&
!isNotErrorValueOrEmptyCell(row.originalValue),
// takes precedence over the series color if given
color: this.useValueBasedColorScheme
? pointColor
: undefined,
}
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -394,12 +394,13 @@ export class StackedBarChart
columns={[formatColumn]}
totals={[totalValue]}
rows={sortedHoverPoints.map(
({ point, seriesName: name, seriesColor: swatch }) => {
({ point, seriesName: name, seriesColor }) => {
const focused = hoverSeries?.seriesName === name
const blurred = point?.fake ?? true
const values = [
point?.fake ? undefined : point?.value,
]
const swatch = point?.color ?? seriesColor

return { name, swatch, blurred, focused, values }
}
Expand Down Expand Up @@ -641,7 +642,7 @@ export class StackedBarChart
<StackedBarSegment
key={index}
bar={bar}
color={series.color}
color={bar.color ?? series.color}
xOffset={xPos}
opacity={barOpacity}
yAxis={verticalAxis}
Expand Down Expand Up @@ -692,6 +693,25 @@ export class StackedBarChart

defaultBaseColorScheme = ColorSchemeName.stackedAreaDefault

/**
* Colour positive and negative values differently if there is only one series
* (and that series has both positive and negative values)
*/
@computed get shouldUseValueBasedColorScheme(): boolean {
return (
this.rawSeries.length === 1 &&
this.rawSeries[0].rows.some((row) => row.value < 0) &&
this.rawSeries[0].rows.some((row) => row.value > 0)
)
}

@computed get useValueBasedColorScheme(): boolean {
return (
this.manager.useValueBasedColorScheme ||
this.shouldUseValueBasedColorScheme
)
}

@computed get series(): readonly StackedSeries<number>[] {
// TODO: remove once monthly data is supported (https://github.com/owid/owid-grapher/issues/2007)
const enforceUniformSpacing = !(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface StackedPoint<PositionType extends StackedPointPositionType> {
time: number
interpolated?: boolean
fake?: boolean
color?: string
}

export interface StackedSeries<PositionType extends StackedPointPositionType>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export const withMissingValuesAsZeroes = <
valueOffset: 0,
interpolated: point?.interpolated,
fake: !point || !!point.interpolated,
color: point?.color,
})
}),
}
Expand Down

0 comments on commit 6cafe62

Please sign in to comment.