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

feat(explorers): allow referencing indicators by catalog path #3748

Merged
merged 24 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bea09df
feat(db): allow querying variables by etl path
marcelgerber Jun 17, 2024
d6433ed
feat(explorer): include catalog path -> indicator id mapping on explo…
marcelgerber Jun 25, 2024
24b7ba0
feat(explorer): using catalog paths mostly works
marcelgerber Jun 25, 2024
2d59cbb
enhance(explorer): enhance grammar types and descriptions
marcelgerber Jun 26, 2024
f4dd0a0
enhance(explorer): use rowFilter for dropping columnDefs
marcelgerber Jun 26, 2024
0d5de69
feat(explorer): handle `variableId` column def containing ETL path
marcelgerber Jun 26, 2024
a511fa8
enhance(explorer): css style for `IndicatorIdOrEtlPath`
marcelgerber Jun 26, 2024
fcf9aa2
style: remove unused import
marcelgerber Jun 26, 2024
81ca1c2
enhance(explorer): better regex
marcelgerber Jun 26, 2024
064bcd6
wip: transform explorer server-side
marcelgerber Jul 2, 2024
cff7994
feat: resolve indicator ids at bake time
marcelgerber Jul 2, 2024
6e75b04
enhance: mark method private again
marcelgerber Jul 2, 2024
dbba59f
more efficient and more fitting regex's
marcelgerber Jul 2, 2024
be115f2
improve regex perf
marcelgerber Jul 2, 2024
3e19a75
properly fix regex, once and for all
marcelgerber Jul 2, 2024
2861020
cleanup
marcelgerber Jul 2, 2024
d1a02e1
cleanup comment
marcelgerber Jul 2, 2024
9e20c78
cleanup
marcelgerber Jul 2, 2024
479885d
don't fail parsing
marcelgerber Jul 2, 2024
450a742
clean up explorer transformation code
marcelgerber Jul 2, 2024
5424375
correct css class
marcelgerber Jul 2, 2024
ae035a0
feat(explorer): add admin checkbox to switch between saved/changed ex…
marcelgerber Jul 2, 2024
17775c6
style: remove unused import
marcelgerber Jul 2, 2024
ee25fb6
enhance: only send error to Bugsnag if not previewing
marcelgerber Jul 2, 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
100 changes: 100 additions & 0 deletions baker/ExplorerBaker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,106 @@ import { ExplorerAdminServer } from "../explorerAdminServer/ExplorerAdminServer.
import { explorerRedirectTable } from "../explorerAdminServer/ExplorerRedirects.js"
import { renderExplorerPage } from "./siteRenderers.js"
import * as db from "../db/db.js"
import { getVariableIdsByCatalogPath } from "../db/model/Variable.js"
import { ExplorerGrammar } from "../explorer/ExplorerGrammar.js"
import {
CoreTable,
ErrorValueTypes,
isNotErrorValueOrEmptyCell,
} from "@ourworldindata/core-table"
import { ColumnGrammar } from "../explorer/ColumnGrammar.js"
import { ColumnTypeNames } from "@ourworldindata/types"

