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: multi-dim data pages #3657

Merged
merged 65 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
47eebef
feat(multi-dim): prototype
marcelgerber May 21, 2024
096f8a3
feat(multi-dim): Multi-dim data page config
marcelgerber Jul 22, 2024
05a199a
chore: rename folder
marcelgerber Jul 22, 2024
1791d00
enhance(multi-dim): fetch data on the server side
marcelgerber Jul 22, 2024
06b8835
enhance(multi-dim): use new react components
marcelgerber Jul 23, 2024
81ae088
enhance(multi-dim): remove timeout
marcelgerber Jul 23, 2024
f2c87af
wip(multi-dim): url serialization & deserialization works
marcelgerber Jul 23, 2024
471f674
fix: url serialization is not weird any more
marcelgerber Jul 24, 2024
862ea95
feat(site): hook to observe bounds of an element
marcelgerber Jul 24, 2024
6d9c2d3
fix(multi-dim): re-render on url change
marcelgerber Jul 24, 2024
f0e82c9
enhance(multi-dim): allow for default selection
marcelgerber Jul 24, 2024
174a993
fix: properly serialize country selection
marcelgerber Jul 24, 2024
07ff726
feat(multi-dim): move dropdowns to header
marcelgerber Jul 24, 2024
c902d79
style(multi-dim): split into multiple files
marcelgerber Jul 24, 2024
842ff4d
enhance(multi-dim): start implementing new design
marcelgerber Jul 24, 2024
5340b34
enhance: new styles are mostly in place
marcelgerber Jul 25, 2024
8c86638
style: rename css classes and refactor code a bit
marcelgerber Jul 25, 2024
a8242ed
fix: make ts happy
marcelgerber Jul 25, 2024
0809799
enhance: max width for dropdown text
marcelgerber Jul 25, 2024
cfdddcb
style: remove unused import
marcelgerber Jul 25, 2024
73a5969
feat(multi-dim): first steps towards baking
marcelgerber Jul 25, 2024
53e2464
enhance(multi-dim): better baking code, incl. mock router
marcelgerber Jul 25, 2024
c3f4f3c
enhance: get rid of temporary url param handling
marcelgerber Jul 25, 2024
0897b8d
chore: add causes-of-death to sample mdd pages
marcelgerber Jul 25, 2024
d2dd628
enhance(functions): don't fail if `canonicalUrl` cannot be parsed
marcelgerber Jul 25, 2024
01ab4ce
enhance: remove explicit `tab` handling
marcelgerber Jul 25, 2024
eee2a1b
enhance: more clever URL param resolution, don't fail if it's missing…
marcelgerber Jul 25, 2024
5ea21bb
chore: set default selection for all mdd demos
marcelgerber Jul 25, 2024
4958992
enhance: handle missing `partialGrapherConfig`
marcelgerber Jul 25, 2024
49b1e4e
enhance: prevent settings word wrap
marcelgerber Jul 25, 2024
2030d26
enhance: topic tags work
marcelgerber Jul 25, 2024
2381a8d
enhance: nicer topic tags
marcelgerber Jul 25, 2024
1b3a528
feat(multi-dim): change indicator config format
marcelgerber Jul 25, 2024
7a62c25
fix(multi-dim): handle missing indicators gracefully
marcelgerber Jul 25, 2024
41775b7
enhance(multi-dim): use topic tags from mdd config
marcelgerber Jul 30, 2024
94fd50e
enhance(multi-dim): specify static `title` and `titleVariant`
marcelgerber Jul 30, 2024
f65f8ca
enhance(multi-dim): add `mixed` mdd, to test faqs
marcelgerber Jul 30, 2024
598cfdd
refactor: move most datapage FAQ code over to `DatapageHelpers`
marcelgerber Jul 31, 2024
d88bfb4
feat(multi-dim): support faq entries
marcelgerber Jul 31, 2024
8a39dd2
chore: fix broken mdd pages
marcelgerber Aug 5, 2024
b3403f3
enhance(multi-dim): support `primaryTopic`
marcelgerber Aug 5, 2024
6c31702
fix: fix tsc build
marcelgerber Aug 7, 2024
655bcb4
fix: fix tsc build
marcelgerber Aug 7, 2024
fe7ee8c
style: fix eslint warnings
marcelgerber Aug 9, 2024
e4cc443
cleanup
marcelgerber Aug 9, 2024
622c47d
cleanup
marcelgerber Aug 9, 2024
39d2165
enhance(multi-dim): all props now available under `window._OWID_MULTI…
marcelgerber Aug 12, 2024
808d443
refactor: cleanup `MultiDimDataPageContent` a bit
marcelgerber Aug 12, 2024
50c3c1e
refactor: remove some old code
marcelgerber Aug 12, 2024
64ce88b
docs(multi-dim): basic README with lingo
marcelgerber Aug 12, 2024
b403bdf
cleanup: remove `relatedData` code
marcelgerber Aug 13, 2024
f3679ef
refactor: pull out custom hook
marcelgerber Aug 13, 2024
838c416
enhance: move more logic out into custom hooks
marcelgerber Aug 13, 2024
d3d70c8
refactor: get rid of unnecessary `grapherManager`
marcelgerber Aug 13, 2024
9ccc8e8
refactor: use custom type
marcelgerber Aug 13, 2024
c6d89f4
feat(settings): add feature flag code
marcelgerber Aug 13, 2024
dd97a07
enhance(multi-dim): feature flag
marcelgerber Aug 13, 2024
f00b5f2
fix: handle empty default selection
marcelgerber Aug 13, 2024
fa35753
fix: fix wrong import
marcelgerber Aug 15, 2024
f580f6d
refactor: move `MultiDimDataPageTypes` to types package
marcelgerber Aug 20, 2024
f5925f0
enhance: address review comment
marcelgerber Aug 20, 2024
e030e6d
enhance: add additional class on `<body>`
marcelgerber Aug 20, 2024
efe7493
refactor: move `MultiDimDataPage{Config,Url}` to `utils`
marcelgerber Aug 20, 2024
29b9dc2
style: add explicit return types
marcelgerber Aug 20, 2024
ef530be
test: fix test failures
marcelgerber Aug 20, 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
29 changes: 22 additions & 7 deletions adminSiteServer/mockSiteRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
} from "./plainRouterHelpers.js"
import { DEFAULT_LOCAL_BAKE_DIR } from "../site/SiteConstants.js"
import { DATA_INSIGHTS_ATOM_FEED_NAME } from "../site/gdocs/utils.js"
import { renderMultiDimDataPageBySlug } from "../baker/MultiDimBaker.js"

require("express-async-errors")

Expand Down Expand Up @@ -201,15 +202,29 @@ getPlainRouteNonIdempotentWithRWTransaction(
mockSiteRouter,
"/grapher/:slug",
async (req, res, trx) => {
const entity = await getChartConfigBySlug(trx, req.params.slug)
if (!entity) throw new JsonError("No such chart", 404)
const chartRow = await getChartConfigBySlug(trx, req.params.slug).catch(
console.error
)
if (chartRow) {
// XXX add dev-prod parity for this
res.set("Access-Control-Allow-Origin", "*")

// XXX add dev-prod parity for this
res.set("Access-Control-Allow-Origin", "*")
const previewDataPageOrGrapherPage =
await renderPreviewDataPageOrGrapherPage(chartRow.config, trx)
res.send(previewDataPageOrGrapherPage)
return
} else {
const page = await renderMultiDimDataPageBySlug(
trx,
req.params.slug
).catch(console.error)
if (page) {
res.send(page)
return
}
}

const previewDataPageOrGrapherPage =
await renderPreviewDataPageOrGrapherPage(entity.config, trx)
res.send(previewDataPageOrGrapherPage)
throw new JsonError("No such chart", 404)
}
)

Expand Down
111 changes: 110 additions & 1 deletion baker/DatapageHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,25 @@ import {
getLastUpdatedFromVariable,
getNextUpdateFromVariable,
omitUndefinedValues,
partition,
} from "@ourworldindata/utils"
import {
getGdocBaseObjectById,
getPublishedGdocBaseObjectBySlug,
loadGdocFromGdocBase,
} from "../db/model/Gdoc/GdocFactory.js"
import { OwidGoogleAuth } from "../db/OwidGoogleAuth.js"
import { GrapherInterface, OwidGdocBaseInterface } from "@ourworldindata/types"
import {
EnrichedFaq,
FaqDictionary,
GrapherInterface,
OwidGdocBaseInterface,
} from "@ourworldindata/types"
import { KnexReadWriteTransaction } from "../db/db.js"
import { parseFaqs } from "../db/model/Gdoc/rawToEnriched.js"
import { logErrorAndMaybeSendToBugsnag } from "../serverUtils/errorLog.js"
import { getSlugForTopicTag } from "./GrapherBakingUtils.js"
import { getShortPageCitation } from "../site/gdocs/utils.js"

export const getDatapageDataV2 = async (
variableMetadata: OwidVariableWithSource,
Expand Down Expand Up @@ -112,3 +123,101 @@ export const getDatapageGdoc = async (

return datapageGdoc
}

type EnrichedFaqLookupError = {
type: "error"
error: string
}

type EnrichedFaqLookupSuccess = {
type: "success"
enrichedFaq: EnrichedFaq
}

type EnrichedFaqLookupResult = EnrichedFaqLookupError | EnrichedFaqLookupSuccess

export const fetchAndParseFaqs = async (
knex: KnexReadWriteTransaction, // TODO: this transaction is only RW because somewhere inside it we fetch images
faqGdocIds: string[],
{ isPreviewing }: { isPreviewing: boolean }
) => {
const gdocFetchPromises = faqGdocIds.map((gdocId) =>
getDatapageGdoc(knex, gdocId, isPreviewing)
)
const gdocs = await Promise.all(gdocFetchPromises)
const gdocIdToFragmentIdToBlock: Record<string, FaqDictionary> = {}
gdocs.forEach((gdoc) => {
if (!gdoc) return
const faqs = parseFaqs(
("faqs" in gdoc.content && gdoc.content?.faqs) ?? [],
gdoc.id
)
gdocIdToFragmentIdToBlock[gdoc.id] = faqs.faqs
})

return gdocIdToFragmentIdToBlock
}

export const resolveFaqsForVariable = (
gdocIdToFragmentIdToBlock: Record<string, FaqDictionary>,
variableMetadata: OwidVariableWithSource
) => {
const resolvedFaqResults: EnrichedFaqLookupResult[] = variableMetadata
.presentation?.faqs
? variableMetadata.presentation.faqs.map((faq) => {
const enrichedFaq = gdocIdToFragmentIdToBlock[faq.gdocId]?.[
faq.fragmentId
] as EnrichedFaq | undefined
if (!enrichedFaq)
return {
type: "error",
error: `Could not find fragment ${faq.fragmentId} in gdoc ${faq.gdocId}`,
}
return {
type: "success",
enrichedFaq,
}
})
: []

const [resolvedFaqs, errors] = partition(
resolvedFaqResults,
(result) => result.type === "success"
) as [EnrichedFaqLookupSuccess[], EnrichedFaqLookupError[]]

return { resolvedFaqs, errors }
}

export const getPrimaryTopic = async (
knex: KnexReadWriteTransaction,
firstTopicTag: string | undefined
) => {
if (!firstTopicTag) return undefined

let topicSlug: string
try {
topicSlug = await getSlugForTopicTag(knex, firstTopicTag)
} catch (e) {
await logErrorAndMaybeSendToBugsnag(
`Data page is using "${firstTopicTag}" as its primary tag, which we are unable to resolve to a tag in the grapher DB`
)
return undefined
}

if (topicSlug) {
const gdoc = await getPublishedGdocBaseObjectBySlug(
knex,
topicSlug,
true
)
if (gdoc) {
const citation = getShortPageCitation(
gdoc.content.authors,
gdoc.content.title ?? "",
gdoc?.publishedAt
)
return { topicTag: firstTopicTag, citation }
}
}
return undefined
}
98 changes: 15 additions & 83 deletions baker/GrapherBaker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
keyBy,
mergePartialGrapherConfigs,
compact,
partition,
} from "@ourworldindata/utils"
import fs from "fs-extra"
import * as lodash from "lodash"
Expand All @@ -37,29 +36,28 @@ import {
DimensionProperty,
OwidVariableWithSource,
OwidChartDimensionInterface,
EnrichedFaq,
FaqEntryData,
FaqDictionary,
ImageMetadata,
OwidGdocBaseInterface,
} from "@ourworldindata/types"
import ProgressBar from "progress"
import {
getVariableData,
getMergedGrapherConfigForVariable,
getVariableOfDatapageIfApplicable,
} from "../db/model/Variable.js"
import { getDatapageDataV2, getDatapageGdoc } from "./DatapageHelpers.js"
import {
fetchAndParseFaqs,
getDatapageDataV2,
getPrimaryTopic,
resolveFaqsForVariable,
} from "./DatapageHelpers.js"
import { Image, getAllImages } from "../db/model/Image.js"
import { logErrorAndMaybeSendToBugsnag } from "../serverUtils/errorLog.js"

import { parseFaqs } from "../db/model/Gdoc/rawToEnriched.js"
import { getShortPageCitation } from "../site/gdocs/utils.js"
import { getSlugForTopicTag, getTagToSlugMap } from "./GrapherBakingUtils.js"
import { getTagToSlugMap } from "./GrapherBakingUtils.js"
import { knexRaw } from "../db/db.js"
import { getRelatedChartsForVariable } from "../db/model/Chart.js"
import pMap from "p-map"
import { getPublishedGdocBaseObjectBySlug } from "../db/model/Gdoc/GdocFactory.js"

const renderDatapageIfApplicable = async (
grapher: GrapherInterface,
Expand Down Expand Up @@ -114,18 +112,6 @@ export const renderDataPageOrGrapherPage = async (
return renderGrapherPage(grapher, knex)
}

type EnrichedFaqLookupError = {
type: "error"
error: string
}

type EnrichedFaqLookupSuccess = {
type: "success"
enrichedFaq: EnrichedFaq
}

type EnrichedFaqLookupResult = EnrichedFaqLookupError | EnrichedFaqLookupSuccess

export async function renderDataPageV2(
{
variableId,
Expand Down Expand Up @@ -158,45 +144,16 @@ export async function renderDataPageV2(
? mergePartialGrapherConfigs(grapherConfigForVariable, pageGrapher)
: pageGrapher ?? {}

const faqDocs = compact(
const faqDocIds = compact(
uniq(variableMetadata.presentation?.faqs?.map((faq) => faq.gdocId))
)
const gdocFetchPromises = faqDocs.map((gdocId) =>
getDatapageGdoc(knex, gdocId, isPreviewing)

const faqGdocs = await fetchAndParseFaqs(knex, faqDocIds, { isPreviewing })

const { resolvedFaqs, errors: faqResolveErrors } = resolveFaqsForVariable(
faqGdocs,
variableMetadata
)
const gdocs = await Promise.all(gdocFetchPromises)
const gdocIdToFragmentIdToBlock: Record<string, FaqDictionary> = {}
gdocs.forEach((gdoc) => {
if (!gdoc) return
const faqs = parseFaqs(
("faqs" in gdoc.content && gdoc.content?.faqs) ?? [],
gdoc.id
)
gdocIdToFragmentIdToBlock[gdoc.id] = faqs.faqs
})

const resolvedFaqsResults: EnrichedFaqLookupResult[] = variableMetadata
.presentation?.faqs
? variableMetadata.presentation.faqs.map((faq) => {
const enrichedFaq = gdocIdToFragmentIdToBlock[faq.gdocId]?.[
faq.fragmentId
] as EnrichedFaq | undefined
if (!enrichedFaq)
return {
type: "error",
error: `Could not find fragment ${faq.fragmentId} in gdoc ${faq.gdocId}`,
}
return {
type: "success",
enrichedFaq,
}
})
: []

const [resolvedFaqs, faqResolveErrors] = partition(
resolvedFaqsResults,
(result) => result.type === "success"
) as [EnrichedFaqLookupSuccess[], EnrichedFaqLookupError[]]

if (faqResolveErrors.length > 0) {
for (const error of faqResolveErrors) {
Expand Down Expand Up @@ -234,32 +191,7 @@ export async function renderDataPageV2(
)

const firstTopicTag = datapageData.topicTagsLinks?.[0]

let slug = ""
if (firstTopicTag) {
try {
slug = await getSlugForTopicTag(knex, firstTopicTag)
} catch (error) {
await logErrorAndMaybeSendToBugsnag(
`Datapage with variableId "${variableId}" and title "${datapageData.title.title}" is using "${firstTopicTag}" as its primary tag, which we are unable to resolve to a tag in the grapher DB`
)
}
let gdoc: OwidGdocBaseInterface | undefined = undefined
if (slug) {
gdoc = await getPublishedGdocBaseObjectBySlug(knex, slug, true)
}
if (gdoc) {
const citation = getShortPageCitation(
gdoc.content.authors,
gdoc.content.title ?? "",
gdoc?.publishedAt
)
datapageData.primaryTopic = {
topicTag: firstTopicTag,
citation,
}
}
}
datapageData.primaryTopic = await getPrimaryTopic(knex, firstTopicTag)

// Get the charts this variable is being used in (aka "related charts")
// and exclude the current chart to avoid duplicates
Expand Down
Loading
Loading