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.
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:
// 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:

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.
2024 regular season
| Team | Wins | Losses | PF |
|---|---|---|---|
![]() | 15 | 2 | 385 |
![]() | 11 | 6 | 402 |
![]() | 10 | 7 | 425 |
![]() | 4 | 13 | 309 |
| Team | Wins | Losses | PF |
|---|---|---|---|
![]() | 13 | 4 | 525 |
![]() | 8 | 9 | 345 |
![]() | 5 | 12 | 338 |
![]() | 4 | 13 | 289 |
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" },
);
| Team | Quarterback | Wins | Losses |
|---|---|---|---|
| KC | Patrick Mahomes | 15 | 2 |
| LAC | Justin Herbert | 11 | 6 |
| DEN | Bo Nix | 10 | 7 |
| LV | Gardner Minshew | 4 | 13 |
| Team | Quarterback | Wins | Losses |
|---|---|---|---|
| BUF | Josh Allen | 13 | 4 |
| MIA | Tua Tagovailoa | 8 | 9 |
| NYJ | Aaron Rodgers | 5 | 12 |
| NE | Drake Maye | 4 | 13 |
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.
AFC, 2024 regular season
| Team | Points for | Points against |
|---|---|---|
| KC | 385 | 326 |
| LAC | 402 | 301 |
| DEN | 425 | 311 |
| LV | 309 | 434 |
| BUF | 525 | 368 |
| MIA | 345 | 364 |
| NYJ | 338 | 404 |
| NE | 289 | 417 |
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.