export const transformExplorerProgramToResolveCatalogPaths = async (
program: ExplorerProgram,
knex: db.KnexReadonlyTransaction
): Promise<{
program: ExplorerProgram
unresolvedCatalogPaths?: Set<string>
}> => {
const { decisionMatrix } = program
const { requiredCatalogPaths } = decisionMatrix

if (requiredCatalogPaths.size === 0) return { program }

const catalogPathToIndicatorIdMap = await getVariableIdsByCatalogPath(
[...requiredCatalogPaths],
knex
)
const unresolvedCatalogPaths = new Set(
[...requiredCatalogPaths].filter(
(path) => !catalogPathToIndicatorIdMap.get(path)
)
)

const colSlugsToUpdate = decisionMatrix.allColumnsWithIndicatorIds.map(
(col) => col.slug
)
// In the decision matrix table, replace any catalog paths with their corresponding indicator ids
// If a catalog path is not found, it will be left as is
const newDecisionMatrixTable =
decisionMatrix.tableWithOriginalColumnNames.replaceCells(
colSlugsToUpdate,
(val) => {
if (typeof val === "string") {
const vals = val.split(" ")
const updatedVals = vals.map(
(val) =>
catalogPathToIndicatorIdMap.get(val)?.toString() ??
val
)
return updatedVals.join(" ")
}
return val
}
)
const grapherBlockLine = program.getRowMatchingWords(
ExplorerGrammar.graphers.keyword
)
const newProgram = program.updateBlock(
grapherBlockLine,
newDecisionMatrixTable.toMatrix()
)

program.columnDefsByTableSlug.forEach((columnDefs, tableSlug) => {
const lineNoInProgram = newProgram.getRowMatchingWords(
ExplorerGrammar.columns.keyword,
tableSlug
)
const columnDefTable = new CoreTable(
newProgram.getBlock(lineNoInProgram)
)
const newColumnDefsTable = columnDefTable.combineColumns(
[
ColumnGrammar.variableId.keyword,
ColumnGrammar.catalogPath.keyword,
],
{
slug: ColumnGrammar.variableId.keyword,
type: ColumnTypeNames.Numeric,
},
(row) => {
const variableId = row[ColumnGrammar.variableId.keyword]
if (isNotErrorValueOrEmptyCell(variableId)) return variableId

const catalogPath = row[ColumnGrammar.catalogPath.keyword]
if (
isNotErrorValueOrEmptyCell(catalogPath) &&
typeof catalogPath === "string"
) {
return (
catalogPathToIndicatorIdMap.get(catalogPath) ??
ErrorValueTypes.NoMatchingVariableId
)
}
return ErrorValueTypes.NoMatchingVariableId
}
)
newProgram.updateBlock(lineNoInProgram, newColumnDefsTable.toMatrix())
})

return { program: newProgram, unresolvedCatalogPaths }
}

export const bakeAllPublishedExplorers = async (
outputFolder: string,
Expand Down
32 changes: 26 additions & 6 deletions baker/siteRenderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
getAndLoadGdocBySlug,
getAndLoadGdocById,
} from "../db/model/Gdoc/GdocFactory.js"
import { getVariableIdsByCatalogPath } from "../db/model/Variable.js"

Check warning on line 94 in baker/siteRenderers.tsx

View workflow job for this annotation

GitHub Actions / eslint

'getVariableIdsByCatalogPath' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 94 in baker/siteRenderers.tsx

View workflow job for this annotation

GitHub Actions / eslint

'getVariableIdsByCatalogPath' is defined but never used. Allowed unused vars must match /^_/u
import { transformExplorerProgramToResolveCatalogPaths } from "./ExplorerBaker.js"

export const renderToHtmlPage = (element: any) =>
`<!doctype html>${ReactDOMServer.renderToStaticMarkup(element)}`
Expand Down Expand Up @@ -696,7 +698,22 @@
knex: KnexReadonlyTransaction,
urlMigrationSpec?: ExplorerPageUrlMigrationSpec
) => {
const { requiredGrapherIds, requiredVariableIds } = program.decisionMatrix
const transformResult = await transformExplorerProgramToResolveCatalogPaths(
program,
knex
)
const { program: transformedProgram, unresolvedCatalogPaths } =
transformResult
if (unresolvedCatalogPaths?.size) {
void logErrorAndMaybeSendToBugsnag(
new JsonError(
`${unresolvedCatalogPaths.size} catalog paths cannot be found for explorer ${transformedProgram.slug}: ${[...unresolvedCatalogPaths].join(", ")}.`
)
)
}

const { requiredGrapherIds, requiredVariableIds } =
transformedProgram.decisionMatrix

type ChartRow = { id: number; config: string }
let grapherConfigRows: ChartRow[] = []
Expand Down Expand Up @@ -726,7 +743,7 @@
if (missingIds.length > 0) {
void logErrorAndMaybeSendToBugsnag(
new JsonError(
`Referenced variable IDs do not exist in the database for explorer ${program.slug}: ${missingIds.join(", ")}.`
`Referenced variable IDs do not exist in the database for explorer ${transformedProgram.slug}: ${missingIds.join(", ")}.`
)
)
}
Expand Down Expand Up @@ -758,10 +775,13 @@
return mergePartialGrapherConfigs(etlConfig, adminConfig)
})

