Skip to main content

Export to PNG

Two subpaths turn figures and tables into images in Node, with the defaults of sdvplotR's and the Python sdvplot's gt_save_crop, gt_social_crop, gt_save_batch, gt_grid and gt_stack_tables. @sportsdataverse/sdvplot/export draws an SVG figure with resvg and needs no browser. @sportsdataverse/sdvtables/export lays tables out in headless Chromium through playwright. Both renderers are optional peers: a function whose peer is not installed throws OptionalDependencyError naming the install command, and arguments are checked before anything starts.

A figure as a PNG, without a browser​

socialCard frames an SVG for a post the way gt_social_crop does: padded, then the short side widened to the ratio, never cropped. toPNG draws it with @resvg/resvg-js (pnpm add -D @resvg/resvg-js), downloading each remote logo once.

The 2024 AFC West as team logos, points for against points against, framed 16:9: Kansas City, the Chargers, Denver and Las Vegas
import * as Plot from "@observablehq/plot";
import { STANDINGS } from "@sportsdataverse/examples/data";
import { loadLeague } from "@sportsdataverse/sdvplot";
import { socialCard, toPNG } from "@sportsdataverse/sdvplot/export";
import { logos } from "@sportsdataverse/sdvplot/plot";
import { JSDOM } from "jsdom";

await loadLeague("nfl");
const west = STANDINGS.filter((s) => s.division === "West");
// Points for (x) against points against (y, reversed: the best teams sit top right), grid lines every 40 points. No
// axis text: a build server may have no fonts for resvg to draw it with.
const every40 = [280, 320, 360, 400, 440];
const svg = Plot.plot({
document: new JSDOM("").window.document, // Plot in Node
width: 480,
height: 300,
inset: 30,
axis: null,
x: { domain: [280, 440] },
y: { domain: [280, 440], reverse: true },
marks: [
Plot.gridX(every40),
Plot.gridY(every40),
Plot.frame(),
logos(west, { league: "nfl", x: "pf", y: "pa", team: "team", height: 0.18 }),
],
});
// gt_social_crop's framing: pad, then widen the short side to 16:9, never cropping; toPNG downloads each logo once
const png = await toPNG(socialCard(svg.outerHTML, { aspect: "16:9", padding: 40, background: "#f4f4f4" }), {
width: 640,
});
`<img src="data:image/png;base64,${Buffer.from(png).toString("base64")}" width="640" height="360" alt="The 2024 AFC West as team logos, points for against points against, framed 16:9: Kansas City, the Chargers, Denver and Las Vegas">`;

The framing is plain arithmetic, shared with sdvtables' socialCrop: the figure's size (svgSize), the ratio (parseAspect), the canvas (canvasFor) and where the figure sits on it (parseGravity, offsetFor).

{
  "figure": {
    "width": 480,
    "height": 300
  },
  "ratio": 0.8,
  "canvas": {
    "width": 560,
    "height": 700
  },
  "offsets": {
    "north": [
      0,
      0
    ],
    "center": [
      0,
      160
    ],
    "south": [
      0,
      320
    ]
  },
  "background": "#f4f4f4"
}
import * as Plot from "@observablehq/plot";
import { STANDINGS } from "@sportsdataverse/examples/data";
import { loadLeague } from "@sportsdataverse/sdvplot";
import {
canvasFor,
checkColor,
offsetFor,
parseAspect,
parseGravity,
svgSize,
} from "@sportsdataverse/sdvplot/export";
import { logos } from "@sportsdataverse/sdvplot/plot";
import { JSDOM } from "jsdom";

await loadLeague("nfl");
const svg = Plot.plot({
document: new JSDOM("").window.document,
width: 480,
height: 300,
marks: [logos(STANDINGS, { league: "nfl", x: "pf", y: "pa", team: "team" })],
}).outerHTML;

const padding = 40;
const figure = svgSize(svg); // the root <svg>'s width and height (or its viewBox's)
const ratio = parseAspect("4:5"); // "16:9", "4x5" and plain numbers too
const padded = [figure.width + 2 * padding, figure.height + 2 * padding] as const;
const [width, height] = canvasFor(padded[0], padded[1], ratio); // the short side grows; nothing is cropped
// where the padded figure sits on that canvas, for three of the nine ImageMagick gravities
const at = (g: string) => offsetFor(width - padded[0], height - padded[1], parseGravity(g));
{
figure: { width: figure.width, height: figure.height },
ratio,
canvas: { width, height },
offsets: { north: at("north"), center: at("Center"), south: at("south") },
background: checkColor("background", "#f4f4f4"), // a CSS colour, refused if it could break out of the markup
};

toPNG draws text in the system fonts. A host without any (a slim Docker image, a minimal CI runner) gets every label drawn as nothing, with no error: give it font files with fonts, or install fontconfig and a font (on GitHub Actions, sudo apt-get install -y fontconfig fonts-dejavu-core).

