Skip to content

Commit

Permalink
✨ (grapher) support multiple chart types
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Nov 11, 2024
1 parent a6288be commit f5db56f
Show file tree
Hide file tree
Showing 35 changed files with 579 additions and 202 deletions.
17 changes: 11 additions & 6 deletions adminSiteClient/VariableEditPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -687,18 +687,22 @@ class VariableEditor extends React.Component<{
@computed private get grapherConfig(): GrapherInterface {
const { variable } = this.props
const grapherConfig = variable.grapherConfig
if (grapherConfig)
if (grapherConfig) {
// TODO: add map tab if missing
return {
...grapherConfig,
hasMapTab: true,
tab: GrapherTabOption.map,
// hasMapTab: true,
tab: GrapherTabOption.WorldMap,
}
else
} else {
return {
yAxis: { min: 0 },
map: { columnSlug: this.props.variable.id.toString() },
tab: GrapherTabOption.map,
hasMapTab: true,
tab: GrapherTabOption.WorldMap,
availableTabs: [
GrapherTabOption.Table,
GrapherTabOption.WorldMap,
],
dimensions: [
{
property: DimensionProperty.y,
Expand All @@ -707,6 +711,7 @@ class VariableEditor extends React.Component<{
},
],
}
}
}

dispose!: IReactionDisposer
Expand Down
35 changes: 21 additions & 14 deletions adminSiteServer/testPageRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ async function propsFromQueryParams(

if (params.type) {
if (params.type === ChartTypeName.WorldMap) {
// TODO: add hasMapTab to chart configs table?
query = query.andWhereRaw(`cc.full->>"$.hasMapTab" = "true"`)
tab = tab || GrapherTabOption.map
tab = tab || GrapherTabOption.WorldMap
} else {
if (params.type === "LineChart") {
query = query.andWhereRaw(
Expand All @@ -165,34 +166,39 @@ async function propsFromQueryParams(
{ type: params.type }
)
}
tab = tab || GrapherTabOption.chart
// TODO
// tab = tab || GrapherTabOption.chart
}
}

if (params.logLinear) {
query = query.andWhereRaw(
`cc.full->>'$.yAxis.canChangeScaleType' = "true" OR cc.full->>'$.xAxis.canChangeScaleType' = "true"`
)
tab = GrapherTabOption.chart
// TODO
// tab = GrapherTabOption.chart
}

if (params.comparisonLines) {
query = query.andWhereRaw(
`cc.full->'$.comparisonLines[0].yEquals' != ''`
)
tab = GrapherTabOption.chart
// TODO
// tab = GrapherTabOption.chart
}

if (params.stackMode) {
query = query.andWhereRaw(`cc.full->'$.stackMode' = :stackMode`, {
stackMode: params.stackMode,
})
tab = GrapherTabOption.chart
// TODO
// tab = GrapherTabOption.chart
}

if (params.relativeToggle) {
query = query.andWhereRaw(`cc.full->>'$.hideRelativeToggle' = "false"`)
tab = GrapherTabOption.chart
// TODO
// tab = GrapherTabOption.chart
}

if (params.categoricalLegend) {
Expand All @@ -202,7 +208,7 @@ async function propsFromQueryParams(
query = query.andWhereRaw(
`json_length(cc.full->'$.map.colorScale.customCategoryColors') > 1`
)
tab = GrapherTabOption.map
tab = GrapherTabOption.WorldMap
}

if (params.mixedTimeTypes) {
Expand Down Expand Up @@ -242,13 +248,14 @@ async function propsFromQueryParams(
query = query.andWhereRaw(`charts.id IN (${params.ids})`)
}

if (tab === GrapherTabOption.map) {
query = query.andWhereRaw(`cc.full->>"$.hasMapTab" = "true"`)
} else if (tab === GrapherTabOption.chart) {
query = query.andWhereRaw(
`COALESCE(cc.full->>"$.hasChartTab", "true") = "true"`
)
}
// TODO
// if (tab === GrapherTabOption.WorldMap) {
// query = query.andWhereRaw(`cc.full->>"$.hasMapTab" = "true"`)
// } else if (tab === GrapherTabOption.chart) {
// query = query.andWhereRaw(
// `COALESCE(cc.full->>"$.hasChartTab", "true") = "true"`
// )
// }

if (datasetIds.length > 0) {
const datasetIds = params.datasetIds
Expand Down
12 changes: 9 additions & 3 deletions baker/countryProfiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import {
CountryProfilePage,
} from "../site/CountryProfilePage.js"
import { SiteBaker } from "./SiteBaker.js"
import { countries, getCountryBySlug, JsonError } from "@ourworldindata/utils"
import {
countries,
getCountryBySlug,
JsonError,
getMainChartTypeFromConfig,
hasChartTabFromConfig,
} from "@ourworldindata/utils"
import { renderToHtmlPage } from "./siteRenderers.js"
import { dataAsDF } from "../db/model/Variable.js"
import pl from "nodejs-polars"
Expand All @@ -37,8 +43,8 @@ function bakeCache<T>(cacheKey: any, retriever: () => T): T {
}

const checkShouldShowIndicator = (grapher: GrapherInterface) =>
(grapher.hasChartTab ?? true) &&
(grapher.type ?? "LineChart") === "LineChart" &&
hasChartTabFromConfig(grapher) &&
getMainChartTypeFromConfig(grapher) === "LineChart" &&
grapher.dimensions?.length === 1

// Find the charts that will be shown on the country profile page (if they have that country)
Expand Down
6 changes: 4 additions & 2 deletions baker/updateChartEntities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ const obtainAvailableEntitiesForGrapherConfig = async (
)
grapher.receiveOwidData(variableData)

// TODO: add hasMaptab and hasChartTab as computed props

// If the grapher has a chart tab, then the available entities there are the "most interesting" ones to us
if (grapher.hasChartTab) {
grapher.tab = GrapherTabOption.chart
grapher.tab = GrapherTabOption.LineChart // TODO

// If the grapher allows for changing or multi-selecting entities, then let's index all entities the
// user can choose from. Otherwise, we'll just use the default-selected entities.
Expand All @@ -112,7 +114,7 @@ const obtainAvailableEntitiesForGrapherConfig = async (
return grapher.tableForSelection.availableEntityNames as string[]
else return grapher.selectedEntityNames ?? []
} else if (grapher.hasMapTab) {
grapher.tab = GrapherTabOption.map
grapher.tab = GrapherTabOption.WorldMap
// On a map tab, tableAfterAuthorTimelineAndActiveChartTransform contains all
// mappable entities for which data is available
return grapher.tableAfterAuthorTimelineAndActiveChartTransform
Expand Down
198 changes: 198 additions & 0 deletions db/migration/1731316808331-AddAvailableTabsToGrapherConfigs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { MigrationInterface, QueryRunner } from "typeorm"

export class AddAvailableTabsToGrapherConfigs1731316808331
implements MigrationInterface
{
private async updateSchema(
queryRunner: QueryRunner,
newSchema: string
): Promise<void> {
await queryRunner.query(
`
-- sql
UPDATE chart_configs
SET
patch = JSON_SET(patch, '$.$schema', ?),
full = JSON_SET(full, '$.$schema', ?)
`,
[newSchema, newSchema]
)
}

private async addAvailableTabsToGrapherConfigs(
queryRunner: QueryRunner,
config: "patch" | "full"
): Promise<void> {
// case 1:
// hasMapTab=false, hasChartTab=false -> availableTabs=[]
await queryRunner.query(
`
-- sql
UPDATE chart_configs
SET
?? = JSON_SET(??, '$.availableTabs', JSON_ARRAY('Table'))
WHERE
(?? ->> '$.hasMapTab' = 'false' OR ?? ->> '$.hasMapTab' IS NULL)
AND (?? ->> '$.hasChartTab' = 'false')
`,
[config, config, config, config, config]
)

// case 2:
// hasMapTab=false, hasChartTab=true -> availableTabs=[chartType]
await queryRunner.query(
`
-- sql
UPDATE chart_configs
SET
?? = JSON_SET(??, '$.availableTabs', JSON_ARRAY('Table', COALESCE(?? ->> '$.type', 'LineChart')))
WHERE
(?? ->> '$.hasMapTab' = 'false' OR ?? ->> '$.hasMapTab' IS NULL)
AND (?? ->> '$.hasChartTab' = 'true' OR ?? ->> '$.hasChartTab' IS NULL)
`,
[config, config, config, config, config, config, config]
)

// case 3:
// hasMapTab=true, hasChartTab=false -> availableTabs=["WorldMap"]
await queryRunner.query(
`
-- sql
UPDATE chart_configs
SET
?? = JSON_SET(??, '$.availableTabs', JSON_ARRAY('Table', 'WorldMap'))
WHERE
(?? ->> '$.hasMapTab' = 'true')
AND (?? ->> '$.hasChartTab' = 'false')
`,
[config, config, config, config]
)

// case 4:
// hasMapTab=true, hasChartTab=true -> availableTabs=["WorldMap", chartType]
await queryRunner.query(
`
-- sql
UPDATE chart_configs
SET
?? = JSON_SET(??, '$.availableTabs', JSON_ARRAY('Table', 'WorldMap', COALESCE(?? ->> '$.type', 'LineChart')))
WHERE
(?? ->> '$.hasMapTab' = 'true')
AND (?? ->> '$.hasChartTab' = 'true' OR ?? ->> '$.hasChartTab' IS NULL)
`,
[config, config, config, config, config, config]
)
}

private async removeAvailableTabsFromGrapherConfigs(
queryRunner: QueryRunner
): Promise<void> {
// TODO
}

private async updateTabField(
queryRunner: QueryRunner,
config: "patch" | "full"
): Promise<void> {
await queryRunner.query(
`
-- sql
UPDATE chart_configs
SET ?? = JSON_SET(??, '$.tab', 'WorldMap')
WHERE ?? ->> '$.tab' = 'map'
`,
[config, config, config]
)

await queryRunner.query(
`
-- sql
UPDATE chart_configs
SET ?? = JSON_SET(??, '$.tab', 'Table')
WHERE ?? ->> '$.tab' = 'table'
`,
[config, config, config]
)

await queryRunner.query(
`
-- sql
UPDATE chart_configs
SET ?? = JSON_SET(??, '$.tab', COALESCE(?? ->> '$.type', 'LineChart'))
WHERE ?? ->> '$.tab' = 'chart'
`,
[config, config, config, config]
)
}

private async removeTypeHasMapTabAndHasChartTabFields(
queryRunner: QueryRunner
): Promise<void> {
await queryRunner.query(`
-- sql
UPDATE chart_configs
SET patch = JSON_REMOVE(patch, '$.type', '$.hasChartTab', '$.hasMapTab')
`)
}

private async addTypeHasMapTabAndHasChartTabFields(
queryRunner: QueryRunner
): Promise<void> {
// TODO
}

private async addDerivedTypeColumn(
queryRunner: QueryRunner
): Promise<void> {
// TODO: not future-proof
await queryRunner.query(
`-- sql
ALTER TABLE chart_configs
ADD COLUMN type VARCHAR(255) GENERATED ALWAYS AS
(
CASE
WHEN JSON_LENGTH(full, '$.availableTabs') = 0 THEN NULL
WHEN full ->> '$.availableTabs[0]' != 'Map' THEN full ->> '$.availableTabs[0]'
ELSE full ->> '$.availableTabs[1]'
END
)
VIRTUAL AFTER slug;
`
)
}

public async removeDerivedTypeColumn(
queryRunner: QueryRunner
): Promise<void> {
await queryRunner.query(
`-- sql
ALTER TABLE chart_configs
DROP COLUMN type;
`
)
}

public async up(queryRunner: QueryRunner): Promise<void> {
await this.addAvailableTabsToGrapherConfigs(queryRunner, "patch")
await this.addAvailableTabsToGrapherConfigs(queryRunner, "full")
await this.updateTabField(queryRunner, "patch")
await this.updateTabField(queryRunner, "full")
// await this.removeTypeHasMapTabAndHasChartTabFields(queryRunner)
// await this.updateSchema(
// queryRunner,
// "https://files.ourworldindata.org/schemas/grapher-schema.007.json"
// )
}

public async down(queryRunner: QueryRunner): Promise<void> {
// TODO: order!
// await this.removeAvailableTabsFromGrapherConfigs(queryRunner)
// await this.revertTabFieldMigration(queryRunner)
// await this.addTypeHasMapTabAndHasChartTabFields(queryRunner)
// await this.updateSchema(
// queryRunner,
// "https://files.ourworldindata.org/schemas/grapher-schema.006.json"
// )
// await this.removeDerivedTypeColumn(queryRunner)
}
}
3 changes: 2 additions & 1 deletion db/model/Variable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
omitUndefinedValues,
mergeGrapherConfigs,
diffGrapherConfigs,
getMainChartTypeFromConfig,
} from "@ourworldindata/utils"
import {
getVariableDataRoute,
Expand Down Expand Up @@ -878,7 +879,7 @@ export async function getVariableOfDatapageIfApplicable(
// showing a data page.
if (
yVariableIds.length === 1 &&
(grapher.type !== ChartTypeName.ScatterPlot ||
(getMainChartTypeFromConfig(grapher) !== ChartTypeName.ScatterPlot ||
xVariableIds.length === 0)
) {
const variableId = yVariableIds[0]
Expand Down
Loading

0 comments on commit f5db56f

Please sign in to comment.