const wpContent = program.wpBlockId
const wpContent = transformedProgram.wpBlockId
? await renderReusableBlock(
await getBlockContentFromSnapshot(knex, program.wpBlockId),
program.wpBlockId,
await getBlockContentFromSnapshot(
knex,
transformedProgram.wpBlockId
),
transformedProgram.wpBlockId,
knex
)
: undefined
Expand All @@ -772,7 +792,7 @@
<ExplorerPage
grapherConfigs={grapherConfigs}
partialGrapherConfigs={partialGrapherConfigs}
program={program}
program={transformedProgram}
wpContent={wpContent}
baseUrl={BAKED_BASE_URL}
urlMigrationSpec={urlMigrationSpec}
Expand Down
21 changes: 21 additions & 0 deletions db/model/Variable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
DimensionProperty,
GrapherInterface,
DbRawVariable,
VariablesTableName,
} from "@ourworldindata/types"
import { knexRaw } from "../db.js"

Expand Down Expand Up @@ -578,3 +579,23 @@ export interface VariableResultView {
table: string
shortName: string
}

export const getVariableIdsByCatalogPath = async (
catalogPaths: string[],
knex: db.KnexReadonlyTransaction
): Promise<Map<string, number | null>> => {
const rows: Pick<DbRawVariable, "id" | "catalogPath">[] = await knex
.select("id", "catalogPath")
.from(VariablesTableName)
.whereIn("catalogPath", catalogPaths)

const rowsByPath = _.keyBy(rows, "catalogPath")

// `rowsByPath` only contains the rows that were found, so we need to create
// a map where all keys from `catalogPaths` are present, and set the value to
// undefined if no row was found for that catalog path.
return new Map(
// Sort for good measure and determinism.
catalogPaths.sort().map((path) => [path, rowsByPath[path]?.id ?? null])
)
}
6 changes: 6 additions & 0 deletions explorer/ColumnGrammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ToleranceStrategy } from "@ourworldindata/utils"
import {
BooleanCellDef,
EnumCellDef,
EtlPathCellDef,
Grammar,
IntegerCellDef,
NumericCellDef,
Expand All @@ -22,6 +23,11 @@ export const ColumnGrammar: Grammar = {
keyword: "variableId",
description: "Numerical variable ID",
},
catalogPath: {
...EtlPathCellDef,
keyword: "catalogPath",
description: "Catalog path to the etl indicator",
},
slug: {
...SlugDeclarationCellDef,
keyword: "slug",
Expand Down
5 changes: 3 additions & 2 deletions explorer/Explorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
keyBy,
keyMap,
omitUndefinedValues,
parseIntOrUndefined,
PromiseCache,
PromiseSwitcher,
SerializedGridProgram,
Expand Down Expand Up @@ -501,8 +502,8 @@ export class Explorer

const yVariableIdsList = yVariableIds
.split(" ")
.map((item) => parseInt(item, 10))
.filter((item) => !isNaN(item))
.map(parseIntOrUndefined)
.filter((item) => item !== undefined)

const partialGrapherConfig =
this.partialGrapherConfigsByVariableId.get(yVariableIdsList[0]) ??
Expand Down
40 changes: 40 additions & 0 deletions explorer/ExplorerDecisionMatrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
identity,
trimObject,
uniq,
parseIntOrUndefined,
} from "@ourworldindata/utils"
import { ColumnTypeNames } from "@ourworldindata/types"
import {
Expand Down Expand Up @@ -141,6 +142,45 @@ export class DecisionMatrix {
)
}

get allColumnsWithIndicatorIds() {
const indicatorColKeywords = [
GrapherGrammar.yVariableIds.keyword,
GrapherGrammar.xVariableId.keyword,
GrapherGrammar.colorVariableId.keyword,
GrapherGrammar.sizeVariableId.keyword,
]
return this.table
.getColumns(indicatorColKeywords)
.filter((col) => !col.isMissing)
}

get requiredCatalogPaths(): Set<string> {
const allIndicators = this.allColumnsWithIndicatorIds
.flatMap((col) => col.uniqValues)
.flatMap((value) => value.split(" "))
.filter((value) => value !== "")

// Assume it's a catalog path if it doesn't look like a number
const catalogPaths = allIndicators.filter(
(indicator) => parseIntOrUndefined(indicator) === undefined
)

return new Set(catalogPaths)
}

// This is, basically, the inverse of `dropColumnTypes`.
get tableWithOriginalColumnNames() {
return this.table.renameColumns(
Object.fromEntries(
[...this.choiceNameToControlTypeMap.entries()].map(
([choiceName, controlType]) => {
return [choiceName, `${choiceName} ${controlType}`]
}
)
)
)
}

choiceNameToControlTypeMap: Map<ChoiceName, ExplorerControlType>
hash: string

Expand Down
Loading
Loading