diff --git a/README.md b/README.md index 5cdc623ec68..9769dd9cdbc 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The monorepo we use at [Our World in Data](https://ourworldindata.org) to create and publish embeddable, interactive visualizations like this one: -[![A Grapher chart showing world-wide life expectancy at birth. Click for interactive.](https://ourworldindata.org/grapher/exports/life-expectancy.svg)](https://ourworldindata.org/grapher/life-expectancy) +[![A Grapher chart showing world-wide life expectancy at birth. Click for interactive.](https://ourworldindata.org/grapher/life-expectancy.svg)](https://ourworldindata.org/grapher/life-expectancy) ## ✋ Disclaimer diff --git a/adminSiteClient/ChartRow.tsx b/adminSiteClient/ChartRow.tsx index 410842032e7..ac2bb2733b6 100644 --- a/adminSiteClient/ChartRow.tsx +++ b/adminSiteClient/ChartRow.tsx @@ -7,8 +7,8 @@ import { Timeago } from "./Forms.js" import { EditableTags } from "./EditableTags.js" import { AdminAppContext, AdminAppContextType } from "./AdminAppContext.js" import { - BAKED_GRAPHER_EXPORTS_BASE_URL, BAKED_GRAPHER_URL, + GRAPHER_DYNAMIC_THUMBNAIL_URL, } from "../settings/clientSettings.js" import { ChartListItem, showChartType } from "./ChartList.js" import { TaggableType, DbChartTagJoin } from "@ourworldindata/utils" @@ -48,11 +48,19 @@ export class ChartRow extends React.Component<{ return ( - + {chart.isPublished ? ( diff --git a/adminSiteClient/admin.scss b/adminSiteClient/admin.scss index 7fc8df085ef..d995aca4eaf 100644 --- a/adminSiteClient/admin.scss +++ b/adminSiteClient/admin.scss @@ -753,6 +753,7 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) { .chartPreview { width: 100%; height: auto; + max-width: 140px; } .internalNotes { diff --git a/adminSiteServer/mockSiteRouter.ts b/adminSiteServer/mockSiteRouter.ts index 574a1c64d01..19001c2bd52 100644 --- a/adminSiteServer/mockSiteRouter.ts +++ b/adminSiteServer/mockSiteRouter.ts @@ -25,7 +25,6 @@ import { import { BAKED_BASE_URL, BASE_DIR, - BAKED_SITE_DIR, LEGACY_WORDPRESS_IMAGE_URL, } from "../settings/serverSettings.js" @@ -35,13 +34,9 @@ import { countriesIndexPage, } from "../baker/countryProfiles.js" import { makeSitemap } from "../baker/sitemap.js" -import { - getChartConfigBySlug, - getChartVariableData, -} from "../db/model/Chart.js" +import { getChartConfigBySlug } from "../db/model/Chart.js" import { countryProfileSpecs } from "../site/countryProfileProjects.js" import { ExplorerAdminServer } from "../explorerAdminServer/ExplorerAdminServer.js" -import { grapherToSVG } from "../baker/GrapherImageBaker.js" import { getVariableData, getVariableMetadata } from "../db/model/Variable.js" import { MultiEmbedderTestPage } from "../site/multiembedder/MultiEmbedderTestPage.js" import { @@ -429,27 +424,9 @@ mockSiteRouter.use( res.redirect(assetUrl) } ) -mockSiteRouter.use( - "/exports", - express.static(path.join(BAKED_SITE_DIR, "exports")) -) mockSiteRouter.use("/assets", express.static("dist/assets")) -// TODO: this used to be a mockSiteRouter.use call but otherwise it looked like a route and -// it didn't look like it was making use of any middleware - if this causese issues then -// this has to be reverted to a use call -getPlainRouteWithROTransaction( - mockSiteRouter, - "/grapher/exports/:slug.svg", - async (req, res, trx) => { - const grapher = await getChartConfigBySlug(trx, req.params.slug) - const vardata = await getChartVariableData(grapher.config) - res.setHeader("Content-Type", "image/svg+xml") - res.send(await grapherToSVG(grapher.config, vardata)) - } -) - mockSiteRouter.use( "/fonts", express.static(path.join(BASE_DIR, "public/fonts"), { diff --git a/adminSiteServer/testPageRouter.tsx b/adminSiteServer/testPageRouter.tsx index 93199b4c70a..8dbd771c3ff 100644 --- a/adminSiteServer/testPageRouter.tsx +++ b/adminSiteServer/testPageRouter.tsx @@ -41,6 +41,7 @@ import { ColorSchemes, GrapherProgrammaticInterface, } from "@ourworldindata/grapher" +import { GRAPHER_DYNAMIC_THUMBNAIL_URL } from "../settings/clientSettings.js" const IS_LIVE = ADMIN_BASE_URL === "https://owid.cloud" const DEFAULT_COMPARISON_URL = "https://ourworldindata.org" @@ -467,7 +468,7 @@ getPlainRouteWithROTransaction( Pick & { config: DbRawChartConfig["full"] } >( trx, - `--sql + `-- sql select ca.id, cc.full as config from charts ca join chart_configs cc @@ -539,11 +540,15 @@ function PreviewTestPage(props: { charts: any[] }) { href={`https://ourworldindata.org/grapher/${chart.slug}`} > - - + + ))} @@ -732,7 +737,7 @@ getPlainRouteWithROTransaction( async (req, res, trx) => { const rows = await db.knexRaw<{ config: DbRawChartConfig["full"] }>( trx, - `--sql + `-- sql SELECT cc.full as config FROM charts ca JOIN chart_configs cc @@ -752,7 +757,7 @@ getPlainRouteWithROTransaction( async (req, res, trx) => { const rows = await db.knexRaw<{ config: DbRawChartConfig["full"] }>( trx, - `--sql + `-- sql SELECT cc.full as config FROM charts ca JOIN chart_configs cc diff --git a/baker/GrapherBaker.tsx b/baker/GrapherBaker.tsx index 74f3fa4eb84..f537eda2a7a 100644 --- a/baker/GrapherBaker.tsx +++ b/baker/GrapherBaker.tsx @@ -5,7 +5,6 @@ import { excludeUndefined, urlToSlug, without, - deserializeJSONFromHTML, uniq, keyBy, compact, @@ -13,9 +12,7 @@ import { } from "@ourworldindata/utils" import fs from "fs-extra" import * as lodash from "lodash" -import { bakeGrapherToSvgAndPng } from "./GrapherImageBaker.js" import { - OPTIMIZE_SVG_EXPORTS, BAKED_BASE_URL, BAKED_GRAPHER_URL, } from "../settings/serverSettings.js" @@ -30,7 +27,6 @@ import { } from "../db/model/Post.js" import { GrapherInterface, - OwidVariableDataMetadataDimensions, DimensionProperty, OwidVariableWithSource, OwidChartDimensionInterface, @@ -42,7 +38,6 @@ import { } from "@ourworldindata/types" import ProgressBar from "progress" import { - getVariableData, getMergedGrapherConfigForVariable, getVariableOfDatapageIfApplicable, } from "../db/model/Variable.js" @@ -268,29 +263,12 @@ const renderGrapherPage = async ( ) } -const chartIsSameVersion = async ( - htmlPath: string, - grapherVersion: number | undefined -): Promise => { - if (fs.existsSync(htmlPath)) { - // If the chart is the same version, we can potentially skip baking the data and exports (which is by far the slowest part) - const html = await fs.readFile(htmlPath, "utf8") - const savedVersion = deserializeJSONFromHTML(html) - return savedVersion?.version === grapherVersion - } else { - return false - } -} - -const bakeGrapherPageAndVariablesPngAndSVGIfChanged = async ( +const bakeGrapherPage = async ( bakedSiteDir: string, imageMetadataDictionary: Record, grapher: GrapherInterface, knex: db.KnexReadonlyTransaction ) => { - const htmlPath = `${bakedSiteDir}/grapher/${grapher.slug}.html` - const isSameVersion = await chartIsSameVersion(htmlPath, grapher.version) - // Need to set up the connection for using TypeORM in // renderDataPageOrGrapherPage() when baking using multiple worker threads // (MAX_NUM_BAKE_PROCESSES > 1). It could be done in @@ -309,29 +287,6 @@ const bakeGrapherPageAndVariablesPngAndSVGIfChanged = async ( ) ) console.log(outPath) - - const variableIds = lodash.uniq( - grapher.dimensions?.map((d) => d.variableId) - ) - if (!variableIds.length) return - - await fs.mkdirp(`${bakedSiteDir}/grapher/exports/`) - const svgPath = `${bakedSiteDir}/grapher/exports/${grapher.slug}.svg` - const pngPath = `${bakedSiteDir}/grapher/exports/${grapher.slug}.png` - if (!isSameVersion || !fs.existsSync(svgPath) || !fs.existsSync(pngPath)) { - const loadDataMetadataPromises: Promise[] = - variableIds.map(getVariableData) - const variableDataMetadata = await Promise.all(loadDataMetadataPromises) - const variableDataMedadataMap = new Map( - variableDataMetadata.map((item) => [item.metadata.id, item]) - ) - await bakeGrapherToSvgAndPng( - `${bakedSiteDir}/grapher/exports`, - grapher, - variableDataMedadataMap, - OPTIMIZE_SVG_EXPORTS - ) - } } const deleteOldGraphers = async (bakedSiteDir: string, newSlugs: string[]) => { @@ -345,17 +300,13 @@ const deleteOldGraphers = async (bakedSiteDir: string, newSlugs: string[]) => { // do not delete grapher slugs redirected to explorers .filter((slug) => !isPathRedirectedToExplorer(`/grapher/${slug}`)) for (const slug of toRemove) { - console.log(`DELETING ${slug}`) - try { - const paths = [ - `${bakedSiteDir}/grapher/${slug}.html`, - `${bakedSiteDir}/grapher/exports/${slug}.png`, - ] //, `${BAKED_SITE_DIR}/grapher/exports/${slug}.svg`] - await Promise.all(paths.map((p) => fs.unlink(p))) - paths.map((p) => console.log(p)) - } catch (err) { - console.error(err) - } + const path = `${bakedSiteDir}/grapher/${slug}.html` + console.log(`DELETING ${path}`) + fs.unlink(path, (err) => + err + ? console.error(`Error deleting ${path}`, err) + : console.log(`Deleted ${path}`) + ) } } @@ -381,7 +332,7 @@ export const bakeSingleGrapherChart = async ( return } - await bakeGrapherPageAndVariablesPngAndSVGIfChanged( + await bakeGrapherPage( args.bakedSiteDir, args.imageMetadataDictionary, grapher, @@ -390,72 +341,71 @@ export const bakeSingleGrapherChart = async ( return args } -export const bakeAllChangedGrapherPagesVariablesPngSvgAndDeleteRemovedGraphers = - async (bakedSiteDir: string, knex: db.KnexReadonlyTransaction) => { - const chartsToBake = await knexRaw< - Pick & { - config: DbRawChartConfig["full"] - slug: string - } - >( - knex, - `-- sql - SELECT - c.id, - cc.full as config, - cc.slug - FROM charts c - JOIN chart_configs cc ON c.configId = cc.id - WHERE JSON_EXTRACT(cc.full, "$.isPublished")=true - ORDER BY cc.slug ASC - ` - ) +export const bakeAllChangedGrapherPagesAndDeleteRemovedGraphers = async ( + bakedSiteDir: string, + knex: db.KnexReadonlyTransaction +) => { + const chartsToBake = await knexRaw< + Pick & { + config: DbRawChartConfig["full"] + slug: string + } + >( + knex, + `-- sql + SELECT + c.id, + cc.full as config, + cc.slug + FROM charts c + JOIN chart_configs cc ON c.configId = cc.id + WHERE JSON_EXTRACT(cc.full, "$.isPublished")=true + ORDER BY cc.slug ASC` + ) - const newSlugs = chartsToBake.map((row) => row.slug) - await fs.mkdirp(bakedSiteDir + "/grapher") + const newSlugs = chartsToBake.map((row) => row.slug) + await fs.mkdirp(bakedSiteDir + "/grapher") - // Prefetch imageMetadata instead of each grapher page fetching - // individually. imageMetadata is used by the google docs powering rich - // text (including images) in data pages. - const imageMetadataDictionary = await getAllImages(knex).then( - (images) => keyBy(images, "filename") - ) + // Prefetch imageMetadata instead of each grapher page fetching + // individually. imageMetadata is used by the google docs powering rich + // text (including images) in data pages. + const imageMetadataDictionary = await getAllImages(knex).then((images) => + keyBy(images, "filename") + ) - const jobs: BakeSingleGrapherChartArguments[] = chartsToBake.map( - (row) => ({ - id: row.id, - config: row.config, - bakedSiteDir: bakedSiteDir, - slug: row.slug, - imageMetadataDictionary, - }) - ) + const jobs: BakeSingleGrapherChartArguments[] = chartsToBake.map((row) => ({ + id: row.id, + config: row.config, + bakedSiteDir: bakedSiteDir, + slug: row.slug, + imageMetadataDictionary, + })) - const progressBar = new ProgressBar( - "bake grapher page [:bar] :current/:total :elapseds :rate/s :etas :name\n", - { - width: 20, - total: chartsToBake.length + 1, - renderThrottle: 0, - } - ) + const progressBar = new ProgressBar( + "bake grapher page [:bar] :current/:total :elapseds :rate/s :etas :name\n", + { + width: 20, + total: chartsToBake.length + 1, + renderThrottle: 0, + } + ) - await pMap( - jobs, - async (job) => { - // We want to run this code on multiple threads, so we need to - // be able to use multiple transactions so that we can use - // multiple connections to the database. - // Read-write consistency is not a concern here, thankfully. - await db.knexReadWriteTransaction( - async (knex) => await bakeSingleGrapherChart(job, knex), - db.TransactionCloseMode.KeepOpen - ) - progressBar.tick({ name: `slug ${job.slug}` }) - }, - { concurrency: 10 } - ) + await pMap( + jobs, + async (job) => { + // We want to run this code on multiple threads, so we need to + // be able to use multiple transactions so that we can use + // multiple connections to the database. + // Read-write consistency is not a concern here, thankfully. + await db.knexReadWriteTransaction( + async (knex) => await bakeSingleGrapherChart(job, knex), + db.TransactionCloseMode.KeepOpen + ) + progressBar.tick({ name: `slug ${job.slug}` }) + }, + { concurrency: 10 } + ) - await deleteOldGraphers(bakedSiteDir, excludeUndefined(newSlugs)) - progressBar.tick({ name: `✅ Deleted old graphers` }) - } + await deleteOldGraphers(bakedSiteDir, excludeUndefined(newSlugs)) + progressBar.tick({ name: `✅ Deleted old graphers` }) +} diff --git a/baker/GrapherBakingUtils.ts b/baker/GrapherBakingUtils.ts index 6b62bf76828..11aeae74b93 100644 --- a/baker/GrapherBakingUtils.ts +++ b/baker/GrapherBakingUtils.ts @@ -1,26 +1,9 @@ -import { glob } from "glob" -import path from "path" import * as lodash from "lodash" -import { - OPTIMIZE_SVG_EXPORTS, - BAKED_SITE_DIR, -} from "../settings/serverSettings.js" -import { BAKED_SITE_EXPORTS_BASE_URL } from "../settings/clientSettings.js" import * as db from "../db/db.js" -import { bakeGraphersToSvgs } from "../baker/GrapherImageBaker.js" -import { mapSlugsToIds } from "../db/model/Chart.js" import md5 from "md5" import { DbPlainTag, Url } from "@ourworldindata/utils" -interface ChartExportMeta { - key: string - svgUrl: string - version: number - width: number - height: number -} - // Splits a grapher URL like https://ourworldindata.org/grapher/soil-lifespans?tab=chart // into its slug (soil-lifespans) and queryStr (?tab=chart) export const grapherUrlToSlugAndQueryStr = (grapherUrl: string) => { @@ -47,99 +30,6 @@ export const grapherSlugToExportFileKey = ( return `${slug}${queryStr ? `${separator}${maybeHashedQueryStr}` : ""}` } -export interface GrapherExports { - get: (grapherUrl: string) => ChartExportMeta | undefined -} - -export const bakeGrapherUrls = async ( - knex: db.KnexReadonlyTransaction, - urls: string[] -) => { - const currentExports = await getGrapherExportsByUrl() - const slugToId = await mapSlugsToIds(knex) - const toBake = [] - - // Check that we need to bake this url, and don't already have an export - for (const url of urls) { - const current = currentExports.get(url) - if (!current) { - toBake.push(url) - continue - } - - const slug = lodash.last(Url.fromURL(url).pathname?.split("/")) - if (!slug) { - console.warn(`Invalid chart url ${url}`) - continue - } - - const chartId = slugToId[slug] - if (chartId === undefined) { - console.warn(`Couldn't find chart with slug ${slug}`) - continue - } - - const rows = await db.knexRaw<{ version: number }>( - knex, - `-- sql - SELECT cc.full->>"$.version" AS version - FROM charts c - JOIN chart_configs cc ON c.configId = cc.id - WHERE c.id=? - `, - [chartId] - ) - if (!rows.length) { - console.warn(`Mysteriously missing chart by id ${chartId}`) - continue - } - - if (rows[0].version > current.version) toBake.push(url) - } - - if (toBake.length > 0) { - await bakeGraphersToSvgs( - knex, - toBake, - `${BAKED_SITE_DIR}/exports`, - OPTIMIZE_SVG_EXPORTS - ) - } -} - -export const getGrapherExportsByUrl = async (): Promise => { - // Index the files to see what we have available, using the most recent version - // if multiple exports exist - const files = await glob(`${BAKED_SITE_DIR}/exports/*.svg`) - const exportsByKey = new Map() - for (const filepath of files) { - const filename = path.basename(filepath) - const [key, version, dims] = filename.toLowerCase().split("_") - const versionNumber = parseInt(version.split("v")[1]) - const [width, height] = dims.split("x") - - const current = exportsByKey.get(key) - if (!current || current.version < versionNumber) { - exportsByKey.set(key, { - key: key, - svgUrl: `${BAKED_SITE_EXPORTS_BASE_URL}/${filename}`, - version: versionNumber, - width: parseInt(width), - height: parseInt(height), - }) - } - } - - return { - get(grapherUrl: string) { - const { slug, queryStr } = grapherUrlToSlugAndQueryStr(grapherUrl) - return exportsByKey.get( - grapherSlugToExportFileKey(slug, queryStr).toLowerCase() - ) - }, - } -} - /** * Returns a map that can resolve Tag names and Tag IDs to the Tag's slug * e.g. diff --git a/baker/GrapherImageBaker.tsx b/baker/GrapherImageBaker.tsx index 51362d1cafa..74547910e80 100644 --- a/baker/GrapherImageBaker.tsx +++ b/baker/GrapherImageBaker.tsx @@ -6,17 +6,9 @@ import { } from "@ourworldindata/types" import { Grapher, GrapherProgrammaticInterface } from "@ourworldindata/grapher" import { MultipleOwidVariableDataDimensionsMap } from "@ourworldindata/utils" -import fs from "fs-extra" import path from "path" -import sharp from "sharp" -import svgo from "svgo" import * as db from "../db/db.js" -import { getDataForMultipleVariables } from "../db/model/Variable.js" -import { - grapherSlugToExportFileKey, - grapherUrlToSlugAndQueryStr, -} from "./GrapherBakingUtils.js" -import pMap from "p-map" +import { grapherSlugToExportFileKey } from "./GrapherBakingUtils.js" import { BAKED_GRAPHER_URL } from "../settings/clientSettings.js" interface SvgFilenameFragments { @@ -27,31 +19,6 @@ interface SvgFilenameFragments { queryStr?: string } -export async function bakeGrapherToSvgAndPng( - outDir: string, - jsonConfig: GrapherInterface, - vardata: MultipleOwidVariableDataDimensionsMap, - optimizeSvgs = false -) { - const grapher = initGrapherForSvgExport(jsonConfig) - grapher.receiveOwidData(vardata) - const outPath = path.join(outDir, grapher.slug as string) - - let svgCode = grapher.staticSVG - if (optimizeSvgs) svgCode = await optimizeSvg(svgCode) - - return Promise.all([ - fs - .writeFile(`${outPath}.svg`, svgCode) - .then(() => console.log(`${outPath}.svg`)), - sharp(Buffer.from(grapher.staticSVG), { density: 144 }) - .png() - .resize(grapher.defaultBounds.width, grapher.defaultBounds.height) - .flatten({ background: "#ffffff" }) - .toFile(`${outPath}.png`), - ]) -} - export async function getGraphersAndRedirectsBySlug( knex: db.KnexReadonlyTransaction ) { @@ -99,41 +66,6 @@ export async function getPublishedGraphersBySlug( return { graphersBySlug, graphersById } } -export async function bakeGrapherToSvg( - jsonConfig: GrapherInterface, - outDir: string, - slug: string, - queryStr = "", - optimizeSvgs = false, - overwriteExisting = false, - verbose = true -) { - const grapher = initGrapherForSvgExport(jsonConfig, queryStr) - const { width, height } = grapher.defaultBounds - const outPath = buildSvgOutFilepath( - outDir, - { - slug, - version: jsonConfig.version ?? 0, - width, - height, - queryStr, - }, - verbose - ) - - if (fs.existsSync(outPath) && !overwriteExisting) return - const variableIds = grapher.dimensions.map((d) => d.variableId) - const vardata = await getDataForMultipleVariables(variableIds) - grapher.receiveOwidData(vardata) - - let svgCode = grapher.staticSVG - if (optimizeSvgs) svgCode = await optimizeSvg(svgCode) - - await fs.writeFile(outPath, svgCode) - return svgCode -} - export function initGrapherForSvgExport( jsonConfig: GrapherProgrammaticInterface, queryStr: string = "" @@ -176,57 +108,6 @@ export function buildSvgOutFilepath( return outPath } -export async function bakeGraphersToSvgs( - knex: db.KnexReadonlyTransaction, - grapherUrls: string[], - outDir: string, - optimizeSvgs = false -) { - await fs.mkdirp(outDir) - const graphersBySlug = await getGraphersAndRedirectsBySlug(knex) - - return pMap( - grapherUrls, - async (grapherUrl) => { - const { slug, queryStr } = grapherUrlToSlugAndQueryStr(grapherUrl) - const jsonConfig = graphersBySlug.get(slug) - if (jsonConfig) { - return await bakeGrapherToSvg( - jsonConfig, - outDir, - slug, - queryStr, - optimizeSvgs - ) - } - return undefined - }, - { concurrency: 10 } - ) -} - -const svgoConfig: svgo.Config = { - floatPrecision: 2, - plugins: [ - { - name: "preset-default", - params: { - overrides: { - // disable certain plugins - collapseGroups: false, // breaks the "Our World in Data" logo in the upper right - removeUnknownsAndDefaults: false, // would remove hrefs from links () - removeViewBox: false, - }, - }, - }, - ], -} - -async function optimizeSvg(svgString: string): Promise { - const optimizedSvg = await svgo.optimize(svgString, svgoConfig) - return optimizedSvg.data -} - export async function grapherToSVG( jsonConfig: GrapherInterface, vardata: MultipleOwidVariableDataDimensionsMap diff --git a/baker/SiteBaker.tsx b/baker/SiteBaker.tsx index bb395c4affc..03bc0932afc 100644 --- a/baker/SiteBaker.tsx +++ b/baker/SiteBaker.tsx @@ -5,7 +5,7 @@ import "../serverUtils/instrument.js" import fs from "fs-extra" import path from "path" import { glob } from "glob" -import { keyBy, without, uniq, mapValues, pick, chunk } from "lodash" +import { keyBy, without, mapValues, pick, chunk } from "lodash" import ProgressBar from "progress" import * as db from "../db/db.js" import { @@ -37,11 +37,6 @@ import { renderGdocTombstone, renderExplorerIndexPage, } from "../baker/siteRenderers.js" -import { - bakeGrapherUrls, - getGrapherExportsByUrl, - GrapherExports, -} from "../baker/GrapherBakingUtils.js" import { makeSitemap } from "../baker/sitemap.js" import { bakeCountries } from "../baker/countryProfiles.js" import { @@ -69,7 +64,7 @@ import { getRedirects, flushCache as redirectsFlushCache, } from "./redirects.js" -import { bakeAllChangedGrapherPagesVariablesPngSvgAndDeleteRemovedGraphers } from "./GrapherBaker.js" +import { bakeAllChangedGrapherPagesAndDeleteRemovedGraphers } from "./GrapherBaker.js" import { EXPLORERS_ROUTE_FOLDER } from "@ourworldindata/explorer" import { GIT_CMS_DIR } from "../gitCms/GitCmsConstants.js" import { @@ -87,10 +82,7 @@ import { GdocPost } from "../db/model/Gdoc/GdocPost.js" import { getAllImages } from "../db/model/Image.js" import { generateEmbedSnippet } from "../site/viteUtils.js" import { logErrorAndMaybeCaptureInSentry } from "../serverUtils/errorLog.js" -import { - getChartEmbedUrlsInPublishedWordpressPosts, - mapSlugsToConfigs, -} from "../db/model/Chart.js" +import { mapSlugsToConfigs } from "../db/model/Chart.js" import { FeatureFlagFeature } from "../settings/clientSettings.js" import pMap from "p-map" import { GdocDataInsight } from "../db/model/Gdoc/GdocDataInsight.js" @@ -134,7 +126,6 @@ type PrefetchedAttachments = { const wordpressSteps = [ "assets", "blogIndex", - "embeds", "redirects", "rss", "wordpressPosts", @@ -190,7 +181,6 @@ function getProgressBarTotal(bakeSteps: BakeStepConfig): number { } export class SiteBaker { - private grapherExports!: GrapherExports private bakedSiteDir: string baseUrl: string progressBar: ProgressBar @@ -215,20 +205,6 @@ export class SiteBaker { this.explorerAdminServer = new ExplorerAdminServer(GIT_CMS_DIR) } - private async bakeEmbeds(knex: db.KnexReadonlyTransaction) { - if (!this.bakeSteps.has("embeds")) return - - // Find all grapher urls used as embeds in all Wordpress posts on the site - const grapherUrls = uniq( - await getChartEmbedUrlsInPublishedWordpressPosts(knex) - ) - - await bakeGrapherUrls(knex, grapherUrls) - - this.grapherExports = await getGrapherExportsByUrl() - this.progressBar.tick({ name: "✅ baked embeds" }) - } - private async bakeCountryProfiles(knex: db.KnexReadonlyTransaction) { if (!this.bakeSteps.has("countryProfiles")) return await Promise.all( @@ -242,8 +218,7 @@ export class SiteBaker { const html = await renderCountryProfile( spec, country, - knex, - this.grapherExports + knex ).catch(() => console.error( `${country.name} country profile not baked for project "${spec.project}". Check that both pages "${spec.landingPageSlug}" and "${spec.genericProfileSlug}" exist and are published.` @@ -290,12 +265,7 @@ export class SiteBaker { // Bake an individual post/page private async bakePost(post: FullPost, knex: db.KnexReadonlyTransaction) { - const html = await renderPost( - post, - knex, - this.baseUrl, - this.grapherExports - ) + const html = await renderPost(post, knex, this.baseUrl) const outPath = path.join(this.bakedSiteDir, `${post.slug}.html`) await fs.mkdirp(path.dirname(outPath)) @@ -1118,7 +1088,6 @@ export class SiteBaker { async bakeWordpressPages(knex: db.KnexReadonlyTransaction) { await this.bakeRedirects(knex) - await this.bakeEmbeds(knex) await this.bakeBlogIndex(knex) await this.bakeRSS(knex) await this.bakeAssets(knex) @@ -1133,12 +1102,12 @@ export class SiteBaker { await this.bakeCountryProfiles(knex) await this.bakeExplorers(knex) if (this.bakeSteps.has("charts")) { - await bakeAllChangedGrapherPagesVariablesPngSvgAndDeleteRemovedGraphers( + await bakeAllChangedGrapherPagesAndDeleteRemovedGraphers( this.bakedSiteDir, knex ) this.progressBar.tick({ - name: "✅ bakeAllChangedGrapherPagesVariablesPngSvgAndDeleteRemovedGraphers", + name: "✅ bakeAllChangedGrapherPagesAndDeleteRemovedGraphers", }) } await this.bakeMultiDimPages(knex) diff --git a/baker/formatWordpressPost.tsx b/baker/formatWordpressPost.tsx index 97f1798f4ba..f584e9f30fc 100644 --- a/baker/formatWordpressPost.tsx +++ b/baker/formatWordpressPost.tsx @@ -2,12 +2,10 @@ import cheerio from "cheerio" import urlSlug from "url-slug" import ReactDOMServer from "react-dom/server.js" import { HTTPS_ONLY } from "../settings/serverSettings.js" -import { GrapherExports } from "../baker/GrapherBakingUtils.js" -import { FormattingOptions } from "@ourworldindata/types" -import { FormattedPost, FullPost, TocHeading } from "@ourworldindata/utils" +import { FormattingOptions, GRAPHER_PREVIEW_CLASS } from "@ourworldindata/types" +import { FormattedPost, FullPost, TocHeading, Url } from "@ourworldindata/utils" import { parseKeyValueArgs } from "../serverUtils/wordpressUtils.js" import { Footnote } from "../site/Footnote.js" -import { LoadingIndicator } from "@ourworldindata/grapher" import { PROMINENT_LINK_CLASSNAME } from "../site/blocks/ProminentLink.js" import { DataToken } from "../site/DataToken.js" import { DEEP_LINK_CLASS, formatImages } from "./formatting.js" @@ -18,17 +16,18 @@ import { renderAdditionalInformation } from "../site/blocks/renderAdditionalInfo import { renderHelp } from "../site/blocks/renderHelp.js" import { renderCodeSnippets } from "@ourworldindata/components" import { formatUrls, getBodyHtml } from "../site/formatting.js" -import { GRAPHER_PREVIEW_CLASS } from "../site/SiteConstants.js" -import { INTERACTIVE_ICON_SVG } from "../site/InteractionNotice.js" import { renderProminentLinks } from "./siteRenderers.js" import { RELATED_CHARTS_CLASS_NAME } from "../site/blocks/RelatedCharts.js" import { KnexReadonlyTransaction } from "../db/db.js" +import { + EXPLORER_DYNAMIC_THUMBNAIL_URL, + GRAPHER_DYNAMIC_THUMBNAIL_URL, +} from "../settings/clientSettings.js" export const formatWordpressPost = async ( post: FullPost, formattingOptions: FormattingOptions, - knex: KnexReadonlyTransaction, - grapherExports?: GrapherExports + knex: KnexReadonlyTransaction ): Promise => { let html = post.content @@ -149,53 +148,51 @@ export const formatWordpressPost = async ( // Replace URLs pointing to Explorer redirect URLs with the destination URLs replaceIframesWithExplorerRedirectsInWordPressPost(cheerioEl) - // Replace grapher iframes with static previews - if (grapherExports) { - const grapherIframes = cheerioEl("iframe") - .toArray() - .filter((el) => (el.attribs["src"] || "").match(/\/grapher\//)) - for (const el of grapherIframes) { - const $el = cheerioEl(el) - const src = el.attribs["src"].trim() - const chart = grapherExports.get(src) - if (chart) { - const output = ` + const grapherIframes = cheerioEl("iframe") + .toArray() + .filter((el) => (el.attribs["src"] || "").match(/\/grapher\//)) + + for (const el of grapherIframes) { + const $el = cheerioEl(el) + const src = el.attribs["src"].trim() + const url = Url.fromURL(src) + const output = `
-
-
- ${INTERACTIVE_ICON_SVG} - Click to open interactive version -
-
+
+ +
` - if (el.parent.tagName === "p") { - // We are about to replace

-->

- const $p = $el.parent() - $p.after(output) - $el.remove() - } else if (el.parent.tagName === "figure") { - // Support for -->
- const $figure = $el.parent() - $figure.after(output) - $figure.remove() - } else { - // No lifting up otherwise, just replacing -->
- $el.after(output) - $el.remove() - } - } + if (el.parent.tagName === "p") { + // We are about to replace

-->

+ const $p = $el.parent() + $p.after(output) + $el.remove() + } else if (el.parent.tagName === "figure") { + // Support for -->
+ const $figure = $el.parent() + $figure.after(output) + $figure.remove() + } else { + // No lifting up otherwise, just replacing -->
+ $el.after(output) + $el.remove() } } @@ -208,13 +205,16 @@ export const formatWordpressPost = async ( for (const el of explorerIframes) { const $el = cheerioEl(el) const src = el.attribs["src"].trim() + const url = Url.fromURL(src) // set a default style if none exists on the existing iframe const style = el.attribs["style"] || "width: 100%; height: 600px;" const cssClass = el.attribs["class"] const $figure = cheerioEl( ReactDOMServer.renderToStaticMarkup(
- +
) ) @@ -369,8 +369,7 @@ export const formatWordpressPost = async ( export const formatPost = async ( post: FullPost, formattingOptions: FormattingOptions, - knex: KnexReadonlyTransaction, - grapherExports?: GrapherExports + knex: KnexReadonlyTransaction ): Promise => { // No formatting applied, plain source HTML returned if (formattingOptions.raw) @@ -389,5 +388,5 @@ export const formatPost = async ( ...formattingOptions, } - return formatWordpressPost(post, options, knex, grapherExports) + return formatWordpressPost(post, options, knex) } diff --git a/baker/redirects.ts b/baker/redirects.ts index 74e1cadba56..2037368b598 100644 --- a/baker/redirects.ts +++ b/baker/redirects.ts @@ -66,7 +66,7 @@ export const getRedirects = async (knex: db.KnexReadonlyTransaction) => { // Automatic static grapher exports for every grapher chart // Example: https://assets.ourworldindata.org/grapher/exports/absolute-change-co2.svg - "/grapher/exports/* https://assets.ourworldindata.org/grapher/exports/:splat 301", + "/grapher/exports/* https://ourworldindata.org/grapher/:splat 301", ] // TODO: Fix this transaction locking up the DB for too long. diff --git a/baker/runBakeGraphers.ts b/baker/runBakeGraphers.ts index 02f862ea2e9..350d055c647 100755 --- a/baker/runBakeGraphers.ts +++ b/baker/runBakeGraphers.ts @@ -1,5 +1,5 @@ #! /usr/bin/env node -import { bakeAllChangedGrapherPagesVariablesPngSvgAndDeleteRemovedGraphers } from "./GrapherBaker.js" +import { bakeAllChangedGrapherPagesAndDeleteRemovedGraphers } from "./GrapherBaker.js" import * as db from "../db/db.js" /** @@ -11,10 +11,7 @@ import * as db from "../db/db.js" const main = async (folder: string) => { return db.knexReadonlyTransaction( (trx) => - bakeAllChangedGrapherPagesVariablesPngSvgAndDeleteRemovedGraphers( - folder, - trx - ), + bakeAllChangedGrapherPagesAndDeleteRemovedGraphers(folder, trx), db.TransactionCloseMode.Close ) } diff --git a/baker/siteRenderers.tsx b/baker/siteRenderers.tsx index 1557d41ce98..9fe5c4322fd 100644 --- a/baker/siteRenderers.tsx +++ b/baker/siteRenderers.tsx @@ -13,11 +13,6 @@ import OwidGdocPage from "../site/gdocs/OwidGdocPage.js" import ReactDOMServer from "react-dom/server.js" import lodash from "lodash" import { formatCountryProfile, isCanonicalInternalUrl } from "./formatting.js" -import { - bakeGrapherUrls, - getGrapherExportsByUrl, - GrapherExports, -} from "../baker/GrapherBakingUtils.js" import cheerio from "cheerio" import { BAKED_BASE_URL, @@ -27,8 +22,8 @@ import { import { ADMIN_BASE_URL, BAKED_GRAPHER_URL, - BAKED_GRAPHER_EXPORTS_BASE_URL, RECAPTCHA_SITE_KEY, + GRAPHER_DYNAMIC_THUMBNAIL_URL, } from "../settings/clientSettings.js" import { FeedbackPage } from "../site/FeedbackPage.js" import { @@ -200,32 +195,12 @@ export const renderPreview = async ( export const renderPost = async ( post: FullPost, knex: KnexReadonlyTransaction, - baseUrl: string = BAKED_BASE_URL, - grapherExports?: GrapherExports + baseUrl: string = BAKED_BASE_URL ) => { - if (!grapherExports) { - const $ = cheerio.load(post.content) - - const grapherUrls = $("iframe") - .toArray() - .filter((el) => (el.attribs["src"] || "").match(/\/grapher\//)) - .map((el) => el.attribs["src"].trim()) - - // This can be slow if uncached! - await bakeGrapherUrls(knex, grapherUrls) - - grapherExports = await getGrapherExportsByUrl() - } - // Extract formatting options from post HTML comment (if any) const formattingOptions = extractFormattingOptions(post.content) - const formatted = await formatPost( - post, - formattingOptions, - knex, - grapherExports - ) + const formatted = await formatPost(post, formattingOptions, knex) const pageOverrides = await getPageOverrides(knex, post, formattingOptions) const citationStatus = @@ -470,8 +445,7 @@ export const feedbackPage = () => const getCountryProfilePost = memoize( async ( profileSpec: CountryProfileSpec, - knex: KnexReadonlyTransaction, - grapherExports?: GrapherExports + knex: KnexReadonlyTransaction ): Promise<[FormattedPost, FormattingOptions]> => { // Get formatted content from generic covid country profile page. const genericCountryProfilePost = await getFullPostBySlugFromSnapshot( @@ -485,8 +459,7 @@ const getCountryProfilePost = memoize( const formattedPost = await formatPost( genericCountryProfilePost, profileFormattingOptions, - knex, - grapherExports + knex ) return [formattedPost, profileFormattingOptions] @@ -503,13 +476,11 @@ const getCountryProfileLandingPost = memoize( export const renderCountryProfile = async ( profileSpec: CountryProfileSpec, country: Country, - knex: KnexReadonlyTransaction, - grapherExports?: GrapherExports + knex: KnexReadonlyTransaction ) => { const [formatted, formattingOptions] = await getCountryProfilePost( profileSpec, - knex, - grapherExports + knex ) const formattedCountryProfile = formatCountryProfile(formatted, country) @@ -545,7 +516,6 @@ export const countryProfileCountryPage = async ( const country = getCountryBySlug(countrySlug) if (!country) throw new JsonError(`No such country ${countrySlug}`, 404) - // Voluntarily not dealing with grapherExports on devServer for simplicity return renderCountryProfile(profileSpec, country, knex) } @@ -859,7 +829,7 @@ const getExplorerTitleByUrl = async ( const renderGrapherThumbnailByResolvedChartSlug = ( chartSlug: string ): string | null => { - return `` + return `` } const renderExplorerDefaultThumbnail = (): string => { diff --git a/db/model/Chart.ts b/db/model/Chart.ts index 7a74b18698d..29d10b87b64 100644 --- a/db/model/Chart.ts +++ b/db/model/Chart.ts @@ -14,7 +14,6 @@ import { import { GrapherInterface, RelatedChart, - DbPlainPostLink, DbPlainChart, parseChartConfig, ChartRedirect, @@ -24,10 +23,7 @@ import { GrapherChartType, } from "@ourworldindata/types" import { OpenAI } from "openai" -import { - BAKED_BASE_URL, - OPENAI_API_KEY, -} from "../../settings/serverSettings.js" +import { OPENAI_API_KEY } from "../../settings/serverSettings.js" // XXX hardcoded filtering to public parent tags export const PUBLIC_TAG_PARENT_IDS = [ @@ -607,59 +603,6 @@ export const getRelatedChartsForVariable = async ( ) } -export const getChartEmbedUrlsInPublishedWordpressPosts = async ( - knex: db.KnexReadonlyTransaction -): Promise => { - const chartSlugQueryString: Pick< - DbPlainPostLink, - "target" | "queryString" - >[] = await db.knexRaw( - knex, - `-- sql - SELECT - pl.target, - pl.queryString - FROM - posts_links pl - JOIN posts p ON p.id = pl.sourceId - WHERE - pl.linkType = "grapher" - AND pl.componentType = "src" - AND p.status = "publish" - AND p.type != 'wp_block' - AND p.slug NOT IN ( - -- We want to exclude the slugs of published gdocs, since they override the Wordpress posts - -- published under the same slugs. - SELECT - slug from posts_gdocs pg - WHERE - pg.slug = p.slug - AND pg.content ->> '$.type' <> 'fragment' - AND pg.published = 1 - ) - -- Commenting this out since we currently don't do anything with the baked embeds in gdocs posts - -- see https://github.com/owid/owid-grapher/issues/2992#issuecomment-1934690219 - -- Rename to getChartEmbedUrlsInPublishedPosts if we decide to use this - -- UNION - -- SELECT - -- pgl.target, - -- pgl.queryString - -- FROM - -- posts_gdocs_links pgl - -- JOIN posts_gdocs pg on pg.id = pgl.sourceId - -- WHERE - -- pgl.linkType = "grapher" - -- AND pgl.componentType = "chart" - -- AND pg.content ->> '$.type' <> 'fragment' - -- AND pg.published = 1 - ` - ) - - return chartSlugQueryString.map((row) => { - return `${BAKED_BASE_URL}/${row.target}${row.queryString}` - }) -} - export const getRedirectsByChartId = async ( knex: db.KnexReadonlyTransaction, chartId: number diff --git a/db/model/Gdoc/GdocBase.ts b/db/model/Gdoc/GdocBase.ts index 9fc97412934..37782298301 100644 --- a/db/model/Gdoc/GdocBase.ts +++ b/db/model/Gdoc/GdocBase.ts @@ -32,7 +32,7 @@ import { archieToEnriched } from "./archieToEnriched.js" import { getChartConfigById, mapSlugsToIds } from "../Chart.js" import { BAKED_BASE_URL, - BAKED_GRAPHER_EXPORTS_BASE_URL, + GRAPHER_DYNAMIC_THUMBNAIL_URL, } from "../../../settings/clientSettings.js" import { EXPLORERS_ROUTE_FOLDER } from "@ourworldindata/explorer" import { match, P } from "ts-pattern" @@ -993,7 +993,7 @@ export async function makeGrapherLinkedChart( title: resolvedTitle, tab, resolvedUrl, - thumbnail: `${BAKED_GRAPHER_EXPORTS_BASE_URL}/${resolvedSlug}.svg`, + thumbnail: `${GRAPHER_DYNAMIC_THUMBNAIL_URL}/${resolvedSlug}.png`, tags: [], indicatorId: datapageIndicator?.id, } diff --git a/functions/_common/grapherTools.ts b/functions/_common/grapherTools.ts index 51bedac65db..905e4ed4d04 100644 --- a/functions/_common/grapherTools.ts +++ b/functions/_common/grapherTools.ts @@ -1,4 +1,4 @@ -import { Grapher } from "@ourworldindata/grapher" +import { generateGrapherImageSrcSet, Grapher } from "@ourworldindata/grapher" import { GrapherInterface, R2GrapherConfigDirectory, @@ -165,7 +165,24 @@ export function rewriteMetaTags( // If we fail to capture the origin, we end up with relative image URLs, which should also be okay. let origin = "" + const thumbnailUrl = `${url.pathname}.png${url.search}` + const rewriter = new HTMLRewriter() + .on("picture[data-owid-populate-url-params] source", { + element: (source) => { + if (thumbnailUrl) { + const srcSet = generateGrapherImageSrcSet(thumbnailUrl) + source.setAttribute("srcset", srcSet) + } + }, + }) + .on("picture[data-owid-populate-url-params] img", { + element: (img) => { + if (thumbnailUrl) { + img.setAttribute("src", thumbnailUrl) + } + }, + }) .on('meta[property="og:url"]', { // Replace canonical URL, otherwise the preview image will not include the search parameters. element: (element) => { diff --git a/ops/buildkite/build-code b/ops/buildkite/build-code index b6de1359f0f..b41d3dc7c88 100755 --- a/ops/buildkite/build-code +++ b/ops/buildkite/build-code @@ -55,13 +55,6 @@ update_env() { else sed -i "s|^BAKED_SITE_DIR=.*$|BAKED_SITE_DIR=/home/owid/live-data/bakedSite|" owid-grapher/.env fi - # redirect assets - if ! grep -q "^BAKED_GRAPHER_EXPORTS_BASE_URL=" owid-grapher/.env; then - echo "BAKED_GRAPHER_EXPORTS_BASE_URL=https://assets.ourworldindata.org/grapher/exports" >> owid-grapher/.env - fi - if ! grep -q "^BAKED_SITE_EXPORTS_BASE_URL=" owid-grapher/.env; then - echo "BAKED_SITE_EXPORTS_BASE_URL=https://assets.ourworldindata.org/exports" >> owid-grapher/.env - fi } diff --git a/ops/buildkite/deploy-content b/ops/buildkite/deploy-content index 5bcb26ef38c..609a1c64abc 100755 --- a/ops/buildkite/deploy-content +++ b/ops/buildkite/deploy-content @@ -98,6 +98,7 @@ deploy_to_cloudflare() { create_dist() { echo '--- Creating dist/ folder' # Define a list of excluded directories for rsync + # grapher/exports/ should be removed once #4464 is merged and we've deleted the static exports folder EXCLUDES=(grapher/data/variables/ .git/ grapher/exports/ exports/ uploads/ assets-admin/) # Build rsync command with excluded directories diff --git a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx index 0a0e7a0ad1f..0fc08469d77 100644 --- a/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx +++ b/packages/@ourworldindata/grapher/src/chart/ChartUtils.tsx @@ -1,5 +1,5 @@ import * as React from "react" -import { areSetsEqual, Box, getCountryByName } from "@ourworldindata/utils" +import { areSetsEqual, Box, getCountryByName, Url } from "@ourworldindata/utils" import { SeriesStrategy, EntityName, @@ -260,3 +260,21 @@ export function makeAxisLabel({ return { mainLabel: label } } + +/** + * Given a URL for a CF function grapher thumbnail, generate a srcSet for the image at different widths + * @param defaultSrc - `https://ourworldindata.org/grapher/thumbnail/life-expectancy.png?tab=chart` + * @returns srcSet - `https://ourworldindata.org/grapher/thumbnail/life-expectancy.png?tab=chart&imWidth=850 850w, https://ourworldindata.org/grapher/thumbnail/life-expectancy.png?tab=chart&imWidth=1700 1700w` + */ +export function generateGrapherImageSrcSet(defaultSrc: string): string { + const url = Url.fromURL(defaultSrc) + const existingQueryParams = url.queryParams + const imWidths = ["850", "1700"] + const srcSet = imWidths + .map((imWidth) => { + return `${url.setQueryParams({ ...existingQueryParams, imWidth }).fullUrl} ${imWidth}w` + }) + .join(", ") + + return srcSet +} diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index ed5a9b2c2bc..05b5453daae 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -2433,7 +2433,7 @@ export class Grapher container ) } catch (err) { - container.innerHTML = `

Unable to load interactive visualization

` + container.innerHTML = `

Unable to load interactive visualization

` container.setAttribute("id", "fallback") throw err } diff --git a/packages/@ourworldindata/grapher/src/index.ts b/packages/@ourworldindata/grapher/src/index.ts index 13ca26a69cf..af37b72a2e9 100644 --- a/packages/@ourworldindata/grapher/src/index.ts +++ b/packages/@ourworldindata/grapher/src/index.ts @@ -87,3 +87,4 @@ export { } from "./slideshowController/SlideShowController" export { defaultGrapherConfig } from "./schema/defaultGrapherConfig" export { migrateGrapherConfigToLatestVersion } from "./schema/migrations/migrate" +export { generateGrapherImageSrcSet } from "./chart/ChartUtils.js" diff --git a/packages/@ourworldindata/types/src/index.ts b/packages/@ourworldindata/types/src/index.ts index 19bf8e80fad..6ce5f408ed0 100644 --- a/packages/@ourworldindata/types/src/index.ts +++ b/packages/@ourworldindata/types/src/index.ts @@ -745,3 +745,9 @@ export type { View, ViewEnriched, } from "./siteTypes/MultiDimDataPage.js" + +export { + GRAPHER_PREVIEW_CLASS, + HIDE_IF_JS_DISABLED_CLASSNAME, + HIDE_IF_JS_ENABLED_CLASSNAME, +} from "./siteTypes/SiteConstants.js" diff --git a/packages/@ourworldindata/types/src/siteTypes/SiteConstants.ts b/packages/@ourworldindata/types/src/siteTypes/SiteConstants.ts new file mode 100644 index 00000000000..90afa62c394 --- /dev/null +++ b/packages/@ourworldindata/types/src/siteTypes/SiteConstants.ts @@ -0,0 +1,7 @@ +// Used in GrapherPage.tsx +export const HIDE_IF_JS_ENABLED_CLASSNAME = "js--hide-if-js-enabled" + +export const HIDE_IF_JS_DISABLED_CLASSNAME = "js--hide-if-js-disabled" + +// Used in GrapherWithFallback.tsx +export const GRAPHER_PREVIEW_CLASS = "grapherPreview" diff --git a/settings/clientSettings.ts b/settings/clientSettings.ts index 94082fce24a..4ebb372b673 100644 --- a/settings/clientSettings.ts +++ b/settings/clientSettings.ts @@ -32,10 +32,6 @@ export const BAKED_BASE_URL: string = export const BAKED_GRAPHER_URL: string = process.env.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL}/grapher` -export const BAKED_GRAPHER_EXPORTS_BASE_URL: string = - process.env.BAKED_GRAPHER_EXPORTS_BASE_URL ?? `${BAKED_GRAPHER_URL}/exports` -export const BAKED_SITE_EXPORTS_BASE_URL: string = - process.env.BAKED_SITE_EXPORTS_BASE_URL ?? `${BAKED_BASE_URL}/exports` export const GRAPHER_DYNAMIC_THUMBNAIL_URL: string = process.env.GRAPHER_DYNAMIC_THUMBNAIL_URL ?? `${BAKED_GRAPHER_URL}` diff --git a/settings/serverSettings.ts b/settings/serverSettings.ts index bd61898dddd..3513df2dcc9 100644 --- a/settings/serverSettings.ts +++ b/settings/serverSettings.ts @@ -34,9 +34,6 @@ export const ADMIN_BASE_URL: string = clientSettings.ADMIN_BASE_URL export const BAKED_GRAPHER_URL: string = serverSettings.BAKED_GRAPHER_URL ?? `${BAKED_BASE_URL}/grapher` -export const OPTIMIZE_SVG_EXPORTS: boolean = - serverSettings.OPTIMIZE_SVG_EXPORTS === "true" - export const GITHUB_USERNAME: string = serverSettings.GITHUB_USERNAME ?? "owid-test" export const GIT_DEFAULT_USERNAME: string = diff --git a/site/DataPageV2Content.tsx b/site/DataPageV2Content.tsx index a856513207b..7be34995e79 100644 --- a/site/DataPageV2Content.tsx +++ b/site/DataPageV2Content.tsx @@ -100,6 +100,7 @@ export const DataPageV2Content = ({
@@ -139,6 +140,7 @@ export const DataPageV2Content = ({ // non-grapher pages then we need to make sure that there are thunbnails that are generated for the these non-chart graphers and // then this piece will have to change anyhow and know how to provide the thumbnail. id="explore-the-data" + enablePopulatingUrlParams /> +} + +export default function GrapherImage(props: { + url: string + alt?: string + noFormatting?: boolean + enablePopulatingUrlParams?: boolean +}): JSX.Element +export default function GrapherImage(props: { slug: string + queryString?: string + alt?: string + noFormatting?: boolean + enablePopulatingUrlParams?: boolean +}): JSX.Element +export default function GrapherImage(props: { + url?: string + slug?: string + queryString?: string alt?: string noFormatting?: boolean + enablePopulatingUrlParams?: boolean }) { + let slug: string = "" + let queryString: string = "" + if (props.url) { + const url = Url.fromURL(props.url) + slug = url.slug! + queryString = url.queryStr + } else { + slug = props.slug! + queryString = props.queryString ?? "" + } + + const defaultSrc = `${GRAPHER_DYNAMIC_THUMBNAIL_URL}/${slug}.png${queryString}` return ( - {alt} + + + {props.alt} + ) } diff --git a/site/GrapherPage.tsx b/site/GrapherPage.tsx index c5c3929fc2e..1cca2a5d507 100644 --- a/site/GrapherPage.tsx +++ b/site/GrapherPage.tsx @@ -14,6 +14,10 @@ import { Url, } from "@ourworldindata/utils" import { MarkdownTextWrap } from "@ourworldindata/components" +import { + HIDE_IF_JS_DISABLED_CLASSNAME, + HIDE_IF_JS_ENABLED_CLASSNAME, +} from "@ourworldindata/types" import urljoin from "url-join" import { ADMIN_BASE_URL, @@ -112,16 +116,17 @@ window.Grapher.renderSingleGrapherOnGrapherPage(jsonConfig)`
-
+
{grapher.slug && ( )}

Interactive visualization requires JavaScript

diff --git a/site/GrapherWithFallback.tsx b/site/GrapherWithFallback.tsx index fc0bc1d9c99..b307aaf7bf4 100644 --- a/site/GrapherWithFallback.tsx +++ b/site/GrapherWithFallback.tsx @@ -1,7 +1,7 @@ import { Grapher } from "@ourworldindata/grapher" +import { GRAPHER_PREVIEW_CLASS } from "@ourworldindata/types" import { GrapherFigureView } from "./GrapherFigureView.js" import cx from "classnames" -import { GRAPHER_PREVIEW_CLASS } from "./SiteConstants.js" import GrapherImage from "./GrapherImage.js" export const GrapherWithFallback = ({ @@ -9,11 +9,13 @@ export const GrapherWithFallback = ({ slug, className, id, + enablePopulatingUrlParams = false, }: { grapher?: Grapher | undefined slug?: string className?: string id?: string + enablePopulatingUrlParams?: boolean }) => { return (
- {slug && } + {slug && ( + + )} )} diff --git a/site/InteractionNotice.tsx b/site/InteractionNotice.tsx deleted file mode 100644 index 3ffbad64611..00000000000 --- a/site/InteractionNotice.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export const INTERACTIVE_ICON_SVG = `` - -export default function InteractionNotice() { - return ( -
- - Click to open interactive version -
- ) -} diff --git a/site/SiteConstants.ts b/site/SiteConstants.ts index 98ae960491e..7751f158faa 100644 --- a/site/SiteConstants.ts +++ b/site/SiteConstants.ts @@ -24,8 +24,6 @@ export const POLYFILL_URL: string = `https://cdnjs.cloudflare.com/polyfill/v3/po export const DEFAULT_LOCAL_BAKE_DIR = "localBake" -export const GRAPHER_PREVIEW_CLASS = "grapherPreview" - export const SMALL_BREAKPOINT_MEDIA_QUERY = "(max-width: 768px)" export const TOUCH_DEVICE_MEDIA_QUERY = diff --git a/site/blocks/RelatedCharts.jsdom.test.tsx b/site/blocks/RelatedCharts.jsdom.test.tsx index d99454b4807..b53506376d0 100755 --- a/site/blocks/RelatedCharts.jsdom.test.tsx +++ b/site/blocks/RelatedCharts.jsdom.test.tsx @@ -4,7 +4,7 @@ import Enzyme from "enzyme" import Adapter from "@wojtekmaj/enzyme-adapter-react-17" import { BAKED_BASE_URL, - BAKED_GRAPHER_EXPORTS_BASE_URL, + GRAPHER_DYNAMIC_THUMBNAIL_URL, } from "../../settings/clientSettings.js" import { KeyChartLevel } from "@ourworldindata/utils" import { RelatedCharts } from "./RelatedCharts.js" @@ -35,7 +35,7 @@ it("renders active chart links and loads respective chart on click", () => { .find("li") .first() .find( - `img[src="${BAKED_GRAPHER_EXPORTS_BASE_URL}/${charts[1].slug}.svg"]` + `img[src="${GRAPHER_DYNAMIC_THUMBNAIL_URL}/${charts[1].slug}.png"]` ) ).toHaveLength(1) diff --git a/site/blocks/RelatedCharts.tsx b/site/blocks/RelatedCharts.tsx index 8a0316c10ea..ff92a091f3f 100644 --- a/site/blocks/RelatedCharts.tsx +++ b/site/blocks/RelatedCharts.tsx @@ -1,12 +1,14 @@ import { useState, useRef } from "react" import * as React from "react" -import { orderBy, RelatedChart } from "@ourworldindata/utils" +import { + orderBy, + RelatedChart, + HIDE_IF_JS_ENABLED_CLASSNAME, +} from "@ourworldindata/utils" +import { GRAPHER_PREVIEW_CLASS } from "@ourworldindata/types" import { useEmbedChart } from "../hooks.js" import { GalleryArrow } from "./GalleryArrow.js" -import { - GalleryArrowDirection, - GRAPHER_PREVIEW_CLASS, -} from "../SiteConstants.js" +import { GalleryArrowDirection } from "../SiteConstants.js" import { AllChartsListItem } from "./AllChartsListItem.js" import { BAKED_BASE_URL } from "../../settings/clientSettings.js" import GrapherImage from "../GrapherImage.js" @@ -57,7 +59,7 @@ export const RelatedCharts = ({ key={activeChartSlug} data-grapher-src={grapherUrl} > -
+
{ rel="noopener" > - ) diff --git a/site/collections/StaticCollectionPage.tsx b/site/collections/StaticCollectionPage.tsx index 6556a419934..bd988154662 100644 --- a/site/collections/StaticCollectionPage.tsx +++ b/site/collections/StaticCollectionPage.tsx @@ -1,9 +1,8 @@ import cx from "classnames" import { Head } from "../Head.js" -import { GRAPHER_PREVIEW_CLASS } from "../SiteConstants.js" +import { GRAPHER_PREVIEW_CLASS } from "@ourworldindata/types" import { SiteHeader } from "../SiteHeader.js" import { SiteFooter } from "../SiteFooter.js" -import InteractionNotice from "../InteractionNotice.js" import GrapherImage from "../GrapherImage.js" import { Html } from "../Html.js" @@ -56,7 +55,6 @@ export const StaticCollectionPage = ( rel="noopener" > - ) diff --git a/site/css/content.scss b/site/css/content.scss index 56c34653c04..3c65870aeb4 100644 --- a/site/css/content.scss +++ b/site/css/content.scss @@ -63,30 +63,6 @@ figure[data-explorer-src] { position: relative; } -.interactionNotice { - font-size: 14px; - font-weight: 400; - line-height: 1.6; - display: none; - background-color: rgba(black, 0.03); - padding: 3px 0; - color: $controls-color; - justify-content: center; - align-items: center; - flex-wrap: wrap; - gap: 8px; - - // Anything >680px will get the interactive graphics in-place. - // 680px is a breakpoint also used in the JavaScript code – keep it in sync. - @media screen and (max-device-width: 680px) { - display: flex; - } - - .icon { - font-size: 21px; - } -} - /******************************************************************************* * Tables */ diff --git a/site/formatting.test.ts b/site/formatting.test.ts index ae7e7e50b90..0c9188a7d0c 100644 --- a/site/formatting.test.ts +++ b/site/formatting.test.ts @@ -2,7 +2,7 @@ import cheerio from "cheerio" import { WP_ColumnStyle } from "@ourworldindata/utils" import { splitContentIntoSectionsAndColumns } from "./formatting.js" import { formatAuthors } from "./clientFormatting.js" -import { GRAPHER_PREVIEW_CLASS } from "./SiteConstants.js" +import { GRAPHER_PREVIEW_CLASS } from "@ourworldindata/types" const paragraph = `

Some paragraph

` const chart = `
` diff --git a/site/formatting.tsx b/site/formatting.tsx index 7c10b9856de..214b6413321 100644 --- a/site/formatting.tsx +++ b/site/formatting.tsx @@ -13,8 +13,7 @@ import { bakeGlobalEntitySelector } from "./bakeGlobalEntitySelector.js" import { PROMINENT_LINK_CLASSNAME } from "./blocks/ProminentLink.js" import { Byline } from "./Byline.js" import { SectionHeading } from "./SectionHeading.js" -import { FormattingOptions } from "@ourworldindata/types" -import { GRAPHER_PREVIEW_CLASS } from "./SiteConstants.js" +import { FormattingOptions, GRAPHER_PREVIEW_CLASS } from "@ourworldindata/types" export const RESEARCH_AND_WRITING_CLASSNAME = "wp-block-research-and-writing" diff --git a/site/gdocs/components/Chart.tsx b/site/gdocs/components/Chart.tsx index 6e9a304781d..5e85e97bd51 100644 --- a/site/gdocs/components/Chart.tsx +++ b/site/gdocs/components/Chart.tsx @@ -14,12 +14,10 @@ import { excludeUndefined, isEmpty, } from "@ourworldindata/utils" -import { ChartConfigType } from "@ourworldindata/types" +import { ChartConfigType, GRAPHER_PREVIEW_CLASS } from "@ourworldindata/types" import { useLinkedChart } from "../utils.js" import SpanElements from "./SpanElements.js" import cx from "classnames" -import { GRAPHER_PREVIEW_CLASS } from "../../SiteConstants.js" -import InteractionNotice from "../../InteractionNotice.js" import GrapherImage from "../../GrapherImage.js" export default function Chart({ @@ -44,7 +42,6 @@ export default function Chart({ const url = Url.fromURL(d.url) const resolvedUrl = linkedChart.resolvedUrl - const resolvedSlug = Url.fromURL(resolvedUrl).slug const isExplorer = linkedChart.configType === ChartConfigType.Explorer const isMultiDim = linkedChart.configType === ChartConfigType.MultiDim const hasControls = url.queryParams.hideControls !== "true" @@ -132,12 +129,9 @@ export default function Chart({ {isExplorer || isMultiDim ? (
) : ( - resolvedSlug && ( - - - - - ) + + + )} {d.caption ? ( diff --git a/site/gdocs/components/NarrativeChart.tsx b/site/gdocs/components/NarrativeChart.tsx index d2e5336d9b6..25242c8d0cb 100644 --- a/site/gdocs/components/NarrativeChart.tsx +++ b/site/gdocs/components/NarrativeChart.tsx @@ -1,9 +1,11 @@ import { useContext, useRef } from "react" import { useEmbedChart } from "../../hooks.js" -import { EnrichedBlockNarrativeChart } from "@ourworldindata/types" +import { + EnrichedBlockNarrativeChart, + GRAPHER_PREVIEW_CLASS, +} from "@ourworldindata/types" import { useLinkedChartView } from "../utils.js" import cx from "classnames" -import { GRAPHER_PREVIEW_CLASS } from "../../SiteConstants.js" import { BlockErrorFallback } from "./BlockErrorBoundary.js" import SpanElements from "./SpanElements.js" import { DocumentContext } from "../DocumentContext.js" @@ -17,7 +19,6 @@ import { GRAPHER_DYNAMIC_THUMBNAIL_URL, } from "../../../settings/clientSettings.js" import { queryParamsToStr } from "@ourworldindata/utils" -import InteractionNotice from "../../InteractionNotice.js" export default function NarrativeChart({ d, @@ -85,7 +86,6 @@ export default function NarrativeChart({ loading="lazy" data-no-lightbox /> - {d.caption ? ( diff --git a/site/gdocs/components/ProminentLink.tsx b/site/gdocs/components/ProminentLink.tsx index 5870d816789..ff949f2b113 100644 --- a/site/gdocs/components/ProminentLink.tsx +++ b/site/gdocs/components/ProminentLink.tsx @@ -8,7 +8,7 @@ import Image from "./Image.js" import { useLinkedChart, useLinkedDocument } from "../utils.js" import { DocumentContext } from "../DocumentContext.js" import { BlockErrorFallback } from "./BlockErrorBoundary.js" -import { BAKED_GRAPHER_EXPORTS_BASE_URL } from "../../../settings/clientSettings.js" +import { GRAPHER_DYNAMIC_THUMBNAIL_URL } from "../../../settings/clientSettings.js" import { ARCHVED_THUMBNAIL_FILENAME, DEFAULT_THUMBNAIL_FILENAME, @@ -16,7 +16,7 @@ import { const Thumbnail = ({ thumbnail }: { thumbnail: string }) => { if ( - thumbnail.startsWith(BAKED_GRAPHER_EXPORTS_BASE_URL) || + thumbnail.startsWith(GRAPHER_DYNAMIC_THUMBNAIL_URL) || thumbnail.endsWith(ARCHVED_THUMBNAIL_FILENAME) || thumbnail.endsWith(DEFAULT_THUMBNAIL_FILENAME) ) { diff --git a/site/multiembedder/MultiEmbedder.tsx b/site/multiembedder/MultiEmbedder.tsx index a399ceb7932..d0125fa9e85 100644 --- a/site/multiembedder/MultiEmbedder.tsx +++ b/site/multiembedder/MultiEmbedder.tsx @@ -33,7 +33,7 @@ import { buildExplorerProps, isEmpty, } from "@ourworldindata/explorer" -import { GRAPHER_PREVIEW_CLASS } from "../SiteConstants.js" +import { GRAPHER_PREVIEW_CLASS } from "@ourworldindata/types" import { ADMIN_BASE_URL, BAKED_GRAPHER_URL, @@ -54,14 +54,6 @@ const figuresFromDOM = ( container.querySelectorAll(`*[${selector}]`) ).filter(isPresent) -// Determine whether this device is powerful enough to handle -// loading a bunch of inline interactive charts -// 680px is also used in CSS – keep it in sync if you change this -const shouldProgressiveEmbed = () => true -// disabling this behaviour for now until we have a better way to detect low power devices -// https://github.com/owid/owid-grapher/issues/3661 -// !isMobile() || window.screen.width > 680 || pageContainsGlobalEntitySelector() - // const pageContainsGlobalEntitySelector = () => // globalEntitySelectorElement() !== null @@ -307,14 +299,7 @@ class MultiEmbedder { ? "chartView" : "grapher" - const hasPreview = isExplorer ? false : !!figure.querySelector("img") - if (!shouldProgressiveEmbed() && hasPreview) return - - // Stop observing visibility as soon as possible, that is not before - // shouldProgressiveEmbed gets a chance to reevaluate a possible change - // in screen size on mobile (i.e. after a rotation). Stopping before - // shouldProgressiveEmbed would prevent rendering interactive charts - // when going from portrait to landscape mode (without page reload). + // Stop observing visibility as soon as possible this.figuresObserver?.unobserve(figure) await match(embedType) diff --git a/site/search/ChartHit.tsx b/site/search/ChartHit.tsx index 3008fc9807c..ba1108721ee 100644 --- a/site/search/ChartHit.tsx +++ b/site/search/ChartHit.tsx @@ -6,7 +6,6 @@ import { getEntityQueryStr, pickEntitiesForChartHit } from "./SearchUtils.js" import { HitAttributeHighlightResult } from "instantsearch.js" import { BAKED_BASE_URL, - BAKED_GRAPHER_EXPORTS_BASE_URL, BAKED_GRAPHER_URL, EXPLORER_DYNAMIC_THUMBNAIL_URL, GRAPHER_DYNAMIC_THUMBNAIL_URL, @@ -68,15 +67,13 @@ export function ChartHit({ slug: string, fullQueryParams: string ): string { - return `${EXPLORER_DYNAMIC_THUMBNAIL_URL}/${slug}.svg${fullQueryParams}` + return `${EXPLORER_DYNAMIC_THUMBNAIL_URL}/${slug}.png${fullQueryParams}` } function createGrapherThumbnailUrl( slug: string, - fullQueryParams?: string + fullQueryParams = "" ): string { - return fullQueryParams - ? `${GRAPHER_DYNAMIC_THUMBNAIL_URL}/${slug}.svg${fullQueryParams}` - : `${BAKED_GRAPHER_EXPORTS_BASE_URL}/${slug}.svg` + return `${GRAPHER_DYNAMIC_THUMBNAIL_URL}/${slug}.png${fullQueryParams}` } const previewUrl = isExplorerView ? createExplorerViewThumbnailUrl(hit.slug, fullQueryParams)