From 47c6be3c3ac0fabd342dae0080e1c325e9bc1f9e Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Tue, 26 Nov 2024 13:56:20 +0100 Subject: [PATCH 1/4] feat(functions): add `social-media-square` image type --- functions/README.md | 23 +++++++++++---------- functions/_common/grapherRenderer.ts | 20 +++++++++++++----- functions/_common/grapherTools.ts | 2 ++ functions/_common/imageOptions.ts | 31 +++++++++++++++++++++++++--- 4 files changed, 57 insertions(+), 19 deletions(-) diff --git a/functions/README.md b/functions/README.md index f5402687d66..7ea1bade3f8 100644 --- a/functions/README.md +++ b/functions/README.md @@ -18,13 +18,13 @@ Inside a file-based route we sometimes use an instance of itty-router to decide 2. Start the Cloudflare function development server with either: -- (preferred) `yarn make up.full`: starts the whole local development stack, including the functions development server -- `yarn startLocalCloudflareFunctions`: only starts the functions development server +- (preferred) `yarn make up.full`: starts the whole local development stack, including the functions development server +- `yarn startLocalCloudflareFunctions`: only starts the functions development server Note: compatibility dates between local development, production and preview environments should be kept in sync: -- local: defined in `package.json` -> `startLocalCloudflareFunctions` -- production & preview : see https://dash.cloudflare.com/078fcdfed9955087315dd86792e71a7e/pages/view/owid/settings/functions +- local: defined in `package.json` -> `startLocalCloudflareFunctions` +- production & preview : see https://dash.cloudflare.com/078fcdfed9955087315dd86792e71a7e/pages/view/owid/settings/functions 3. _Refer to each function's "Development" section below for further instructions._ @@ -147,8 +147,8 @@ In order to test the webhook function locally, you can use the Stripe CLI to lis STRIPE_API_KEY=xxx stripe listen --latest --forward-to localhost:8788/donation/thank-you ``` -- replace `xxx` with the value of `STRIPE_API_KEY (dev)` in 1password. Alternatively, if you have access to the Stripe dashboard, you can forgo the `STRIPE_API_KEY=xxx` part and let `stripe listen ...` guide you through a one-time login process. -- `--latest` is required when development code uses a more recent API version than the one set in the Stripe dashboard (which `stripe listen` will default to). +- replace `xxx` with the value of `STRIPE_API_KEY (dev)` in 1password. Alternatively, if you have access to the Stripe dashboard, you can forgo the `STRIPE_API_KEY=xxx` part and let `stripe listen ...` guide you through a one-time login process. +- `--latest` is required when development code uses a more recent API version than the one set in the Stripe dashboard (which `stripe listen` will default to). 3. Copy the webhook secret into `STRIPE_WEBHOOK_SECRET` variable in your `.dev.vars` and then restart the development server. This secret is shown when you ran `stripe listen`, and is stable across restarts. @@ -181,10 +181,10 @@ This route is where the actual thumbnail magic happens 🙌🏻✨ It can: -- Generate _png_ and _svg_ previews -- Render _png_ exports using our custom fonts, Lato and Playfair -- Render a preview according to all its query parameters -- Customize the image output a bunch using various options (see below) +- Generate _png_ and _svg_ previews +- Render _png_ exports using our custom fonts, Lato and Playfair +- Render a preview according to all its query parameters +- Customize the image output a bunch using various options (see below) We (plan to) use these for social media previews, such that the social media user will see the exact chart that is shared, for example with a `?tab=chart&country=IND~CHN&time=2000`. We cannot possibly create static previews for all possible combinations, but we can generate them dynamically on the fly as they are being shared. @@ -232,13 +232,14 @@ All of the below options can be given as query parameters, e.g. `?imType=og&noca imType twitter or og (short for - Open Graph) + Open Graph) or social-media-square If present, will use fitting defaults for the generated image size: All below options will be ignored if imType is set to one of these values. diff --git a/functions/_common/grapherRenderer.ts b/functions/_common/grapherRenderer.ts index 784c32607e5..7dd979a085a 100644 --- a/functions/_common/grapherRenderer.ts +++ b/functions/_common/grapherRenderer.ts @@ -82,7 +82,8 @@ async function fetchAndRenderGrapherToSvg( const svg = grapher.generateStaticSvg() grapherLogger.log("generateStaticSvg") - return svg + + return { svg, backgroundColor: grapher.backgroundColor } } export const fetchAndRenderGrapher = async ( @@ -94,12 +95,17 @@ export const fetchAndRenderGrapher = async ( const options = extractOptions(searchParams) console.log("Rendering", id.id, outType, options) - const svg = await fetchAndRenderGrapherToSvg(id, options, searchParams, env) + const { svg, backgroundColor } = await fetchAndRenderGrapherToSvg( + id, + options, + searchParams, + env + ) console.log("fetched svg") switch (outType) { case "png": - return png(await renderSvgToPng(svg, options)) + return png(await renderSvgToPng(svg, options, backgroundColor)) case "svg": return new Response(svg, { headers: { @@ -111,7 +117,11 @@ export const fetchAndRenderGrapher = async ( let initialized = false -export async function renderSvgToPng(svg: string, options: ImageOptions) { +export async function renderSvgToPng( + svg: string, + options: ImageOptions, + backgroundColor: string +) { if (!initialized) { await initializeSvg2Png(svg2png_wasm) initialized = true @@ -123,7 +133,7 @@ export async function renderSvgToPng(svg: string, options: ImageOptions) { // if we include details, pngHeight is only the height of the chart, but we also have an "appendix" at the bottom that we want to include height: options.details ? undefined : options.pngHeight, - backgroundColor: "#fff", + backgroundColor, fonts: [LatoRegular, LatoMedium, LatoBold, PlayfairSemiBold].map( (f) => new Uint8Array(f) ), diff --git a/functions/_common/grapherTools.ts b/functions/_common/grapherTools.ts index 990391ce476..51bedac65db 100644 --- a/functions/_common/grapherTools.ts +++ b/functions/_common/grapherTools.ts @@ -144,7 +144,9 @@ export async function initGrapher( bounds, staticBounds: bounds, baseFontSize: options.fontSize, + ...options.grapherProps, }) + grapher.isExportingToSvgOrPng = true grapher.shouldIncludeDetailsInStaticExport = options.details return grapher diff --git a/functions/_common/imageOptions.ts b/functions/_common/imageOptions.ts index 4fab7759338..05c70290840 100644 --- a/functions/_common/imageOptions.ts +++ b/functions/_common/imageOptions.ts @@ -1,3 +1,4 @@ +import { GrapherProgrammaticInterface } from "@ourworldindata/grapher" import { DEFAULT_ASPECT_RATIO, MIN_ASPECT_RATIO, @@ -7,6 +8,7 @@ import { DEFAULT_WIDTH, DEFAULT_HEIGHT, } from "./grapherRenderer.js" +import { GrapherStaticFormat } from "@ourworldindata/types" export interface ImageOptions { pngWidth: number @@ -14,7 +16,8 @@ export interface ImageOptions { svgWidth: number svgHeight: number details: boolean - fontSize: number + fontSize: number | undefined + grapherProps?: Partial } export const TWITTER_OPTIONS: ImageOptions = { // Twitter cards are 1.91:1 in aspect ratio, and 800x418 is the recommended size @@ -34,12 +37,34 @@ const OPEN_GRAPH_OPTIONS: ImageOptions = { details: false, fontSize: 21, } -export const extractOptions = (params: URLSearchParams): ImageOptions => { - const options: Partial = {} +const SOCIAL_MEDIA_SQUARE_OPTIONS: ImageOptions = { + pngWidth: 2160, + pngHeight: 2160, + svgWidth: 540, + svgHeight: 540, + details: false, + fontSize: undefined, + grapherProps: { + isSocialMediaExport: true, + staticFormat: GrapherStaticFormat.square, + }, +} +export const extractOptions = (params: URLSearchParams): ImageOptions => { // We have two special images types specified via the `imType` query param: if (params.get("imType") === "twitter") return TWITTER_OPTIONS else if (params.get("imType") === "og") return OPEN_GRAPH_OPTIONS + else if (params.get("imType") === "social-media-square") { + const squareOptions = SOCIAL_MEDIA_SQUARE_OPTIONS + if (params.has("imSquareSize")) { + const size = parseInt(params.get("imSquareSize")!) + squareOptions.pngWidth = size + squareOptions.pngHeight = size + } + return squareOptions + } + + const options: Partial = {} // Otherwise, query params can specify the size to be rendered at; and in addition we're doing a // bunch of normalization to make sure the image is rendered at a reasonable size and aspect ratio. From 6f7e04d6ef585eaf3b82115eb13e3f399ccd2c48 Mon Sep 17 00:00:00 2001 From: marcelgerber Date: Tue, 26 Nov 2024 12:58:33 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A4=96=20style:=20prettify=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- functions/README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/functions/README.md b/functions/README.md index 7ea1bade3f8..bf039c79701 100644 --- a/functions/README.md +++ b/functions/README.md @@ -18,13 +18,13 @@ Inside a file-based route we sometimes use an instance of itty-router to decide 2. Start the Cloudflare function development server with either: -- (preferred) `yarn make up.full`: starts the whole local development stack, including the functions development server -- `yarn startLocalCloudflareFunctions`: only starts the functions development server +- (preferred) `yarn make up.full`: starts the whole local development stack, including the functions development server +- `yarn startLocalCloudflareFunctions`: only starts the functions development server Note: compatibility dates between local development, production and preview environments should be kept in sync: -- local: defined in `package.json` -> `startLocalCloudflareFunctions` -- production & preview : see https://dash.cloudflare.com/078fcdfed9955087315dd86792e71a7e/pages/view/owid/settings/functions +- local: defined in `package.json` -> `startLocalCloudflareFunctions` +- production & preview : see https://dash.cloudflare.com/078fcdfed9955087315dd86792e71a7e/pages/view/owid/settings/functions 3. _Refer to each function's "Development" section below for further instructions._ @@ -147,8 +147,8 @@ In order to test the webhook function locally, you can use the Stripe CLI to lis STRIPE_API_KEY=xxx stripe listen --latest --forward-to localhost:8788/donation/thank-you ``` -- replace `xxx` with the value of `STRIPE_API_KEY (dev)` in 1password. Alternatively, if you have access to the Stripe dashboard, you can forgo the `STRIPE_API_KEY=xxx` part and let `stripe listen ...` guide you through a one-time login process. -- `--latest` is required when development code uses a more recent API version than the one set in the Stripe dashboard (which `stripe listen` will default to). +- replace `xxx` with the value of `STRIPE_API_KEY (dev)` in 1password. Alternatively, if you have access to the Stripe dashboard, you can forgo the `STRIPE_API_KEY=xxx` part and let `stripe listen ...` guide you through a one-time login process. +- `--latest` is required when development code uses a more recent API version than the one set in the Stripe dashboard (which `stripe listen` will default to). 3. Copy the webhook secret into `STRIPE_WEBHOOK_SECRET` variable in your `.dev.vars` and then restart the development server. This secret is shown when you ran `stripe listen`, and is stable across restarts. @@ -181,10 +181,10 @@ This route is where the actual thumbnail magic happens 🙌🏻✨ It can: -- Generate _png_ and _svg_ previews -- Render _png_ exports using our custom fonts, Lato and Playfair -- Render a preview according to all its query parameters -- Customize the image output a bunch using various options (see below) +- Generate _png_ and _svg_ previews +- Render _png_ exports using our custom fonts, Lato and Playfair +- Render a preview according to all its query parameters +- Customize the image output a bunch using various options (see below) We (plan to) use these for social media previews, such that the social media user will see the exact chart that is shared, for example with a `?tab=chart&country=IND~CHN&time=2000`. We cannot possibly create static previews for all possible combinations, but we can generate them dynamically on the fly as they are being shared. From 9e167522179be1a008fee77d7dc8eedc76407687 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Tue, 26 Nov 2024 14:00:09 +0100 Subject: [PATCH 3/4] enhance: extract `GRAPHER_SQUARE_SIZE` as constant --- functions/_common/imageOptions.ts | 13 ++++++++----- .../@ourworldindata/grapher/src/core/Grapher.tsx | 10 ++++++++-- .../grapher/src/core/GrapherConstants.ts | 2 ++ packages/@ourworldindata/grapher/src/index.ts | 1 + 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/functions/_common/imageOptions.ts b/functions/_common/imageOptions.ts index 05c70290840..0ccf0b3be43 100644 --- a/functions/_common/imageOptions.ts +++ b/functions/_common/imageOptions.ts @@ -1,4 +1,7 @@ -import { GrapherProgrammaticInterface } from "@ourworldindata/grapher" +import { + GrapherProgrammaticInterface, + GRAPHER_SQUARE_SIZE, +} from "@ourworldindata/grapher" import { DEFAULT_ASPECT_RATIO, MIN_ASPECT_RATIO, @@ -38,10 +41,10 @@ const OPEN_GRAPH_OPTIONS: ImageOptions = { fontSize: 21, } const SOCIAL_MEDIA_SQUARE_OPTIONS: ImageOptions = { - pngWidth: 2160, - pngHeight: 2160, - svgWidth: 540, - svgHeight: 540, + pngWidth: 4 * GRAPHER_SQUARE_SIZE, + pngHeight: 4 * GRAPHER_SQUARE_SIZE, + svgWidth: GRAPHER_SQUARE_SIZE, + svgHeight: GRAPHER_SQUARE_SIZE, details: false, fontSize: undefined, grapherProps: { diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 0243e038ba2..365028f6d9a 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -139,6 +139,7 @@ import { GRAPHER_FRAME_PADDING_VERTICAL, latestGrapherConfigSchema, validChartTypeCombinations, + GRAPHER_SQUARE_SIZE, } from "../core/GrapherConstants" import { loadVariableDataAndMetadata } from "./loadVariable" import Cookies from "js-cookie" @@ -1936,7 +1937,7 @@ export class Grapher get typeExceptWhenLineChartAndSingleTimeThenWillBeBarChart(): GrapherChartType { return this.isLineChartThatTurnedIntoDiscreteBar ? GRAPHER_CHART_TYPES.DiscreteBar - : (this.activeChartType ?? GRAPHER_CHART_TYPES.LineChart) + : this.activeChartType ?? GRAPHER_CHART_TYPES.LineChart } @computed get isLineChart(): boolean { @@ -2072,7 +2073,12 @@ export class Grapher case GrapherStaticFormat.landscape: return this.defaultBounds case GrapherStaticFormat.square: - return new Bounds(0, 0, 540, 540) + return new Bounds( + 0, + 0, + GRAPHER_SQUARE_SIZE, + GRAPHER_SQUARE_SIZE + ) default: return this.defaultBounds } diff --git a/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts b/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts index fd8bc6d38a5..1485a2658c4 100644 --- a/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts +++ b/packages/@ourworldindata/grapher/src/core/GrapherConstants.ts @@ -19,6 +19,8 @@ export const GRAPHER_LOADED_EVENT_NAME = "grapherLoaded" export const DEFAULT_GRAPHER_WIDTH = 850 export const DEFAULT_GRAPHER_HEIGHT = 600 +export const GRAPHER_SQUARE_SIZE = 540 + export const GRAPHER_FRAME_PADDING_VERTICAL = 16 export const GRAPHER_FRAME_PADDING_HORIZONTAL = 16 diff --git a/packages/@ourworldindata/grapher/src/index.ts b/packages/@ourworldindata/grapher/src/index.ts index 81efb82e166..b2476752247 100644 --- a/packages/@ourworldindata/grapher/src/index.ts +++ b/packages/@ourworldindata/grapher/src/index.ts @@ -14,6 +14,7 @@ export { GRAPHER_IS_IN_IFRAME_CLASS, DEFAULT_GRAPHER_WIDTH, DEFAULT_GRAPHER_HEIGHT, + GRAPHER_SQUARE_SIZE, STATIC_EXPORT_DETAIL_SPACING, DEFAULT_GRAPHER_ENTITY_TYPE, GRAPHER_LOADED_EVENT_NAME, From 5ccf02fbe3d5675940ef16346c8a9a1636ff9caf Mon Sep 17 00:00:00 2001 From: marcelgerber Date: Tue, 26 Nov 2024 13:02:32 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A4=96=20style:=20prettify=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/grapher/src/core/Grapher.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index 365028f6d9a..99080b5f556 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -1937,7 +1937,7 @@ export class Grapher get typeExceptWhenLineChartAndSingleTimeThenWillBeBarChart(): GrapherChartType { return this.isLineChartThatTurnedIntoDiscreteBar ? GRAPHER_CHART_TYPES.DiscreteBar - : this.activeChartType ?? GRAPHER_CHART_TYPES.LineChart + : (this.activeChartType ?? GRAPHER_CHART_TYPES.LineChart) } @computed get isLineChart(): boolean {