Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎉 (slope) feature parity with line charts / TAS-708 #4192

Merged
merged 52 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
245737c
✨ (slope) show selected entities only
sophiamersmann Nov 20, 2024
cea23f9
✨ (slope) support relative mode
sophiamersmann Nov 20, 2024
e3d156a
✨ (slope) only allow selection using the entity selector
sophiamersmann Nov 20, 2024
e8c89cb
✨ (slope) drop support for color dim and focus state
sophiamersmann Nov 20, 2024
d92bdb5
✨ (slope) support multiple y-dimensions
sophiamersmann Nov 20, 2024
8feef99
✨ (slope) drop non-positive values in log mode
sophiamersmann Nov 20, 2024
b88a64b
✨ (slope) use line legend instead of custom labels
sophiamersmann Nov 20, 2024
fd20f96
✨ (slope) add support for entity name annotations
sophiamersmann Nov 20, 2024
f43c02d
✨ (grapher) disallow switching to a slope chart for projections
sophiamersmann Nov 20, 2024
1782794
✨ (slope) add tooltips
sophiamersmann Nov 20, 2024
e5bc37e
✨ (slope) refactor
sophiamersmann Nov 21, 2024
2484b8f
🔨 (slope) remove color dimension
sophiamersmann Nov 21, 2024
d4ca6cf
✨ (slope) allow to hide legend
sophiamersmann Nov 21, 2024
df3f87d
🐛 (slope) fix start/end time selection
sophiamersmann Nov 21, 2024
b9cd1e2
🐛 (slope) error out when start and end time are the same
sophiamersmann Nov 21, 2024
25837a4
🐛 (slope) fix No Data section when missing data strategy is hide
sophiamersmann Nov 22, 2024
b25082a
🐛 (slope) fix tabs for line chart that turned into discrete bar
sophiamersmann Nov 22, 2024
16000a9
🐛 (slope) fix entity selector title
sophiamersmann Nov 22, 2024
889194a
🔨 (slope) cleanup
sophiamersmann Nov 22, 2024
6c9dc6e
🧪 (slope) fix tests
sophiamersmann Nov 22, 2024
a9a6d01
✨ (slope) visual changes
sophiamersmann Nov 22, 2024
ad7a2b3
🔨 always apply transforms of the main chart type for the entity selec…
sophiamersmann Nov 27, 2024
3138296
✨ (slope) add Change in to title in relative mode
sophiamersmann Nov 27, 2024
beb3099
✨ (admin) show relative toggle for slope charts
sophiamersmann Nov 27, 2024
8243fbc
🔨 migrate slope charts
sophiamersmann Nov 25, 2024
8fa86dc
✨ (slope) improve tooltip in relative mode
sophiamersmann Nov 27, 2024
a409069
✨ (slope) improve No Data section
sophiamersmann Nov 27, 2024
f2b593b
✨ nicer entity/column labels
sophiamersmann Nov 27, 2024
99a8c6e
🔨 (slope) clean up code
sophiamersmann Nov 27, 2024
47d65d0
🔨 update slope migration
sophiamersmann Nov 28, 2024
30d6b7c
🧪 (slope) add tests
sophiamersmann Nov 28, 2024
378e619
✨ (slope) drop entities from selector if time selection disabled
sophiamersmann Nov 28, 2024
19b63c6
🐛 (slope) fix relative value calculation
sophiamersmann Nov 28, 2024
9b4e8b0
🐛 (slope) hide relative toggle for log scale
sophiamersmann Nov 28, 2024
4984f41
🐛 (line) use startTime for relative mode calculation
sophiamersmann Nov 28, 2024
2d22a26
🔨 update slope migration after feedback
sophiamersmann Nov 28, 2024
c8c26eb
🔨 (slope) drop non-numeric data in any case
sophiamersmann Nov 28, 2024
c4fb3e5
🐛 (slope) show correct label for relative toggle
sophiamersmann Nov 28, 2024
dd7384d
🔨 use current chart instance for table for selection
sophiamersmann Nov 29, 2024
29d5cc9
🐛 (slope) only drop entities if start and end time are different
sophiamersmann Nov 29, 2024
e89593c
🐛 hide slope chart tab if line chart really is a bar chart
sophiamersmann Nov 29, 2024
c4097e9
✨ automatically adjust handles when switching from line to slope
sophiamersmann Nov 29, 2024
fddfee9
🐛 (slope) only add entity name if necessary
sophiamersmann Nov 29, 2024
6b6ff35
🎉 enable timeline animation for slope charts
sophiamersmann Nov 29, 2024
135ade9
🔨 (slope) update comments
sophiamersmann Nov 29, 2024
3050254
🔨 remove accidental commits
sophiamersmann Nov 29, 2024
0b814ab
🐛 merge arrays correctly
sophiamersmann Dec 2, 2024
5d0cea4
Revert "🐛 hide slope chart tab if line chart really is a bar chart"
sophiamersmann Dec 2, 2024
e116909
✨ (slope) incorporate pr feedback
sophiamersmann Dec 2, 2024
647e347
🔨 rename line chart helpers file
sophiamersmann Dec 2, 2024
2b38230
🔨 remove slope chart migrations
sophiamersmann Dec 10, 2024
3100127
🔨 fix rebase
sophiamersmann Dec 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions adminSiteClient/EditorBasicTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ class DimensionSlotView<
const { selection } = grapher
const { availableEntityNames, availableEntityNameSet } = selection

