diff --git a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts index d00e2912578..8b24bdd70a3 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartManager.ts +++ b/packages/@ourworldindata/grapher/src/chart/ChartManager.ts @@ -68,6 +68,7 @@ export interface ChartManager { hidePoints?: boolean // for line options startHandleTimeBound?: TimeBound // for relative-to-first-year line chart + hideNoDataSection?: boolean // for slope charts // we need endTime so DiscreteBarCharts and StackedDiscreteBarCharts can // know what date the timeline is set to. and let's pass startTime in, too. @@ -78,7 +79,7 @@ export interface ChartManager { seriesStrategy?: SeriesStrategy sortConfig?: SortConfig - showNoDataArea?: boolean + showNoDataArea?: boolean // No data area in Marimekko charts externalLegendHoverBin?: ColorScaleBin | undefined disableIntroAnimation?: boolean diff --git a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx index fb7f4351ce7..961863e4d64 100644 --- a/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx +++ b/packages/@ourworldindata/grapher/src/controls/SettingsMenu.tsx @@ -195,6 +195,7 @@ export class SettingsMenu extends React.Component<{ StackedBar, StackedDiscreteBar, LineChart, + SlopeChart, ].includes(this.chartType as any) const hasProjection = filledDimensions.some( diff --git a/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx b/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx index ee2870aac03..6ae1555fb84 100644 --- a/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx +++ b/packages/@ourworldindata/grapher/src/facetChart/FacetChart.tsx @@ -295,7 +295,9 @@ export class FacetChart return series.map((series, index) => { const { bounds } = gridBoundsArr[index] const showLegend = !this.hideFacetLegends + const hidePoints = true + const hideNoDataSection = true // NOTE: The order of overrides is important! // We need to preserve most config coming in. @@ -319,6 +321,7 @@ export class FacetChart endTime, missingDataStrategy, backgroundColor, + hideNoDataSection, ...series.manager, xAxisConfig: { ...globalXAxisConfig, @@ -373,6 +376,13 @@ export class FacetChart ) } + @computed private get isYAxisHidden(): boolean { + return ( + this.chartTypeName === GRAPHER_CHART_TYPES.SlopeChart && + this.facetCount >= SHARED_X_AXIS_MIN_FACET_COUNT + ) + } + // Only made public for testing @computed get placedSeries(): PlacedFacetSeries[] { const { intermediateChartInstances } = this @@ -495,11 +505,10 @@ export class FacetChart ...axes.x.config, }, yAxisConfig: { - hideAxis: shouldHideFacetAxis( - yAxis, - cellEdges, - sharedAxesSizes - ), + hideAxis: + this.isYAxisHidden || + shouldHideFacetAxis(yAxis, cellEdges, sharedAxesSizes), + hideGridlines: this.isYAxisHidden, ...series.manager.yAxisConfig, ...axes.y.config, }, @@ -756,7 +765,8 @@ export class FacetChart ) if (this.facetStrategy === FacetStrategy.metric && newBins.length <= 1) return [] - return newBins + const sortedBins = sortBy(newBins, (bin) => bin.label) + return sortedBins } @observable.ref private legendHoverBin: ColorScaleBin | undefined = diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 63f60e2ed30..6e3e82d6cad 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -34,6 +34,7 @@ import { EntityName, RenderMode, VerticalAlign, + FacetStrategy, } from "@ourworldindata/types" import { ChartInterface } from "../chart/ChartInterface" import { ChartManager } from "../chart/ChartManager" @@ -75,6 +76,8 @@ import { getColorKey, getSeriesName, } from "../lineCharts/LineChartHelpers" +import { HorizontalColorLegendManager } from "../horizontalColorLegend/HorizontalColorLegends" +import { CategoricalBin } from "../color/ColorScaleBin" type SVGMouseOrTouchEvent = | React.MouseEvent @@ -83,6 +86,7 @@ type SVGMouseOrTouchEvent = export interface SlopeChartManager extends ChartManager { canSelectMultipleEntities?: boolean // used to pick an appropriate series name hasTimeline?: boolean // used to filter the table for the entity selector + hideNoDataSection?: boolean } const TOP_PADDING = 6 // leave room for overflowing dots @@ -219,7 +223,7 @@ export class SlopeChart } @computed private get isFocusModeActive(): boolean { - return this.hoveredSeriesName !== undefined + return this.focusedSeriesNames.length > 0 } @computed private get yColumns(): CoreColumn[] { @@ -258,6 +262,17 @@ export class SlopeChart return autoDetectSeriesStrategy(this.manager, true) } + @computed get availableFacetStrategies(): FacetStrategy[] { + const strategies: FacetStrategy[] = [FacetStrategy.none] + + if (this.selectionArray.numSelectedEntities > 1) + strategies.push(FacetStrategy.entity) + + if (this.yColumns.length > 1) strategies.push(FacetStrategy.metric) + + return strategies + } + @computed private get categoricalColorAssigner(): CategoricalColorAssigner { return new CategoricalColorAssigner({ colorScheme: this.colorScheme, @@ -424,6 +439,8 @@ export class SlopeChart } @computed private get showNoDataSection(): boolean { + if (this.manager.hideNoDataSection) return false + // nothing to show if there are no series with missing data if (this.noDataSeries.length === 0) return false @@ -501,6 +518,22 @@ export class SlopeChart : 0 } + @computed get externalLegend(): HorizontalColorLegendManager | undefined { + if (!this.manager.showLegend) { + const categoricalLegendData = this.series.map( + (series, index) => + new CategoricalBin({ + index, + value: series.seriesName, + label: series.seriesName, + color: series.color, + }) + ) + return { categoricalLegendData } + } + return undefined + } + @computed get maxLineLegendWidth(): number { return 0.25 * this.innerBounds.width } @@ -548,7 +581,6 @@ export class SlopeChart } @computed get lineLegendWidthLeft(): number { - if (!this.manager.showLegend) return 0 return LineLegend.width({ labelSeries: this.lineLegendSeriesLeft, yAxis: this.yAxis, @@ -561,13 +593,13 @@ export class SlopeChart } @computed get lineLegendWidthRight(): number { - if (!this.manager.showLegend) return 0 return LineLegend.width({ labelSeries: this.lineLegendSeriesRight, yAxis: this.yAxis, maxWidth: this.maxLineLegendWidth, connectorLineWidth: this.lineLegendConnectorLinesWidth, fontSize: this.fontSize, + fontWeight: this.manager.showLegend ? 700 : undefined, isStatic: this.manager.isStatic, }) } @@ -622,12 +654,28 @@ export class SlopeChart } @computed get useCompactLineLegend(): boolean { - return !!this.manager.isSemiNarrow + return !!this.manager.isSemiNarrow || this.bounds.width < 400 } - // used by LineLegend @computed get focusedSeriesNames(): SeriesName[] { - return this.hoveredSeriesName ? [this.hoveredSeriesName] : [] + const focusedSeriesNames: SeriesName[] = [] + + // hovered series name + if (this.hoveredSeriesName) + focusedSeriesNames.push(this.hoveredSeriesName) + + // hovered legend item in the external facet legend + if (this.manager.externalLegendHoverBin) { + focusedSeriesNames.push( + ...this.series + .map((s) => s.seriesName) + .filter((name) => + this.manager.externalLegendHoverBin?.contains(name) + ) + ) + } + + return focusedSeriesNames } /** @@ -635,7 +683,7 @@ export class SlopeChart * name to make it clear which slope the value belongs to */ @computed private get showSeriesNamesInLineLegendLeft(): boolean { - return this.lineLegendLeftHasConnectorLines + return this.lineLegendLeftHasConnectorLines && !!this.manager.showLegend } @computed get lineLegendSeriesLeft(): LineLabelSeries[] { @@ -659,14 +707,19 @@ export class SlopeChart @computed get lineLegendSeriesRight(): LineLabelSeries[] { return this.series.map((series) => { const { seriesName, color, end, annotation } = series - const formattedValue = this.formatColumn.formatValueShort(end.value) + const value = this.formatColumn.formatValueShort(end.value) + const label = this.manager.showLegend ? seriesName : value + const formattedValue = this.manager.showLegend ? value : undefined return { color, seriesName, - label: seriesName, - annotation: this.useCompactLineLegend ? undefined : annotation, + label, formattedValue, valueInNewLine: this.useCompactLineLegend, + annotation: + this.manager.showLegend && this.useCompactLineLegend + ? undefined + : annotation, yValue: end.value, } }) @@ -775,6 +828,7 @@ export class SlopeChart if (message) return message else if (this.startTime === this.endTime) return "No data to display for the selected time period" + else if (this.series.length === 0) return "No matching data" return "" } @@ -953,7 +1007,7 @@ export class SlopeChart const [focusedSeries, backgroundSeries] = partition( this.placedSeries, - (series) => series.seriesName === this.hoveredSeriesName + (series) => this.focusedSeriesNames.includes(series.seriesName) ) return ( @@ -968,23 +1022,30 @@ export class SlopeChart ) } - private renderChartArea() { - const { bounds, xDomain, yRange, startX, endX } = this + private renderYAxis() { + return ( + <> + {!this.yAxis.hideGridlines && ( + + )} + {!this.yAxis.hideAxis && ( + + )} + + ) + } + + private renderXAxis() { + const { xDomain, yRange, startX, endX } = this const [bottom, top] = yRange return ( - - - + <> + + ) + } + + private renderChartArea() { + return ( + + {this.renderYAxis()} + {this.renderXAxis()} {this.renderSlopes()} @@ -1032,7 +1102,7 @@ export class SlopeChart verticalAlign={VerticalAlign.top} connectorLineWidth={this.lineLegendConnectorLinesWidth} fontSize={this.fontSize} - fontWeight={700} + fontWeight={this.manager.showLegend ? 700 : undefined} isStatic={this.manager.isStatic} focusedSeriesNames={this.focusedSeriesNames} onMouseLeave={this.onLineLegendMouseLeave} @@ -1090,8 +1160,6 @@ export class SlopeChart } private renderLineLegends(): React.ReactElement | void { - if (!this.manager.showLegend) return - return ( <> {this.renderLineLegendLeft()} @@ -1103,12 +1171,15 @@ export class SlopeChart render() { if (this.failMessage) return ( - + <> + {this.renderYAxis()} + + ) return ( @@ -1233,10 +1304,9 @@ function HaloLine(props: HaloLineProps): React.ReactElement { interface GridLinesProps { bounds: Bounds yAxis: VerticalAxis - endX: number } -function GridLines({ bounds, yAxis, endX }: GridLinesProps) { +function GridLines({ bounds, yAxis }: GridLinesProps) { return ( {yAxis.tickLabels.map((tick) => { @@ -1252,7 +1322,7 @@ function GridLines({ bounds, yAxis, endX }: GridLinesProps) {