{
  "0": 137,
  "1": 80,
  "2": 78,
  "3": 71,
  "4": 13,
  "5": 10,
  "6": 26,
  "7": 10,
  "8": 0,
  "9": 0,
  "10": 0,
  "11": 13,
  "12": 73,
  "13": 72,
  "14": 68,
  "15": 82,
  "16": 0,
  "17": 0,
  "18": 0,
  "19": 240,
  "20": 0,
  "21": 0,
  "22": 0,
  "23": 60,
  "24": 8,
  "25": 6,
  "26": 0,
  "27": 0,
  "28": 0,
  "29": 88,
  "30": 127,
  "31": 55,
  "32": 93,
  "33": 0,
  "34": 0,
  "35": 8,
  "36": 5,
  "37": 73,
  "38": 68,
  "39": 65,
  "40": 84,
  "41": 120,
  "42": 156,
  "43": 237,
  "44": 154,
  "45": 129,
  "46": 117,
  "47": 211,
  "48": 58,
  "49": 20,
  "50": 134,
  "51": 197,
  "52": 4,
  "53": 192,
  "54": 4,
  "55": 220,
  "56": 76,
  "57": 0,
  "58": 76,
  "59": 128,
  "60": 152,
  "61": 0,
  "62": 152,
  "63": 128,
  "64": 48,
  "65": 1,
  "66": 48,
  "67": 1,
  "68": 102,
  "69": 2,
  "70": 96,
  "71": 2,
  "72": 196,
  "73": 4,
  "74": 192,
  "75": 4,
  "76": 136,
  "77": 9,
  "78": 128,
  "79": 9,
  "80": 48,
  "81": 19,
  "82": 0,
  "83": 19,
  "84": 188,
  "85": 247,
  "86": 127,
  "87": 169,
  "88": 117,
  "89": 158,
  "90": 94,
  "91": 176,
  "92": 83,
  "93": 59,
  "94": 53,
  "95": 109,
  "96": 213,
  "97": 115,
  "98": 191,
  "99": 115,
  "100": 210,
  "101": 56,
  "102": 181,
  "103": 124,
  "104": 45,
  "105": 41,
  "106": 247,
  "107": 147,
  "108": 100,
  "109": 181,
  "110": 215,
  "111": 254,
  "112": 17,
  "113": 193,
  "114": 113,
  "115": 156,
  "116": 38,
  "117": 113,
  "118": 129,
  "119": 29,
  "120": 167,
  "121": 97,
  "122": 92,
  "123": 96,
  "124": 199,
  "125": 105,
  "126": 24,
  "127": 23,
  "128": 216,
  "129": 113,
  "130": 26,
  "131": 198,
  "132": 5,
  "133": 118,
  "134": 156,
  "135": 134,
  "136": 113,
  "137": 129,
  "138": 29,
  "139": 167,
  "140": 97,
  "141": 92,
  "142": 96,
  "143": 199,
  "144": 105,
  "145": 24,
  "146": 23,
  "147": 216,
  "148": 113,
  "149": 26,
  "150": 198,
  "151": 5,
  "152": 118,
  "153": 156,
  "154": 134,
  "155": 113,
  "156": 129,
  "157": 29,
  "158": 167,
  "159": 97,
  "160": 92,
  "161": 96,
  "162": 199,
  "163": 105,
  "164": 24,
  "165": 23,
  "166": 216,
  "167": 113,
  "168": 26,
  "169": 198,
  "170": 5,
  "171": 118,
  "172": 156,
  "173": 134,
  "174": 113,
  "175": 129,
  "176": 29,
  "177": 167,
  "178": 97,
  "179": 92,
  "180": 96,
  "181": 199,
  "182": 105,
  "183": 24,
  "184": 23,
  "185": 216,
  "186": 113,
  "187": 26,
  "188": 198,
  "189": 5,
  "190": 118,
  "191": 156,
  "192": 134,
  "193": 113,
  "194": 129,
  "195": 29,
  "196": 167,
  "197": 97,
  "198": 92,
  "199": 96,
  "200": 199,
  "201": 105,
  "202": 152,
  "203": 11,
  "204": 21,
  "205": 248,
  "206": 235,
  "207": 215,
  "208": 175,
  "209": 193,
  "210": 204,
  "211": 194,
  "212": 141,
  "213": 27,
  "214": 55,
  "215": 244,
  "216": 201,
  "217": 113,
  "218": 156,
  "219": 165,
  "220": 172,
  "221": 34,
  "222": 48,
  "223": 34,
  "224": 254,
  "225": 254,
  "226": 253,
  "227": 91,
  "228": 71,
  "229": 39,
  "230": 220,
  "231": 186,
  "232": 117,
  "233": 43,
  "234": 152,
  "235": 196,
  "236": 60,
  "237": 196,
  "238": 163,
  "239": 71,
  "240": 143,
  "241": 194,
  "242": 135,
  "243": 15,
  "244": 31,
  "245": 118,
  "246": 242,
  "247": 126,
  "248": 249,
  "249": 242,
  "250": 37,
  "251": 216,
  "252": 41,
  "253": 229,
  "254": 29,
  "255": 199,
  "256": 249,
  "257": 147,
  "258": 163,
  "259": 5,
  "260": 238,
  "261": 251,
  "262": 62,
  "263": 188,
  "264": 121,
  "265": 243,
  "266": 38,
  "267": 164,
  "268": 148,
  "269": 194,
  "270": 175,
  "271": 95,
  "272": 191,
  "273": 244,
  "274": 155,
  "275": 255,
  "276": 19,
  "277": 99,
  "278": 12,
  "279": 47,
  "280": 94,
  "281": 188,
  "282": 8,
  "283": 81,
  "284": 239,
  "285": 99,
  "286": 92,
  "287": 187,
  "288": 118,
  "289": 77,
  "290": 63,
  "291": 79,
  "292": 120,
  "293": 251,
  "294": 246,
  "295": 109,
  "296": 216,
  "297": 110,
  "298": 183,
  "299": 225,
  "300": 50,
  "301": 210,
  "302": 171,
  "303": 157,
  "304": 239,
  "305": 222,
  "306": 189,
  "307": 211,
  "308": 81,
  "309": 216,
  "310": 181,
  "311": 231,
  "312": 188,
  "313": 225,
  "314": 222,
  "315": 57,
  "316": 231,
  "317": 208,
  "318": 171,
  "319": 30,
  "320": 16,
  "321": 213,
  "322": 159,
  "323": 183,
  "324": 111,
  "325": 223,
  "326": 14,
  "327": 15,
  "328": 31,
  "329": 62,
  "330": 212,
  "331": 167,
  "332": 63,
  "333": 233,
  "334": 85,
  "335": 142,
  "336": 107,
  "337": 150,
  "338": 114,
  "339": 17,
  "340": 109,
  "341": 115,
  "342": 206,
  "343": 206,
  "344": 81,
  "345": 2,
  "346": 39,
  "347": 73,
  "348": 251,
  "349": 228,
  "350": 201,
  "351": 19,
  "352": 29,
  "353": 157,
  "354": 206,
  "355": 179,
  "356": 103,
  "357": 207,
  "358": 194,
  "359": 171,
  "360": 87,
  "361": 175,
  "362": 116,
  "363": 244,
  "364": 127,
  "365": 162,
  "366": 18,
  "367": 241,
  "368": 243,
  "369": 231,
  "370": 207,
  "371": 225,
  "372": 250,
  "373": 245,
  "374": 235,
  "375": 187,
  "376": 25,
  "377": 220,
  "378": 204,
  "379": 194,
  "380": 101,
  "381": 36,
  "382": 75,
  "383": 158,
  "384": 251,
  "385": 247,
  "386": 239,
  "387": 235,
  "388": 40,
  "389": 132,
  "390": 35,
  "391": 186,
  "392": 234,
  "393": 104,
  "394": 178,
  "395": 238,
  "396": 75,
  "397": 31,
  "398": 247,
  "399": 18,
  "400": 114,
  "401": 12,
  "402": 4,
  "403": 166,
  "404": 95,
  "405": 205,
  "406": 44,
  "407": 212,
  "408": 100,
  "409": 93,
  "410": 87,
  "411": 234,
  "412": 187,
  "413": 132,
  "414": 243,
  "415": 108,
  "416": 155,
  "417": 179,
  "418": 30,
  "419": 139,
  "420": 5,
  "421": 70,
  "422": 72,
  "423": 102,
  "424": 222,
  "425": 194,
  "426": 227,
  "427": 199,
  "428": 143,
  "429": 195,
  "430": 86,
  "431": 179,
  "432": 103,
  "433": 148,
  "434": 144,
  "435": 5,
  "436": 150,
  "437": 198,
  "438": 93,
  "439": 215,
  "440": 133,
  "441": 111,
  "442": 223,
  "443": 190,
  "444": 29,
  "445": 20,
  "446": 52,
  "447": 43,
  "448": 217,
  "449": 76,
  "450": 191,
  "451": 55,
  "452": 189,
  "453": 46,
  "454": 43,
  "455": 180,
  "456": 133,
  "457": 229,
  "458": 62,
  "459": 44,
  "460": 236,
  "461": 170,
  "462": 163,
  "463": 73,
  "464": 213,
  "465": 0,
  "466": 201,
  "467": 227,
  "468": 200,
  "469": 86,
  "470": 253,
  "471": 107,
  "472": 102,
  "473": 161,
  "474": 151,
  "475": 204,
  "476": 212,
  "477": 135,
  "478": 126,
  "479": 133,
  "480": 168,
  "481": 62,
  "482": 255,
  "483": 244,
  "484": 233,
  "485": 147,
  "486": 142,
  "487": 254,
  "488": 131,
  "489": 190,
  "490": 230,
  "491": 59,
  "492": 154,
  "493": 3,
  "494": 3,
  "495": 40,
  "496": 48,
  "497": 163,
  "498": 115,
  "499": 157,
  "500": 211,
  "501": 30,
  "502": 139,
  "503": 4,
  "504": 206,
  "505": 18,
  "506": 174,
  "507": 140,
  "508": 238,
  "509": 136,
  "510": 153,
  "511": 245,
  "512": 249,
  "513": 206,
  "514": 157,
  "515": 59,
  "516": 250,
  "517": 52,
  "518": 14,
  "519": 137,
  "520": 180,
  "521": 85,
  "522": 242,
  "523": 29,
  "524": 42,
  "525": 115,
  "526": 217,
  "527": 233,
  "528": 52,
  "529": 16,
  "530": 189,
  "531": 124,
  "532": 249,
  "533": 82,
  "534": 71,
  "535": 231,
  "536": 35,
  "537": 48,
  "538": 143,
  "539": 35,
  "540": 102,
  "541": 182,
  "542": 219,
  "543": 83,
  "544": 96,
  "545": 112,
  "546": 76,
  "547": 146,
  "548": 121,
  "549": 159,
  "550": 173,
  "551": 250,
  "552": 180,
  "553": 44,
  "554": 147,
  "555": 143,
  "556": 125,
  "557": 252,
  "558": 96,
  "559": 32,
  "560": 96,
  "561": 96,
  "562": 226,
  "563": 123,
  "564": 68,
  "565": 94,
  "566": 51,
  "567": 11,
  "568": 78,
  "569": 123,
  "570": 204,
  "571": 22,
  "572": 152,
  "573": 196,
  "574": 218,
  "575": 108,
  "576": 54,
  "577": 187,
  "578": 119,
  "579": 96,
  "580": 227,
  "581": 169,
  "582": 101,
  "583": 49,
  "584": 231,
  "585": 210,
  "586": 157,
  "587": 179,
  "588": 192,
  "589": 128,
  "590": 80,
  "591": 73,
  "592": 226,
  "593": 190,
  "594": 126,
  "595": 253,
  "596": 90,
  "597": 159,
  "598": 254,
  "599": 132,
  "600": 239,
  "601": 192,
  "602": 236,
  "603": 68,
  "604": 242,
  "605": 123,
  "606": 247,
  "607": 238,
  "608": 133,
  "609": 172,
  "610": 129,
  "611": 116,
  "612": 9,
  "613": 189,
  "614": 102,
  "615": 242,
  "616": 187,
  "617": 119,
  "618": 239,
  "619": 238,
  "620": 226,
  "621": 188,
  "622": 127,
  "623": 255,
  "624": 126,
  "625": 183,
  "626": 28,
  "627": 119,
  "628": 218,
  "629": 100,
  "630": 182,
  "631": 192,
  "632": 73,
  "633": 9,
  "634": 85,
  "635": 150,
  "636": 117,
  "637": 108,
  "638": 120,
  "639": 116,
  "640": 74,
  "641": 236,
  "642": 179,
  "643": 144,
  "644": 20,
  "645": 239,
  "646": 199,
  "647": 143,
  "648": 31,
  "649": 58,
  "650": 58,
  "651": 137,
  "652": 55,
  "653": 5,
  "654": 201,
  "655": 204,
  "656": 108,
  "657": 195,
  "658": 59,
  "659": 47,
  "660": 118,
  "661": 173,
  "662": 25,
  "663": 56,
  "664": 162,
  "665": 150,
  "666": 143,
  "667": 79,
  "668": 159,
  "669": 62,
  "670": 85,
  "671": 137,
  "672": 113,
  "673": 210,
  "674": 16,
  "675": 159,
  "676": 4,
  "677": 143,
  "678": 42,
  "679": 11,
  "680": 89,
  "681": 137,
  "682": 206,
  "683": 242,
  "684": 159,
  "685": 56,
  "686": 64,
  "687": 28,
  "688": 102,
  "689": 185,
  "690": 177,
  "691": 4,
  "692": 46,
  "693": 210,
  "694": 102,
  "695": 93,
  "696": 147,
  "697": 245,
  "698": 130,
  "699": 110,
  "700": 175,
  "701": 205,
  "702": 92,
  "703": 107,
  "704": 102,
  "705": 161,
  "706": 128,
  "707": 16,
  "708": 92,
  "709": 71,
  "710": 252,
  "711": 94,
  "712": 146,
  "713": 128,
  "714": 233,
  "715": 60,
  "716": 241,
  "717": 41,
  "718": 75,
  "719": 221,
  "720": 215,
  "721": 34,
  "722": 170,
  "723": 77,
  "724": 44,
  "725": 129,
  "726": 77,
  "727": 241,
  "728": 191,
  "729": 127,
  "730": 255,
  "731": 174,
  "732": 223,
  "733": 204,
  "734": 135,
  "735": 85,
  "736": 84,
  "737": 86,
  "738": 155,
  "739": 30,
  "740": 60,
  "741": 120,
  "742": 176,
  "743": 155,
  "744": 137,
  "745": 157,
  "746": 118,
  "747": 153,
  "748": 45,
  "749": 48,
  "750": 73,
  "751": 248,
  "752": 241,
  "753": 227,
  "754": 71,
  "755": 29,
  "756": 133,
  "757": 240,
  "758": 243,
  "759": 231,
  "760": 207,
  "761": 51,
  "762": 39,
  "763": 99,
  "764": 28,
  "765": 18,
  "766": 16,
  "767": 198,
  "768": 170,
  "769": 144,
  "770": 149,
  "771": 96,
  "772": 200,
  "773": 144,
  "774": 245,
  "775": 62,
  "776": 69,
  "777": 84,
  "778": 12,
  "779": 102,
  "780": 144,
  "781": 177,
  "782": 186,
  "783": 68,
  "784": 157,
  "785": 35,
  "786": 62,
  "787": 73,
  "788": 154,
  "789": 36,
  "790": 51,
  "791": 131,
  "792": 207,
  "793": 84,
  "794": 178,
  "795": 50,
  "796": 128,
  "797": 116,
  "798": 123,
  "799": 114,
  "800": 214,
  "801": 187,
  "802": 228,
  "803": 83,
  "804": 240,
  "805": 252,
  "806": 25,
  "807": 117,
  "808": 31,
  "809": 64,
  "810": 90,
  "811": 196,
  "812": 64,
  "813": 226,
  "814": 49,
  "815": 168,
  "816": 35,
  "817": 203,
  "818": 93,
  "819": 250,
  "820": 113,
  "821": 13,
  "822": 120,
  "823": 60,
  "824": 97,
  "825": 48,
  "826": 130,
  "827": 177,
  "828": 254,
  "829": 155,
  "830": 34,
  "831": 171,
  "832": 63,
  "833": 169,
  "834": 167,
  "835": 47,
  "836": 157,
  "837": 175,
  "838": 6,
  "839": 179,
  "840": 5,
  "841": 46,
  "842": 9,
  "843": 189,
  "844": 214,
  "845": 134,
  "846": 71,
  "847": 84,
  "848": 226,
  "849": 35,
  "850": 24,
  "851": 140,
  "852": 85,
  "853": 129,
  "854": 217,
  "855": 177,
  "856": 108,
  "857": 214,
  "858": 48,
  "859": 123,
  "860": 69,
  "861": 149,
  "862": 55,
  "863": 179,
  "864": 208,
  "865": 107,
  "866": 102,
  "867": 75,
  "868": 18,
  "869": 178,
  "870": 92,
  "871": 59,
  "872": 38,
  "873": 31,
  "874": 68,
  "875": 149,
  "876": 167,
  "877": 12,
  "878": 113,
  "879": 128,
  "880": 58,
  "881": 151,
  "882": 217,
  "883": 216,
  "884": 204,
  "885": 66,
  "886": 86,
  "887": 34,
  "888": 51,
  "889": 179,
  "890": 23,
  "891": 152,
  "892": 197,
  "893": 204,
  "894": 44,
  "895": 20,
  "896": 186,
  "897": 33,
  "898": 102,
  "899": 210,
  "900": 189,
  "901": 152,
  "902": 201,
  "903": 129,
  "904": 123,
  "905": 213,
  "906": 108,
  "907": 245,
  "908": 236,
  "909": 105,
  "910": 102,
  "911": 59,
  "912": 105,
  "913": 89,
  "914": 146,
  "915": 246,
  "916": 170,
  "917": 27,
  "918": 98,
  "919": 176,
  "920": 244,
  "921": 229,
  "922": 190,
  "923": 252,
  "924": 190,
  "925": 215,
  "926": 239,
  "927": 146,
  "928": 98,
  "929": 112,
  "930": 255,
  "931": 172,
  "932": 123,
  "933": 242,
  "934": 251,
  "935": 53,
  "936": 136,
  "937": 106,
  "938": 7,
  "939": 237,
  "940": 163,
  "941": 77,
  "942": 89,
  "943": 113,
  "944": 231,
  "945": 178,
  "946": 217,
  "947": 108,
  "948": 66,
  "949": 175,
  "950": 58,
  "951": 209,
  "952": 150,
  "953": 110,
  "954": 104,
  "955": 163,
  "956": 211,
  "957": 46,
  "958": 179,
  "959": 4,
  "960": 38,
  "961": 249,
  "962": 72,
  "963": 80,
  "964": 88,
  "965": 235,
  "966": 139,
  "967": 143,
  "968": 67,
  "969": 2,
  "970": 194,
  "971": 88,
  "972": 21,
  "973": 178,
  "974": 146,
  "975": 146,
  "976": 25,
  "977": 179,
  "978": 211,
  "979": 189,
  "980": 152,
  "981": 189,
  "982": 106,
  "983": 16,
  "984": 3,
  "985": 17,
  "986": 16,
  "987": 139,
  "988": 115,
  "989": 200,
  "990": 199,
  "991": 123,
  "992": 77,
  "993": 172,
  "994": 226,
  "995": 3,
  "996": 179,
  "997": 223,
  "998": 86,
  "999": 194,
  "1000": 213,
  "1001": 116,
  "1002": 138,
  "1003": 205,
  "1004": 44,
  "1005": 15,
  "1006": 252,
  "1007": 73,
  "1008": 134,
  "1009": 89,
  "1010": 109,
  "1011": 159,
  "1012": 88,
  "1013": 197,
  "1014": 25,
  "1015": 171,
  "1016": 39,
  "1017": 100,
  "1018": 213,
  "1019": 149,
  "1020": 89,
  "1021": 13,
  "1022": 234,
  "1023": 89,
  "1024": 185,
  "1025": 166,
  "1026": 151,
  "1027": 52,
  "1028": 102,
  "1029": 22,
  "1030": 214,
  "1031": 128,
  "1032": 246,
  "1033": 223,
  "1034": 188,
  "1035": 121,
  "1036": 83,
  "1037": 71,
  "1038": 97,
  "1039": 247,
  "1040": 24,
  "1041": 193,
  "1042": 128,
  "1043": 49,
  "1044": 135,
  "1045": 164,
  "1046": 129,
  "1047": 132,
  "1048": 149,
  "1049": 8,
  "1050": 172,
  "1051": 177,
  "1052": 138,
  "1053": 114,
  "1054": 46,
  "1055": 158,
  "1056": 89,
  "1057": 2,
  "1058": 231,
  "1059": 42,
  "1060": 65,
  "1061": 207,
  "1062": 75,
  "1063": 224,
  "1064": 211,
  "1065": 72,
  "1066": 85,
  "1067": 50,
  "1068": 142,
  "1069": 73,
  "1070": 19,
  "1071": 245,
  "1072": 185,
  "1073": 196,
  "1074": 31,
  "1075": 59,
  "1076": 15,
  "1077": 181,
  "1078": 8,
  "1079": 204,
  "1080": 242,
  "1081": 73,
  "1082": 49,
  "1083": 247,
  "1084": 137,
  "1085": 186,
  "1086": 174,
  "1087": 196,
  "1088": 153,
  "1089": 170,
  "1090": 103,
  "1091": 167,
  "1092": 254,
  "1093": 40,
  "1094": 3,
  "1095": 193,
  "1096": 121,
  "1097": 136,
  "1098": 209,
  "1099": 85,
  "1100": 247,
  "1101": 99,
  "1102": 240,
  "1103": 50,
  "1104": 179,
  "1105": 48,
  "1106": 7,
  "1107": 190,
  "1108": 195,
  "1109": 172,
  "1110": 239,
  "1111": 114,
  "1112": 170,
  "1113": 173,
  "1114": 78,
  "1115": 123,
  "1116": 172,
  "1117": 38,
  "1118": 112,
  "1119": 174,
  "1120": 202,
  "1121": 236,
  "1122": 51,
  "1123": 118,
  "1124": 77,
  "1125": 156,
  "1126": 33,
  "1127": 198,
  "1128": 33,
  "1129": 114,
  "1130": 117,
  "1131": 63,
  "1132": 158,
  "1133": 131,
  "1134": 247,
  "1135": 159,
  "1136": 45,
  "1137": 227,
  "1138": 204,
  "1139": 248,
  "1140": 200,
  "1141": 118,
  "1142": 104,
  "1143": 55,
  "1144": 55,
  "1145": 206,
  "1146": 136,
  "1147": 195,
  "1148": 74,
  "1149": 129,
  "1150": 63,
  "1151": 201,
  "1152": 64,
  "1153": 167,
  "1154": 118,
  "1155": 210,
  "1156": 222,
  "1157": 191,
  "1158": 69,
  "1159": 189,
  "1160": 26,
  "1161": 226,
  "1162": 249,
  "1163": 158,
  "1164": 123,
  "1165": 207,
  "1166": 161,
  "1167": 215,
  "1168": 10,
  "1169": 96,
  "1170": 179,
  "1171": 217,
  "1172": 232,
  "1173": 104,
  "1174": 188,
  "1175": 191,
  "1176": 156,
  "1177": 54,
  "1178": 105,
  "1179": 70,
  "1180": 224,
  "1181": 94,
  "1182": 9,
  "1183": 200,
  "1184": 51,
  "1185": 49,
  "1186": 9,
  "1187": 204,
  "1188": 171,
  "1189": 215,
  "1190": 103,
  "1191": 222,
  "1192": 225,
  "1193": 44,
  "1194": 241,
  "1195": 227,
  "1196": 80,
  "1197": 238,
  "1198": 44,
  "1199": 2,
  "1200": 3,
  "1201": 75,
  "1202": 122,
  "1203": 234,
  "1204": 7,
  "1205": 81,
  "1206": 215,
  "1207": 176,
  "1208": 28,
  "1209": 71,
  "1210": 176,
  "1211": 181,
  "1212": 65,
  "1213": 94,
  "1214": 218,
  "1215": 205,
  "1216": 179,
  "1217": 118,
  "1218": 175,
  "1219": 62,
  "1220": 96,
  "1221": 0,
  "1222": 154,
  "1223": 3,
  "1224": 245,
  "1225": 97,
  "1226": 211,
  "1227": 139,
  "1228": 235,
  "1229": 88,
  "1230": 121,
  "1231": 56,
  "1232": 87,
  "1233": 131,
  "1234": 89,
  "1235": 2,
  "1236": 147,
  "1237": 48,
  "1238": 36,
  "1239": 14,
  "1240": 76,
  "1241": 37,
  "1242": 58,
  "1243": 73,
  "1244": 65,
  "1245": 185,
  "1246": 26,
  "1247": 146,
  "1248": 134,
  "1249": 164,
  "1250": 62,
  "1251": 86,
  "1252": 48,
  "1253": 98,
  "1254": 178,
  "1255": 243,
  "1256": 157,
  "1257": 180,
  "1258": 220,
  "1259": 203,
  "1260": 35,
  "1261": 247,
  "1262": 44,
  "1263": 28,
  "1264": 27,
  "1265": 31,
  "1266": 226,
  "1267": 80,
  "1268": 110,
  "1269": 170,
  "1270": 93,
  "1271": 113,
  "1272": 56,
  "1273": 15,
  "1274": 135,
  "1275": 226,
  "1276": 80,
  "1277": 87,
  "1278": 102,
  "1279": 181,
  "1280": 82,
  "1281": 22,
  "1282": 144,
  "1283": 139,
  "1284": 62,
  "1285": 96,
  "1286": 201,
  "1287": 106,
  "1288": 102,
  "1289": 225,
  "1290": 172,
  "1291": 240,
  "1292": 200,
  "1293": 144,
  "1294": 212,
  "1295": 23,
  "1296": 48,
  "1297": 245,
  "1298": 88,
  "1299": 48,
  "1300": 5,
  "1301": 223,
  "1302": 31,
  "1303": 223,
  "1304": 15,
  "1305": 131,
  "1306": 202,
  "1307": 220,
  "1308": 89,
  "1309": 219,
  "1310": 185,
  "1311": 252,
  "1312": 204,
  "1313": 18,
  "1314": 24,
  "1315": 202,
  "1316": 46,
  "1317": 244,
  "1318": 84,
  "1319": 162,
  "1320": 143,
  "1321": 17,
  "1322": 149,
  "1323": 96,
  "1324": 36,
  "1325": 244,
  "1326": 49,
  "1327": 130,
  "1328": 33,
  "1329": 4,
  "1330": 51,
  "1331": 58,
  "1332": 73,
  "1333": 7,
  "1334": 252,
  "1335": 75,
  "1336": 33,
  "1337": 130,
  "1338": 68,
  "1339": 93,
  "1340": 135,
  "1341": 24,
  "1342": 192,
  "1343": 121,
  "1344": 56,
  "1345": 38,
  "1346": 126,
  "1347": 33,
  "1348": 14,
  "1349": 229,
  "1350": 166,
  "1351": 218,
  "1352": 21,
  "1353": 135,
  "1354": 243,
  "1355": 112,
  "1356": 40,
  "1357": 78,
  "1358": 33,
  "1359": 73,
  "1360": 176,
  "1361": 164,
  "1362": 87,
  "1363": 185,
  "1364": 6,
  "1365": 168,
  "1366": 47,
  "1367": 155,
  "1368": 100,
  "1369": 219,
  "1370": 189,
  "1371": 77,
  "1372": 180,
  "1373": 37,
  "1374": 176,
  "1375": 81,
  "1376": 245,
  "1377": 252,
  "1378": 249,
  "1379": 115,
  "1380": 29,
  "1381": 77,
  "1382": 111,
  "1383": 184,
  "1384": 77,
  "1385": 65,
  "1386": 95,
  "1387": 150,
  "1388": 103,
  "1389": 253,
  "1390": 177,
  "1391": 190,
  "1392": 114,
  "1393": 218,
  "1394": 101,
  "1395": 182,
  "1396": 192,
  "1397": 245,
  "1398": 18,
  "1399": 113,
  "1400": 238,
  "1401": 198,
  "1402": 73,
  "1403": 28,
  "1404": 146,
  "1405": 127,
  "1406": 44,
  "1407": 105,
  "1408": 226,
  "1409": 112,
  "1410": 14,
  "1411": 246,
  "1412": 171,
  "1413": 208,
  "1414": 107,
  "1415": 105,
  "1416": 136,
  "1417": 156,
  "1418": 189,
  "1419": 222,
  "1420": 89,
  "1421": 242,
  "1422": 37,
  "1423": 9,
  "1424": 129,
  "1425": 188,
  "1426": 53,
  "1427": 89,
  "1428": 178,
  "1429": 81,
  "1430": 6,
  "1431": 150,
  "1432": 198,
  "1433": 175,
  "1434": 137,
  "1435": 67,
  "1436": 185,
  "1437": 181,
  "1438": 4,
  "1439": 46,
  "1440": 244,
  "1441": 170,
  "1442": 59,
  "1443": 210,
  "1444": 177,
  "1445": 108,
  "1446": 45,
  "1447": 140,
  "1448": 237,
  "1449": 132,
  "1450": 207,
  "1451": 33,
  "1452": 169,
  "1453": 253,
  "1454": 204,
  "1455": 190,
  "1456": 192,
  "1457": 108,
  "1458": 158,
  "1459": 244,
  "1460": 121,
  "1461": 9,
  "1462": 89,
  "1463": 237,
  "1464": 42,
  "1465": 125,
  "1466": 181,
  "1467": 116,
  "1468": 230,
  "1469": 118,
  "1470": 46,
  "1471": 55,
  "1472": 179,
  "1473": 5,
  "1474": 78,
  "1475": 74,
  "1476": 154,
  "1477": 165,
  "1478": 73,
  "1479": 20,
  "1480": 149,
  "1481": 40,
  "1482": 36,
  "1483": 255,
  "1484": 82,
  "1485": 193,
  "1486": 146,
  "1487": 98,
  "1488": 151,
  "1489": 123,
  "1490": 77,
  "1491": 37,
  "1492": 92,
  "1493": 174,
  "1494": 146,
  "1495": 114,
  "1496": 105,
  "1497": 252,
  "1498": 154,
  "1499": 56,
  "1500": 148,
  "1501": 91,
  "1502": 91,
  "1503": 224,
  "1504": 2,
  "1505": 43,
  "1506": 136,
  "1507": 168,
  "1508": 24,
  "1509": 108,
  "1510": 148,
  "1511": 49,
  "1512": 19,
  "1513": 179,
  "1514": 75,
  "1515": 189,
  "1516": 132,
  "1517": 84,
  "1518": 245,
  "1519": 197,
  "1520": 220,
  "1521": 126,
  "1522": 223,
  "1523": 167,
  "1524": 83,
  "1525": 223,
  "1526": 28,
  "1527": 179,
  "1528": 107,
  "1529": 237,
  "1530": 92,
  "1531": 126,
  "1532": 102,
  "1533": 11,
  "1534": 204,
  "1535": 50,
  "1536": 204,
  "1537": 244,
  "1538": 197,
  "1539": 147,
  "1540": 136,
  "1541": 48,
  "1542": 231,
  "1543": 127,
  "1544": 161,
  "1545": 163,
  "1546": 18,
  "1547": 151,
  "1548": 228,
  "1549": 95,
  "1550": 42,
  "1551": 216,
  "1552": 86,
  "1553": 179,
  "1554": 20,
  "1555": 255,
  "1556": 100,
  "1557": 193,
  "1558": 178,
  "1559": 185,
  "1560": 215,
  "1561": 76,
  "1562": 54,
  "1563": 70,
  "1564": 150,
  "1565": 108,
  "1566": 231,
  "1567": 33,
  "1568": 48,
  "1569": 51,
  "1570": 63,
  "1571": 207,
  "1572": 225,
  "1573": 112,
  "1574": 40,
  "1575": 206,
  "1576": 33,
  "1577": 58,
  "1578": 213,
  "1579": 173,
  "1580": 8,
  "1581": 180,
  "1582": 36,
  "1583": 70,
  "1584": 146,
  "1585": 172,
  "1586": 69,
  "1587": 222,
  "1588": 169,
  "1589": 250,
  "1590": 205,
  "1591": 97,
  "1592": 59,
  "1593": 244,
  "1594": 39,
  "1595": 44,
  "1596": 185,
  "1597": 191,
  "1598": 115,
  "1599": 249,
  "1600": 153,
  "1601": 45,
  "1602": 48,
  "1603": 176,
  "1604": 249,
  "1605": 81,
  "1606": 254,
  "1607": 92,
  "1608": 194,
  "1609": 108,
  "1610": 194,
  "1611": 159,
  "1612": 35,
  "1613": 162,
  "1614": 4,
  "1615": 152,
  "1616": 162,
  "1617": 108,
  "1618": 156,
  "1619": 44,
  "1620": 21,
  "1621": 108,
  "1622": 142,
  "1623": 52,
  "1624": 36,
  "1625": 118,
  "1626": 82,
  "1627": 130,
  "1628": 195,
  "1629": 210,
  "1630": 248,
  "1631": 53,
  "1632": 113,
  "1633": 40,
  "1634": 55,
  "1635": 37,
  "1636": 72,
  "1637": 167,
  "1638": 184,
  "1639": 69,
  "1640": 190,
  "1641": 169,
  "1642": 217,
  "1643": 171,
  "1644": 215,
  "1645": 32,
  "1646": 67,
  "1647": 127,
  "1648": 240,
  "1649": 26,
  "1650": 131,
  "1651": 231,
  "1652": 213,
  "1653": 178,
  "1654": 148,
  "1655": 62,
  "1656": 84,
  "1657": 151,
  "1658": 154,
  "1659": 164,
  "1660": 182,
  "1661": 209,
  "1662": 70,
  "1663": 224,
  "1664": 191,
  "1665": 223,
  "1666": 178,
  "1667": 234,
  "1668": 54,
  "1669": 21,
  "1670": 255,
  "1671": 52,
  "1672": 226,
  "1673": 208,
  "1674": 70,
  "1675": 30,
  "1676": 71,
  "1677": 24,
  "1678": 136,
  "1679": 157,
  "1680": 171,
  "1681": 195,
  "1682": 34,
  "1683": 129,
  "1684": 97,
  "1685": 91,
  "1686": 141,
  "1687": 230,
  "1688": 64,
  "1689": 114,
  "1690": 70,
  "1691": 37,
  "1692": 8,
  "1693": 73,
  "1694": 102,
  "1695": 102,
  "1696": 187,
  "1697": 4,
  "1698": 33,
  "1699": 89,
  "1700": 120,
  "1701": 254,
  "1702": 203,
  "1703": 74,
  "1704": 58,
  "1705": 88,
  "1706": 42,
  "1707": 88,
  "1708": 167,
  "1709": 178,
  "1710": 69,
  "1711": 26,
  "1712": 226,
  "1713": 179,
  "1714": 105,
  "1715": 83,
  "1716": 200,
  "1717": 138,
  "1718": 201,
  "1719": 185,
  "1720": 172,
  "1721": 247,
  "1722": 194,
  "1723": 210,
  "1724": 248,
  "1725": 53,
  "1726": 113,
  "1727": 40,
  "1728": 55,
  "1729": 37,
  "1730": 112,
  "1731": 61,
  "1732": 104,
  "1733": 49,
  "1734": 176,
  "1735": 240,
  "1736": 28,
  "1737": 139,
  "1738": 72,
  "1739": 180,
  "1740": 179,
  "1741": 192,
  "1742": 74,
  "1743": 128,
  "1744": 207,
  "1745": 157,
  "1746": 234,
  "1747": 192,
  "1748": 50,
  "1749": 183,
  "1750": 38,
  "1751": 43,
  "1752": 38,
  "1753": 215,
  "1754": 115,
  "1755": 126,
  "1756": 234,
  "1757": 30,
  "1758": 251,
  "1759": 164,
  "1760": 61,
  "1761": 121,
  "1762": 105,
  "1763": 47,
  "1764": 247,
  "1765": 60,
  "1766": 4,
  "1767": 177,
  "1768": 167,
  "1769": 216,
  "1770": 232,
  "1771": 239,
  "1772": 191,
  "1773": 189,
  "1774": 6,
  "1775": 25,
  "1776": 202,
  "1777": 228,
  "1778": 25,
  "1779": 247,
  "1780": 119,
  "1781": 218,
  "1782": 97,
  "1783": 177,
  "1784": 192,
  "1785": 128,
  "1786": 156,
  "1787": 101,
  "1788": 71,
  "1789": 244,
  "1790": 52,
  "1791": 72,
  "1792": 64,
  "1793": 36,
  "1794": 48,
  "1795": 179,
  "1796": 80,
  "1797": 19,
  "1798": 7,
  "1799": 113,
  "1800": 96,
  "1801": 191,
  "1802": 10,
  "1803": 36,
  "1804": 59,
  "1805": 203,
  "1806": 115,
  "1807": 254,
  "1808": 85,
  "1809": 178,
  "1810": 16,
  "1811": 85,
  "1812": 158,
  "1813": 217,
  "1814": 156,
  "1815": 115,
  "1816": 128,
  "1817": 180,
  "1818": 73,
  "1819": 137,
  "1820": 78,
  "1821": 25,
  "1822": 142,
  "1823": 59,
  "1824": 201,
  "1825": 83,
  "1826": 19,
  "1827": 85,
  "1828": 126,
  "1829": 42,
  "1830": 126,
  "1831": 77,
  "1832": 28,
  "1833": 202,
  "1834": 29,
  "1835": 74,
  "1836": 110,
  "1837": 51,
  "1838": 219,
  "1839": 221,
  "1840": 167,
  "1841": 96,
  "1842": 102,
  "1843": 161,
  "1844": 151,
  "1845": 16,
  "1846": 200,
  "1847": 140,
  "1848": 212,
  "1849": 81,
  "1850": 49,
  "1851": 202,
  "1852": 6,
  "1853": 31,
  "1854": 162,
  "1855": 81,
  "1856": 247,
  "1857": 66,
  "1858": 30,
  "1859": 98,
  "1860": 50,
  "1861": 251,
  "1862": 101,
  "1863": 29,
  "1864": 215,
  "1865": 231,
  "1866": 198,
  "1867": 160,
  "1868": 141,
  "1869": 172,
  "1870": 92,
  "1871": 150,
  "1872": 114,
  "1873": 168,
  "1874": 141,
  "1875": 199,
  "1876": 252,
  "1877": 5,
  "1878": 193,
  "1879": 105,
  "1880": 131,
  "1881": 163,
  "1882": 4,
  "1883": 6,
  "1884": 18,
  "1885": 13,
  "1886": 145,
  "1887": 235,
  "1888": 217,
  "1889": 184,
  "1890": 6,
  "1891": 113,
  "1892": 153,
  "1893": 61,
  "1894": 183,
  "1895": 154,
  "1896": 177,
  "1897": 199,
  "1898": 136,
  "1899": 74,
  "1900": 122,
  "1901": 196,
  "1902": 129,
  "1903": 177,
  "1904": 42,
  "1905": 16,
  "1906": 159,
  "1907": 235,
  "1908": 75,
  "1909": 153,
  "1910": 2,
  "1911": 73,
  "1912": 200,
  "1913": 125,
  "1914": 17,
  "1915": 33,
  "1916": 14,
  "1917": 49,
  "1918": 254,
  "1919": 182,
  "1920": 192,
  "1921": 189,
  "1922": 100,
  "1923": 69,
  "1924": 212,
  "1925": 34,
  "1926": 105,
  "1927": 129,
  "1928": 149,
  "1929": 1,
  "1930": 117,
  "1931": 132,
  "1932": 164,
  "1933": 193,
  "1934": 164,
  "1935": 83,
  "1936": 29,
  "1937": 106,
  "1938": 209,
  "1939": 11,
  "1940": 196,
  "1941": 78,
  "1942": 58,
  "1943": 111,
  "1944": 102,
  "1945": 225,
  "1946": 52,
  "1947": 178,
  "1948": 234,
  "1949": 192,
  "1950": 140,
  "1951": 190,
  "1952": 148,
  "1953": 67,
  "1954": 109,
  "1955": 116,
  "1956": 129,
  "1957": 175,
  "1958": 46,
  "1959": 71,
  "1960": 11,
  "1961": 92,
  "1962": 147,
  "1963": 135,
  "1964": 164,
  "1965": 96,
  "1966": 118,
  "1967": 100,
  "1968": 6,
  "1969": 50,
  "1970": 179,
  "1971": 96,
  "1972": 122,
  "1973": 173,
  "1974": 1,
  "1975": 34,
  "1976": 247,
  "1977": 18,
  "1978": 104,
  "1979": 237,
  "1980": 184,
  "1981": 199,
  "1982": 64,
  "1983": 251,
  "1984": 168,
  "1985": 15,
  "1986": 48,
  "1987": 128,
  "1988": 80,
  "1989": 167,
  "1990": 125,
  "1991": 122,
  "1992": 213,
  "1993": 181,
  "1994": 215,
  "1995": 139,
  "1996": 178,
  "1997": 156,
  "1998": 55,
  "1999": 213,
  "2000": 215,
  "2001": 244,
  "2002": 114,
  "2003": 156,
  "2004": 191,
  "2005": 193,
  "2006": 42,
  "2007": 2,
  "2008": 59,
  "2009": 142,
  "2010": 115,
  "2011": 49,
  "2012": 184,
  "2013": 192,
  "2014": 142,
  "2015": 211,
  "2016": 48,
  "2017": 46,
  "2018": 176,
  "2019": 227,
  "2020": 52,
  "2021": 140,
  "2022": 11,
  "2023": 236,
  "2024": 56,
  "2025": 13,
  "2026": 227,
  "2027": 2,
  "2028": 59,
  "2029": 78,
  "2030": 195,
  "2031": 184,
  "2032": 192,
  "2033": 142,
  "2034": 211,
  "2035": 48,
  "2036": 46,
  "2037": 176,
  "2038": 227,
  "2039": 52,
  "2040": 140,
  "2041": 11,
  "2042": 236,
  "2043": 56,
  "2044": 13,
  "2045": 227,
  "2046": 2,
  "2047": 59,
  "2048": 78,
  "2049": 195,
  "2050": 184,
  "2051": 192,
  "2052": 142,
  "2053": 211,
  "2054": 48,
  "2055": 46,
  "2056": 176,
  "2057": 227,
  "2058": 52,
  "2059": 140,
  "2060": 11,
  "2061": 236,
  "2062": 56,
  "2063": 13,
  "2064": 227,
  "2065": 2,
  "2066": 59,
  "2067": 78,
  "2068": 195,
  "2069": 184,
  "2070": 192,
  "2071": 142,
  "2072": 211,
  "2073": 48,
  "2074": 46,
  "2075": 176,
  "2076": 227,
  "2077": 52,
  "2078": 140,
  "2079": 11,
  "2080": 236,
  "2081": 56,
  "2082": 13,
  "2083": 227,
  "2084": 2,
  "2085": 59,
  "2086": 78,
  "2087": 195,
  "2088": 252,
  "2089": 11,
  "2090": 12,
  "2091": 152,
  "2092": 47,
  "2093": 167,
  "2094": 30,
  "2095": 0,
  "2096": 136,
  "2097": 236,
  "2098": 0,
  "2099": 0,
  "2100": 0,
  "2101": 0,
  "2102": 73,
  "2103": 69,
  "2104": 78,
  "2105": 68,
  "2106": 174,
  "2107": 66,
  "2108": 96,
  "2109": 130
}
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { toPNG } from "@sportsdataverse/sdvplot/export";