if (grapher.isScatter || grapher.isSlopeChart || grapher.isMarimekko) {
if (grapher.isScatter || grapher.isMarimekko) {
// chart types that display all entities by default shouldn't select any by default
selection.clearSelection()
} else if (
Expand Down Expand Up @@ -372,8 +372,8 @@ export class EditorBasicTab<
grapher.stackMode = StackMode.relative
}

// Give scatterplots and slope charts a default color dimension if they don't have one
if (grapher.isScatter || grapher.isSlopeChart) {
// Give scatterplots a default color and size dimensions
if (grapher.isScatter) {
const hasColor = grapher.dimensions.find(
(d) => d.property === DimensionProperty.color
)
Expand All @@ -382,10 +382,7 @@ export class EditorBasicTab<
variableId: CONTINENTS_INDICATOR_ID,
property: DimensionProperty.color,
})
}

// Give scatterplots a default size dimension if they don't have one
if (grapher.isScatter) {
const hasSize = grapher.dimensions.find(
(d) => d.property === DimensionProperty.size
)
Expand Down
8 changes: 5 additions & 3 deletions adminSiteClient/EditorFeatures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class EditorFeatures {
@computed get hideLegend() {
return (
this.grapher.isLineChart ||
this.grapher.isSlopeChart ||
this.grapher.isStackedArea ||
this.grapher.isStackedDiscreteBar
)
Expand All @@ -77,6 +78,7 @@ export class EditorFeatures {
this.grapher.isStackedBar ||
this.grapher.isStackedDiscreteBar ||
this.grapher.isLineChart ||
this.grapher.isSlopeChart ||
this.grapher.isScatter ||
this.grapher.isMarimekko
)
Expand Down Expand Up @@ -118,9 +120,9 @@ export class EditorFeatures {
return true
}

// for line charts, specifying a missing data strategy only makes sense
// for line and slope charts, specifying a missing data strategy only makes sense
// if there are multiple entities
if (this.grapher.isLineChart) {
if (this.grapher.isLineChart || this.grapher.isSlopeChart) {
return (
this.grapher.canChangeEntity ||
this.grapher.canSelectMultipleEntities
Expand All @@ -132,7 +134,7 @@ export class EditorFeatures {

@computed get showChangeInPrefixToggle() {
return (
this.grapher.isLineChart &&
(this.grapher.isLineChart || this.grapher.isSlopeChart) &&
(this.grapher.isRelativeMode || this.grapher.canToggleRelativeMode)
)
}
Expand Down
2 changes: 1 addition & 1 deletion baker/updateChartEntities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const obtainAvailableEntitiesForGrapherConfig = async (

// In these chart types, an unselected entity is still shown
const chartTypeShowsUnselectedEntities =
grapher.isScatter || grapher.isSlopeChart || grapher.isMarimekko
grapher.isScatter || grapher.isMarimekko

if (canChangeEntities || chartTypeShowsUnselectedEntities)
return grapher.tableForSelection.availableEntityNames as string[]
Expand Down
12 changes: 11 additions & 1 deletion packages/@ourworldindata/core-table/src/CoreTableColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,12 +305,22 @@ export abstract class AbstractCoreColumn<JS_TYPE extends PrimitiveType> {
@imemo get displayName(): string {
return (
this.display?.name ??
this.def.presentation?.titlePublic ?? // this is a bit of an unusual fallback - if display.name is not given, titlePublic is the next best thing before name
// this is a bit of an unusual fallback - if display.name is not given, titlePublic is the next best thing before name
this.def.presentation?.titlePublic ??
this.name ??
""
)
}

@imemo get nonEmptyDisplayName(): string {
return (
this.display?.name ||
// this is a bit of an unusual fallback - if display.name is not given, titlePublic is the next best thing before name
this.def.presentation?.titlePublic ||
this.nonEmptyName
)
}

@imemo get titlePublicOrDisplayName(): IndicatorTitleWithFragments {
return this.def.presentation?.titlePublic
? {
Expand Down
89 changes: 89 additions & 0 deletions packages/@ourworldindata/core-table/src/OwidTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,95 @@ export class OwidTable extends CoreTable<OwidRow, OwidColumnDef> {
)
}

// Drop _all rows_ for an entity if all columns have at least one invalid or missing value for that entity.
dropEntitiesThatHaveSomeMissingOrErrorValueInAllColumns(
columnSlugs: ColumnSlug[]
): this {
const indexesByEntityName = this.rowIndicesByEntityName
const uniqTimes = new Set(this.allTimes)

// entity names to iterate over
const entityNamesToIterateOver = new Set(indexesByEntityName.keys())

// set of entities we want to keep
const entityNamesToKeep = new Set<string>()

// total number of entities
const entityCount = entityNamesToIterateOver.size

// helper function to generate operation name
const makeOpName = (entityNamesToKeep: Set<EntityName>): string => {
const entityNamesToDrop = differenceOfSets([
this.availableEntityNameSet,
entityNamesToKeep,
])
const droppedEntitiesStr =
entityNamesToDrop.size > 0
? [...entityNamesToDrop].join(", ")
: "(None)"
return `Drop entities that have some missing or error value in all column: ${columnSlugs.join(", ")}.\nDropped entities: ${droppedEntitiesStr}`
}

// Optimization: if there is a column that has a valid data entry for
// every entity and every time, we are done
for (let i = 0; i <= columnSlugs.length; i++) {
const slug = columnSlugs[i]
const col = this.get(slug)

if (
col.numValues === entityCount * uniqTimes.size &&
col.numErrorValues === 0
) {
const entityNamesToKeep = new Set(indexesByEntityName.keys())

return this.columnFilter(
this.entityNameSlug,
(rowEntityName) =>
entityNamesToKeep.has(rowEntityName as string),
makeOpName(entityNamesToKeep)
)
}
}

for (let i = 0; i <= columnSlugs.length; i++) {
const slug = columnSlugs[i]
const col = this.get(slug)

for (const entityName of entityNamesToIterateOver) {
const indicesForEntityName = indexesByEntityName.get(entityName)
if (!indicesForEntityName)
throw new Error("Unexpected: entity not found in index map")

// Optimization: If the column is missing values for the entity,
// we know we can't make a decision yet, so we skip this entity
if (indicesForEntityName.length < uniqTimes.size) continue

// Optimization: We don't care about the number of valid/error
// values, we just need to know if there is at least one invalid value
const hasSomeInvalidValueForEntityInCol =
indicesForEntityName.some(
(index) =>
!isNotErrorValue(
col.valuesIncludingErrorValues[index]
)
)

// Optimization: If all values are valid, we know we want to keep this entity,
// so we remove it from the entities to iterate over
if (!hasSomeInvalidValueForEntityInCol) {
entityNamesToKeep.add(entityName)
entityNamesToIterateOver.delete(entityName)
}
}
}

return this.columnFilter(
this.entityNameSlug,
(rowEntityName) => entityNamesToKeep.has(rowEntityName as string),
makeOpName(entityNamesToKeep)
)
}

private sumsByTime(columnSlug: ColumnSlug): Map<number, number> {
const timeValues = this.timeColumn.values
const values = this.get(columnSlug).values as number[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,14 @@ export class DiscreteBarChart
<g id={makeIdForHumanConsumption("entity-labels")}>
{this.placedSeries.map((series) => {
return (
series.label &&
series.label.render(
series.entityLabelX,
series.barY - series.label.height / 2,
{ textProps: style }
series.label && (
<React.Fragment key={series.seriesName}>
{series.label.render(
series.entityLabelX,
series.barY - series.label.height / 2,
{ textProps: style }
)}
</React.Fragment>
)
)
})}
Expand Down Expand Up @@ -990,6 +993,7 @@ function makeProjectedDataPattern(color: string): React.ReactElement {
const size = 7
return (
<pattern
key={color}
id={makeProjectedDataPatternId(color)}
patternUnits="userSpaceOnUse"
width={size}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export interface CaptionedChartManager
isOnMapTab?: boolean
isOnTableTab?: boolean
activeChartType?: GrapherChartType
isLineChartThatTurnedIntoDiscreteBar?: boolean
isLineChartThatTurnedIntoDiscreteBarActive?: boolean
showEntitySelectionToggle?: boolean
isExportingForSocialMedia?: boolean

Expand Down Expand Up @@ -197,7 +197,7 @@ export class CaptionedChart extends React.Component<CaptionedChartProps> {
if (manager.isOnTableTab) return undefined
if (manager.isOnMapTab) return GRAPHER_MAP_TYPE
if (manager.isOnChartTab) {
return manager.isLineChartThatTurnedIntoDiscreteBar
return manager.isLineChartThatTurnedIntoDiscreteBarActive
? GRAPHER_CHART_TYPES.DiscreteBar
: manager.activeChartType
}
Expand Down
15 changes: 14 additions & 1 deletion packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react"
import { Box, getCountryByName } from "@ourworldindata/utils"
import { areSetsEqual, Box, getCountryByName } from "@ourworldindata/utils"
import {
SeriesStrategy,
EntityName,
Expand All @@ -15,6 +15,7 @@ import {
GRAPHER_SIDE_PANEL_CLASS,
GRAPHER_TIMELINE_CLASS,
GRAPHER_SETTINGS_CLASS,
validChartTypeCombinations,
} from "../core/GrapherConstants"

export const autoDetectYColumnSlugs = (manager: ChartManager): string[] => {
Expand Down Expand Up @@ -175,3 +176,15 @@ export function mapChartTypeNameToQueryParam(
return GRAPHER_TAB_QUERY_PARAMS.marimekko
}
}

export function findValidChartTypeCombination(
chartTypes: GrapherChartType[]
): GrapherChartType[] | undefined {
const chartTypeSet = new Set(chartTypes)
for (const validCombination of validChartTypeCombinations) {
const validCombinationSet = new Set(validCombination)
if (areSetsEqual(chartTypeSet, validCombinationSet))
return validCombination
}
return undefined
}
19 changes: 15 additions & 4 deletions packages/@ourworldindata/grapher/src/controls/ContentSwitchers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface ContentSwitchersManager {
activeTab?: GrapherTabName
hasMultipleChartTypes?: boolean
setTab: (tab: GrapherTabName) => void
onTabChange: (oldTab: GrapherTabName, newTab: GrapherTabName) => void
isNarrow?: boolean
isMedium?: boolean
isLineChartThatTurnedIntoDiscreteBar?: boolean
Expand Down Expand Up @@ -112,8 +113,10 @@ export class ContentSwitchers extends React.Component<{
}

@action.bound setTab(tabIndex: number): void {
const oldTab = this.manager.activeTab
const newTab = this.availableTabs[tabIndex]
this.manager.setTab(newTab)
this.manager.onTabChange?.(oldTab!, newTab)
}

render(): React.ReactElement {
Expand Down Expand Up @@ -175,9 +178,11 @@ function TabIcon({
case GRAPHER_TAB_NAMES.WorldMap:
return <FontAwesomeIcon icon={faEarthAmericas} />
default:
const chartIcon = isLineChartThatTurnedIntoDiscreteBar
? chartIcons[GRAPHER_CHART_TYPES.DiscreteBar]
: chartIcons[tab]
const chartIcon =
tab === GRAPHER_TAB_NAMES.LineChart &&
isLineChartThatTurnedIntoDiscreteBar
? chartIcons[GRAPHER_CHART_TYPES.DiscreteBar]
: chartIcons[tab]
return chartIcon
}
}
Expand All @@ -193,9 +198,15 @@ function makeTabLabelText(
if (tab === GRAPHER_TAB_NAMES.WorldMap) return "Map"
if (!options.hasMultipleChartTypes) return "Chart"

if (
tab === GRAPHER_TAB_NAMES.LineChart &&
options.isLineChartThatTurnedIntoDiscreteBar
)
return "Bar"

switch (tab) {
case GRAPHER_TAB_NAMES.LineChart:
return options.isLineChartThatTurnedIntoDiscreteBar ? "Bar" : "Line"
return "Line"
case GRAPHER_TAB_NAMES.SlopeChart:
return "Slope"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const {
StackedDiscreteBar,
StackedBar,
Marimekko,
SlopeChart,
} = GRAPHER_CHART_TYPES

export interface SettingsMenuManager
Expand Down Expand Up @@ -170,6 +171,7 @@ export class SettingsMenu extends React.Component<{
ScatterPlot,
LineChart,
Marimekko,
SlopeChart,
].includes(this.chartType as any)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from "@ourworldindata/types"
import { LabeledSwitch } from "@ourworldindata/components"

const { LineChart, ScatterPlot } = GRAPHER_CHART_TYPES
const { LineChart, ScatterPlot, SlopeChart } = GRAPHER_CHART_TYPES

export interface AbsRelToggleManager {
stackMode?: StackMode
Expand Down Expand Up @@ -38,7 +38,7 @@ export class AbsRelToggle extends React.Component<{
const { activeChartType } = this.manager
return activeChartType === ScatterPlot
? "Show the percentage change per year over the the selected time range."
: activeChartType === LineChart
: activeChartType === LineChart || activeChartType === SlopeChart
? "Show proportional changes over time or actual values in their original units."
: "Show values as their share of the total or as actual values in their original units."
}
Expand Down
Loading
Loading