diff --git a/.gitignore b/.gitignore index a8651ff3d84..c98987c7232 100755 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ dist/ .wrangler/ .nx/cache .dev.vars +**/tsup.config.bundled*.mjs diff --git a/packages/@ourworldindata/components/src/MarkdownTextWrap/MarkdownTextWrap.tsx b/packages/@ourworldindata/components/src/MarkdownTextWrap/MarkdownTextWrap.tsx index d00ac03367e..56053b52808 100644 --- a/packages/@ourworldindata/components/src/MarkdownTextWrap/MarkdownTextWrap.tsx +++ b/packages/@ourworldindata/components/src/MarkdownTextWrap/MarkdownTextWrap.tsx @@ -643,9 +643,11 @@ export class MarkdownTextWrap extends React.Component { { textProps, detailsMarker = "superscript", + id, }: { textProps?: React.SVGProps detailsMarker?: DetailsMarker + id?: string } = {} ): JSX.Element | null { const { fontSize, lineHeight } = this @@ -670,7 +672,7 @@ export class MarkdownTextWrap extends React.Component { yOffset + lineHeight * fontSize * lineIndex return ( - + } = {} + { + textProps, + id, + }: { textProps?: React.SVGProps; id?: string } = {} ): JSX.Element | null { const { props, lines, fontSize, fontWeight, lineHeight } = this @@ -267,6 +270,7 @@ export class TextWrap { return ( + {axis.getTickValues().map((t, i) => { const color = t.faint ? FAINT_TICK_COLOR @@ -47,6 +51,10 @@ export class VerticalAxisGridLines extends React.Component<{ return ( + {axis.getTickValues().map((t, i) => { const color = t.faint ? FAINT_TICK_COLOR @@ -96,6 +107,10 @@ export class HorizontalAxisGridLines extends React.Component<{ return ( { ) return ( - + {horizontalAxisComponent} {verticalAxisComponent} {verticalGridlines} @@ -243,23 +262,19 @@ export class VerticalAxisComponent extends React.Component<{ } = this.props const { tickLabels, labelTextWrap } = verticalAxis - const tickMarks = showTickMarks ? ( - - verticalAxis.place(label.value) - )} - tickMarkLeftPosition={bounds.left + verticalAxis.width} - color={SOLID_TICK_COLOR} - /> - ) : undefined - return ( - + {labelTextWrap && labelTextWrap.renderSVG( -verticalAxis.rangeCenter - labelTextWrap.width / 2, bounds.left, { + id: makeIdForHumanConsumption( + "vertical-axis-label" + ), textProps: { transform: "rotate(-90)", fill: labelColor || GRAPHER_DARK_TEXT, @@ -267,29 +282,50 @@ export class VerticalAxisComponent extends React.Component<{ detailsMarker, } )} - {tickMarks} - {tickLabels.map((label, i) => { - const { y, xAlign, yAlign, formattedValue } = label - return ( - - {formattedValue} - - ) - })} + {showTickMarks && ( + + {tickLabels.map((label, i) => ( + + ))} + + )} + + {tickLabels.map((label, i) => { + const { y, xAlign, yAlign, formattedValue } = label + return ( + + {formattedValue} + + ) + })} + ) } @@ -329,10 +365,11 @@ export class HorizontalAxisComponent extends React.Component<{ preferredAxisPosition, labelColor, tickColor, - tickMarkWidth, + tickMarkWidth = 1, detailsMarker, } = this.props const { tickLabels, labelTextWrap: label, labelOffset, orient } = axis + const tickSize = 5 const horizontalAxisLabelsOnTop = orient === Position.top const labelYPosition = horizontalAxisLabelsOnTop ? bounds.top @@ -342,49 +379,61 @@ export class HorizontalAxisComponent extends React.Component<{ ? bounds.top + axis.height - 5 : preferredAxisPosition ?? bounds.bottom - const tickMarks = showTickMarks ? ( - - axis.place(label.value) - )} - color={SOLID_TICK_COLOR} - width={tickMarkWidth} - /> - ) : undefined - const tickLabelYPlacement = horizontalAxisLabelsOnTop ? bounds.top + labelOffset + 10 : bounds.bottom - labelOffset + return ( - + {label && label.renderSVG( axis.rangeCenter - label.width / 2, labelYPosition, { + id: makeIdForHumanConsumption( + "horizontal-axis-label" + ), textProps: { fill: labelColor || GRAPHER_DARK_TEXT, }, detailsMarker, } )} - {tickMarks} - {tickLabels.map((label, i) => { + {tickLabels.map((label) => { const { x, xAlign, formattedValue } = label return ( - - {formattedValue} - + {showTickMarks && ( + + )} + + {formattedValue} + + ) })} @@ -392,56 +441,54 @@ export class HorizontalAxisComponent extends React.Component<{ } } -export class HorizontalAxisTickMarks extends React.Component<{ +export class HorizontalAxisTickMark extends React.Component<{ tickMarkTopPosition: number - tickMarkXPositions: number[] + tickMarkXPosition: number color: string width?: number + id?: string }> { - render(): JSX.Element[] { - const { tickMarkTopPosition, tickMarkXPositions, color, width } = + render(): JSX.Element { + const { tickMarkTopPosition, tickMarkXPosition, color, width, id } = this.props const tickSize = 5 const tickBottom = tickMarkTopPosition + tickSize - return tickMarkXPositions.map((tickMarkPosition, index) => { - return ( - - ) - }) + return ( + + ) } } -export class VerticalAxisTickMarks extends React.Component<{ +export class VerticalAxisTickMark extends React.Component<{ tickMarkLeftPosition: number - tickMarkYPositions: number[] + tickMarkYPosition: number color: string width?: number + id?: string }> { - render(): JSX.Element[] { - const { tickMarkYPositions, tickMarkLeftPosition, color, width } = + render(): JSX.Element { + const { tickMarkYPosition, tickMarkLeftPosition, color, width, id } = this.props const tickSize = 5 const tickRight = tickMarkLeftPosition + tickSize - return tickMarkYPositions.map((tickMarkPosition, index) => { - return ( - - ) - }) + return ( + + ) } } diff --git a/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx b/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx index 9f4127518c3..dfdb0c51232 100644 --- a/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx +++ b/packages/@ourworldindata/grapher/src/barCharts/DiscreteBarChart.tsx @@ -18,6 +18,7 @@ import { HorizontalAlign, AxisAlign, uniqBy, + makeIdForHumanConsumption, } from "@ourworldindata/utils" import { computed } from "mobx" import { observer } from "mobx-react" @@ -424,7 +425,11 @@ export class DiscreteBarChart : GRAPHER_AXIS_LINE_WIDTH_DEFAULT return ( - + {this.hasProjectedData && ( {/* passed to the legend as pattern for the @@ -481,6 +486,10 @@ export class DiscreteBarChart const result = ( diff --git a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx index 7a05b4a79be..3c806f0d80c 100644 --- a/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx +++ b/packages/@ourworldindata/grapher/src/captionedChart/CaptionedChart.tsx @@ -6,6 +6,7 @@ import { DEFAULT_BOUNDS, exposeInstanceOnWindow, isEmpty, + makeIdForHumanConsumption, } from "@ourworldindata/utils" import { MarkdownTextWrap } from "@ourworldindata/components" import { Header, StaticHeader } from "../header/Header" @@ -556,6 +557,7 @@ export class StaticCaptionedChart extends CaptionedChart { return ( <> - + {/* We cannot render a table to svg, but would rather display nothing at all to avoid issues. See https://github.com/owid/owid-grapher/issues/3283 diff --git a/packages/@ourworldindata/grapher/src/captionedChart/Logos.tsx b/packages/@ourworldindata/grapher/src/captionedChart/Logos.tsx index 52e8e277152..661226f114a 100644 --- a/packages/@ourworldindata/grapher/src/captionedChart/Logos.tsx +++ b/packages/@ourworldindata/grapher/src/captionedChart/Logos.tsx @@ -7,6 +7,7 @@ import { SMALL_OWID_LOGO_SVG, } from "./LogosSVG" import { LogoOption } from "@ourworldindata/types" +import { makeIdForHumanConsumption } from "@ourworldindata/utils" interface LogoAttributes { svg: string @@ -92,6 +93,7 @@ export class Logo { (this.spec.svg.match(/(.*)<\/svg>/) || "")[1] || this.spec.svg return ( { return ( - {sources.renderSVG(targetX, targetY)} + {sources.renderSVG(targetX, targetY, { + id: makeIdForHumanConsumption("sources"), + })} {this.showNote && note.renderSVG( targetX, targetY + sources.height + this.verticalPadding, - { detailsMarker: this.manager.detailsMarkerInSvg } + { + id: makeIdForHumanConsumption("note"), + detailsMarker: this.manager.detailsMarkerInSvg, + } )} {showLicenseNextToSources ? licenseAndOriginUrl.render( targetX + maxWidth - licenseAndOriginUrl.width, - targetY + targetY, + { id: makeIdForHumanConsumption("origin-url") } ) : licenseAndOriginUrl.render( targetX, @@ -782,7 +794,8 @@ export class StaticFooter extends Footer { (this.showNote ? note.height + this.verticalPadding : 0) + - this.verticalPadding + this.verticalPadding, + { id: makeIdForHumanConsumption("origin-url") } )} ) diff --git a/packages/@ourworldindata/grapher/src/header/Header.tsx b/packages/@ourworldindata/grapher/src/header/Header.tsx index 7bd664d1af0..61eee2b71ad 100644 --- a/packages/@ourworldindata/grapher/src/header/Header.tsx +++ b/packages/@ourworldindata/grapher/src/header/Header.tsx @@ -1,5 +1,10 @@ import React from "react" -import { DEFAULT_BOUNDS, range, LogoOption } from "@ourworldindata/utils" +import { + DEFAULT_BOUNDS, + range, + LogoOption, + makeIdForHumanConsumption, +} from "@ourworldindata/utils" import { MarkdownTextWrap, TextWrap } from "@ourworldindata/components" import { computed } from "mobx" import { observer } from "mobx-react" @@ -275,12 +280,13 @@ export class StaticHeader extends Header { const { targetX: x, targetY: y } = this.props const { title, logo, subtitle, manager, maxWidth } = this return ( - + {logo && logo.height > 0 && logo.renderSVG(x + maxWidth - logo.width, y)} {this.showTitle && ( { ? title.height + this.subtitleMarginTop : 0), { + id: makeIdForHumanConsumption("subtitle"), textProps: { fill: manager.secondaryColorInStaticCharts, }, diff --git a/packages/@ourworldindata/grapher/src/horizontalColorLegend/HorizontalColorLegends.tsx b/packages/@ourworldindata/grapher/src/horizontalColorLegend/HorizontalColorLegends.tsx index bb2d9a28d36..47f7ef10d70 100644 --- a/packages/@ourworldindata/grapher/src/horizontalColorLegend/HorizontalColorLegends.tsx +++ b/packages/@ourworldindata/grapher/src/horizontalColorLegend/HorizontalColorLegends.tsx @@ -15,6 +15,7 @@ import { Color, HorizontalAlign, VerticalAlign, + makeIdForHumanConsumption, } from "@ourworldindata/utils" import { TextWrap } from "@ourworldindata/components" import { @@ -556,7 +557,11 @@ export class HorizontalNumericColorLegend extends HorizontalColorLegend { const bottomY = this.numericLegendY + height return ( - + {numericLabels.map((label, index) => ( - - {marks.map((mark, index) => { - const isActive = activeColors?.includes(mark.bin.color) - const isFocus = focusColors?.includes(mark.bin.color) - - const fill = mark.bin.patternRef - ? `url(#${mark.bin.patternRef})` - : mark.bin.color - - return ( - - manager.onLegendMouseOver - ? manager.onLegendMouseOver(mark.bin) - : undefined - } - onMouseLeave={(): void => - manager.onLegendMouseLeave - ? manager.onLegendMouseLeave() - : undefined + + {marks.map((mark, index) => { + const isActive = activeColors?.includes(mark.bin.color) + const isFocus = focusColors?.includes(mark.bin.color) + + const fill = mark.bin.patternRef + ? `url(#${mark.bin.patternRef})` + : mark.bin.color + + return ( + + manager.onLegendMouseOver + ? manager.onLegendMouseOver(mark.bin) + : undefined + } + onMouseLeave={(): void => + manager.onLegendMouseLeave + ? manager.onLegendMouseLeave() + : undefined + } + onClick={(): void => + manager.onLegendClick + ? manager.onLegendClick(mark.bin) + : undefined + } + style={{ cursor: "default" }} + > + {/* for hover interaction */} + - manager.onLegendClick - ? manager.onLegendClick(mark.bin) - : undefined + fill="#fff" + opacity={0} + /> + + - {/* for hover interaction */} - - - - {mark.label.text} - - - ) - })} - + {mark.label.text} + + + ) + })} ) } diff --git a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx index 1f8d2416adb..425861b1eac 100644 --- a/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx +++ b/packages/@ourworldindata/grapher/src/lineCharts/LineChart.tsx @@ -25,6 +25,7 @@ import { Color, HorizontalAlign, PrimitiveType, + makeIdForHumanConsumption, } from "@ourworldindata/utils" import { computed, action, observable } from "mobx" import { observer } from "mobx-react" @@ -170,12 +171,16 @@ class Lines extends React.Component { const strokeDasharray = series.isProjection ? "2,3" : undefined return ( - + {/* Create a white outline around the lines so they're easier to follow when they overlap. */} { )} /> {showMarkers && ( - + {series.placedPoints.map((value, index) => ( { private renderBackgroundGroups(): JSX.Element[] { return this.backgroundLines.map((series, index) => ( - + { const { bounds } = this return ( - + {needsLines && ( - + )} + {this.renderBackground()} {this.renderFocus()} diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ComparisonLine.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/ComparisonLine.tsx index f541a6ac52e..0797990e62f 100644 --- a/packages/@ourworldindata/grapher/src/scatterCharts/ComparisonLine.tsx +++ b/packages/@ourworldindata/grapher/src/scatterCharts/ComparisonLine.tsx @@ -1,5 +1,10 @@ import { line as d3_line, curveLinear } from "d3-shape" -import { guid, Bounds, PointVector } from "@ourworldindata/utils" +import { + guid, + Bounds, + PointVector, + makeIdForHumanConsumption, +} from "@ourworldindata/utils" import React from "react" import { computed } from "mobx" import { observer } from "mobx-react" @@ -78,7 +83,10 @@ export class ComparisonLine extends React.Component<{ const { linePath, renderUid, placedLabel } = this return ( - + + const { markerStart, markerMid, markerEnd, ...polylineProps } = this.props return ( - <> + {this.segments.map((group, index) => ( } /> ))} - + ) } } diff --git a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPoints.tsx b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPoints.tsx index 997af6bf50a..ae2b00d6187 100644 --- a/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPoints.tsx +++ b/packages/@ourworldindata/grapher/src/scatterCharts/ScatterPoints.tsx @@ -1,4 +1,9 @@ -import { PointVector, first, last } from "@ourworldindata/utils" +import { + PointVector, + first, + last, + makeIdForHumanConsumption, +} from "@ourworldindata/utils" import { observer } from "mobx-react" import React from "react" import { MultiColorPolyline } from "./MultiColorPolyline" @@ -32,6 +37,10 @@ export class ScatterPoint extends React.Component<{ return ( { @@ -95,7 +104,14 @@ export class ScatterLine extends React.Component<{ const opacity = 0.7 return ( - + + {!hideConnectedScatterLines && ( ({ @@ -543,6 +555,10 @@ export class ScatterPointsWithLabels extends React.Component + {this.renderLegend(targetX, targetY)} {this.label.render( centerX, diff --git a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx index 2b7a30ee52c..843c307b7b8 100644 --- a/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx +++ b/packages/@ourworldindata/grapher/src/slopeCharts/SlopeChart.tsx @@ -18,6 +18,7 @@ import { clamp, HorizontalAlign, difference, + makeIdForHumanConsumption, } from "@ourworldindata/utils" import { TextWrap } from "@ourworldindata/components" import { observable, computed, action } from "mobx" @@ -427,7 +428,10 @@ export class SlopeChart ) return ( - + { isFocused, isHovered, isMultiHoverMode, + seriesName, } = this.props const { isInBackground } = this @@ -669,7 +674,10 @@ class SlopeEntry extends React.Component { } return ( - + (this.line = el)} x1={x1} @@ -1275,11 +1283,20 @@ class LabelledSlopes fill="rgba(255,255,255,0)" opacity={0} /> - + {this.yAxis.tickLabels.map((tick) => { const y = yAxis.place(tick.value) return ( - + {/* grid lines connecting the chart area to the axis */} {this.yColumn.formatTime(xDomain[1])} - + {this.renderGroups(this.backgroundGroups)} {this.renderGroups(this.foregroundGroups)} diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx index bdd1a46f770..154fc578f97 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/MarimekkoChart.tsx @@ -21,6 +21,7 @@ import { getRelativeMouse, ColorSchemeName, EntitySelectionMode, + makeIdForHumanConsumption, } from "@ourworldindata/utils" import { action, computed, observable } from "mobx" import { observer } from "mobx-react" @@ -226,6 +227,7 @@ function MarimekkoBarsForOneEntity(props: MarimekkoBarsProps): JSX.Element { return ( onEntityMouseOver(entityName, ev)} @@ -982,6 +984,7 @@ export class MarimekkoChart return ( this.onMouseMove(ev)} > @@ -1550,7 +1553,14 @@ export class MarimekkoChart ? directionUnawareMakerYMid : markerNetHeight - directionUnawareMakerYMid labelLines.push( - + + ( diff --git a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx index 50e94c17a12..fd4bc43d667 100644 --- a/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx +++ b/packages/@ourworldindata/grapher/src/stackedCharts/StackedAreaChart.tsx @@ -12,6 +12,7 @@ import { isMobile, Time, lastOfNonEmptyArray, + makeIdForHumanConsumption, } from "@ourworldindata/utils" import { computed, action, observable } from "mobx" import { SeriesName } from "@ourworldindata/types" @@ -164,6 +165,7 @@ class Areas extends React.Component { return ( { return ( { const { horizontalAxis, verticalAxis } = dualAxis return ( - + - tick.bounds.centerX - )} - color="#666" - width={axisLineWidth} - /> + + {ticks.map((tick, i) => ( + + ))} + - + {ticks.map((tick, i) => { return ( @@ -596,6 +598,10 @@ export class StackedDiscreteBarChart getElementWithHalo( entityName + "-value-label", props?.onMouseEnter(entity, bar.seriesName) } diff --git a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx index 7599076e2b8..94939381e3c 100644 --- a/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx +++ b/packages/@ourworldindata/grapher/src/verticalColorLegend/VerticalColorLegend.tsx @@ -1,5 +1,5 @@ import React from "react" -import { sum, max } from "@ourworldindata/utils" +import { sum, max, makeIdForHumanConsumption } from "@ourworldindata/utils" import { TextWrap } from "@ourworldindata/components" import { computed } from "mobx" import { observer } from "mobx-react" @@ -145,6 +145,7 @@ export class VerticalColorLegend extends React.Component<{ {title && title.render(x, y, { textProps: { fontWeight: 700 } })} @@ -172,6 +173,10 @@ export class VerticalColorLegend extends React.Component<{ const result = ( name.replace(/[^a-z0-9]/g, (str) => { const char = str.charCodeAt(0) - if (char === 32) return "-" + if (char === 32 || char === 45) return "-" if (char === 95) return "_" if (char >= 65 && char <= 90) return str return "__" + ("000" + char.toString(16)).slice(-4) }) +/** + * Make a human-readable string meant to be be used as the ID of a SVG chart + * element. This is useful when a static chart is manually edited in a SVG + * editor since SVG manipulation software like Figma often show the element's + * id as its title. + */ +export function makeIdForHumanConsumption( + name: string, + unsafeKey?: string +): string { + let id = name + if (unsafeKey) { + id += "__" + makeSafeForCSS(unsafeKey) + } + return id +} + export function formatDay( dayAsYear: number, options?: { format?: string } diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts index 4015da99ea6..f1ab2d75e72 100644 --- a/packages/@ourworldindata/utils/src/index.ts +++ b/packages/@ourworldindata/utils/src/index.ts @@ -7,6 +7,7 @@ export { getRelativeMouse, exposeInstanceOnWindow, makeSafeForCSS, + makeIdForHumanConsumption, formatDay, formatYear, numberMagnitude,