const font = join(dirname(fileURLToPath(import.meta.url)), "../../../../fonts/SourceSans3-Regular.ttf");
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="240" height="60"><text x="8" y="42" font-size="32">Giants 27</text></svg>`;
// the same pixels with or without system fonts: only the given file is loaded
await toPNG(svg, { background: "white", fonts: { files: [font], system: false } });

peerMissing is how both subpaths tell a missing optional peer from one that is installed but broken.

true
import { createRequire } from "node:module";
import { peerMissing } from "@sportsdataverse/sdvplot/export";

const name = "@sportsdataverse/no-such-peer";
let error: unknown;
try {
createRequire(import.meta.url).resolve(name);
} catch (e) {
error = e;
}
peerMissing(error, name); // true; peerMissing(error, "@resvg/resvg-js") is false

Tables as PNGs, in Chromium​

Install the peer and its browser once:

pnpm add -D playwright
pnpm exec playwright install chromium

tableToPNG renders a table at zoom 2, trims it to its content and puts 50 px of background back around it. socialCrop puts the trimmed table on a fixed-ratio canvas, htmlToPNG does the same for any HTML (a page of tables, say), and batchToPNG writes one image per value of a group column, all as wide as the widest. This script writes all four for the 2024 AFC standings:

standings-png.ts
// export-standings.ts: one standings table, written as PNGs in Node. Peer: playwright, with its Chromium
// (pnpm add -D playwright && pnpm exec playwright install chromium).
import { join } from "node:path";
import type { Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { batchToPNG, gridTables, htmlToPNG, socialCrop, tableToPNG } from "@sportsdataverse/sdvtables/export";

/** Writes the AFC standings into `dir` as PNGs and returns the paths, the plain table's first. */
export async function exportStandings(rows: readonly Standing[], dir: string): Promise<string[]> {
const spec = defineTable<Standing>()
.columns((c) => [
c.text("team"),
c.text("qb", { label: "Quarterback" }),
c.int("wins"),
c.int("losses"),
c.int("pf", { label: "PF" }),
c.int("pa", { label: "PA" }),
])
.title("AFC")
.subtitle("2024 regular season")
.theme("athletic")
.build();
const table = join(dir, "afc.png");
const post = join(dir, "afc-post.png");
const grid = join(dir, "afc-grid.png");
// gt_save_crop: rendered at zoom 2, trimmed to the table, 50 px of white put back around it, scaled to 900 px wide
await tableToPNG(spec, rows, { file: table, width: 900 });
// gt_social_crop: the same table on a 16:9 canvas, never cropped
await socialCrop(spec, rows, { aspect: "16:9", file: post });
// any HTML: here gt_grid's two divisions side by side
const division = (d: string) => ({ spec, rows: rows.filter((r) => r.division === d) });
await htmlToPNG(gridTables([division("West"), division("East")], { ncol: 2 }), { file: grid });
// gt_save_batch: one image per division, afc-west.png and afc-east.png, all as wide as the widest
const batch = await batchToPNG(
rows,
"division",
(groupRows) => ({ spec, rows: groupRows }),
"afc-{group}.png",
{
dir,
},
);
return [table, post, grid, ...batch];
}

Its first image, afc.png:

The 2024 AFC standings as a PNG: team, quarterback, wins, losses, points for and points against for eight teams, in the athletic theme

These docs are built without a browser, so the image above is not drawn live: it is the script's own output, regenerated by pnpm --filter @sportsdataverse/examples render:export-png. CI's render-tests job runs the script on every push to main and every PR. That job is non-gating; the examples typecheck covers the script too.

An image past Chromium's canvas limit (65,535 px a side, 268,435,456 px in all, near 1,000 plain rows at zoom 2) throws SdvplotError naming its size, and nothing is written. Put fewer rows in each image: a page at a time, batchToPNG by a group, or gridTables side by side.

Pages of tables, as HTML​

gridTables (gt_grid) and stackTables (gt_stack_tables) return HTML: the tables, with a title, subtitle, caption and source note around them. htmlToPNG turns that page into an image, as in the script above. One spec used twice gets distinct ids, so each copy keeps its decorations.

AFC by division

2024 regular season

AFC West
TeamWinsLossesPF
152385
116402
107425
413309
AFC East
TeamWinsLossesPF
134525
89345
512338
413289
import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { gridTables } from "@sportsdataverse/sdvtables/export";
import { prepare } from "@sportsdataverse/sdvtables/html";

const spec = defineTable<Standing>()
.columns((c) => [
c.logo("team", { league: "nfl" }),
c.int("wins"),
c.int("losses"),
c.int("pf", { label: "PF" }),
])
.theme("athletic")
.build();
await prepare(spec);
// HTML, no browser needed; htmlToPNG(page) is the PNG (Chromium, see the export guide)
gridTables(
[
{ spec, rows: STANDINGS.filter((r) => r.division === "West") },
{ spec, rows: STANDINGS.filter((r) => r.division === "East") },
],
{ ncol: 2, labels: ["AFC West", "AFC East"], title: "AFC by division", subtitle: "2024 regular season" },
);
AFC, 2024
TeamQuarterbackWinsLosses
KCPatrick Mahomes152
LACJustin Herbert116
DENBo Nix107
LVGardner Minshew413
TeamQuarterbackWinsLosses
BUFJosh Allen134
MIATua Tagovailoa89
NYJAaron Rodgers512
NEDrake Maye413

West above, East below

Source: nflverse

import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { stackTables } from "@sportsdataverse/sdvtables/export";

const spec = defineTable<Standing>()
.columns((c) => [c.text("team"), c.text("qb", { label: "Quarterback" }), c.int("wins"), c.int("losses")])
.theme("pl")
.build();
// one spec used twice: each copy gets its own id, so each keeps its decorations
stackTables(
[
{ spec, rows: STANDINGS.filter((r) => r.division === "West") },
{ spec, rows: STANDINGS.filter((r) => r.division === "East") },
],
{ title: "AFC, 2024", caption: "West above, East below", sourceNote: "Source: nflverse" },
);

composePage is that frame on its own, around any HTML.

Points for and against

AFC, 2024 regular season

TeamPoints forPoints against
KC385326
LAC402301
DEN425311
LV309434
BUF525368
MIA345364
NYJ338404
NE289417

Buffalo scored the most points of these eight teams, New England the fewest

Source: nflverse

import { STANDINGS, type Standing } from "@sportsdataverse/examples/data";
import { defineTable } from "@sportsdataverse/sdvtables";
import { composePage } from "@sportsdataverse/sdvtables/export";
import { renderHTML } from "@sportsdataverse/sdvtables/html";

const spec = defineTable<Standing>()
.columns((c) => [
c.text("team"),
c.int("pf", { label: "Points for" }),
c.int("pa", { label: "Points against" }),
])
.theme("swiss")
.build();
// the frame gridTables and stackTables put around their tables, here around one table of your own
composePage(renderHTML(spec, STANDINGS), {
title: "Points for and against",
subtitle: "AFC, 2024 regular season",
caption: "Buffalo scored the most points of these eight teams, New England the fewest",
captionRule: true,
sourceNote: "Source: nflverse",
});

In the gallery: Export (Node) for tables and Export (Node) for figures.