csat-chart.js 1.2.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,17 @@
1
1
  // src/core/canvas/renderer.ts
2
- function getFont(size, family = "serif", customFont, weight = "normal") {
3
- const familyMap = {
4
- serif: "'Noto Serif KR', 'NanumMyeongjo', serif",
5
- sans: "'Noto Sans KR', sans-serif",
6
- custom: customFont || "'Noto Serif KR', serif"
7
- };
8
- return `${weight} ${size}px ${familyMap[family]}`;
2
+ var DEFAULT_SERIF_STACK = "'Noto Serif KR', 'NanumMyeongjo', serif";
3
+ var DEFAULT_SANS_STACK = "'Noto Sans KR', sans-serif";
4
+ var CUSTOM_FALLBACK_STACK = "'Noto Serif KR', serif";
5
+ function fontStackOf(fonts, role) {
6
+ if (role === "custom") return fonts.customFont || CUSTOM_FALLBACK_STACK;
7
+ if (role === "sans") return fonts.fontStack?.sans || DEFAULT_SANS_STACK;
8
+ return fonts.fontStack?.serif || DEFAULT_SERIF_STACK;
9
+ }
10
+ function sansFont(fonts) {
11
+ return fontStackOf(fonts, "sans");
12
+ }
13
+ function getFont(size, fonts, weight = "normal", role = fonts.fontFamily ?? "serif") {
14
+ return `${weight} ${size}px ${fontStackOf(fonts, role)}`;
9
15
  }
10
16
  function clearCanvas(ctx, w, h) {
11
17
  ctx.fillStyle = "#fff";
@@ -53,7 +59,6 @@ function autoRange(values, maxTicks = 8) {
53
59
  }
54
60
 
55
61
  // src/core/canvas/labels.ts
56
- var LABEL_FONT = "'Noto Sans KR', sans-serif";
57
62
  var MIN_SHRINK = 0.6;
58
63
  function fitFontSize(ctx, text, fontSize, available, makeFont) {
59
64
  if (available <= 0) return fontSize;
@@ -63,11 +68,12 @@ function fitFontSize(ctx, text, fontSize, available, makeFont) {
63
68
  const scaled = Math.max(fontSize * MIN_SHRINK, fontSize * (available / w));
64
69
  return Math.floor(scaled);
65
70
  }
66
- function drawTitle({ ctx, plotX, plotW, title, fontSize, canvasWidth }) {
71
+ function drawTitle({ ctx, plotX, plotW, title, fontSize, fonts, canvasWidth }) {
67
72
  if (!title) return;
68
73
  ctx.save();
69
74
  ctx.fillStyle = "#000";
70
- const makeFont = (size2) => `bold ${size2}px ${LABEL_FONT}`;
75
+ const labelFont = sansFont(fonts);
76
+ const makeFont = (size2) => `bold ${size2}px ${labelFont}`;
71
77
  const centerX = plotX + plotW / 2;
72
78
  const available = canvasWidth != null ? Math.max(0, Math.min(centerX, canvasWidth - centerX) * 2 - 12) : plotW;
73
79
  const size = fitFontSize(ctx, title, fontSize, available, makeFont);
@@ -88,9 +94,11 @@ function drawSourceAndFootnote({
88
94
  sourceInline,
89
95
  footnotes,
90
96
  fontSize,
97
+ fonts,
91
98
  canvasWidth
92
99
  }) {
93
100
  ctx.save();
101
+ const labelFont = sansFont(fonts);
94
102
  let leftX = plotX;
95
103
  let rightX = plotX + plotW;
96
104
  if (canvasWidth != null) {
@@ -105,7 +113,7 @@ function drawSourceAndFootnote({
105
113
  let y = height - 6 - totalFootnoteH - (sourceBelow ? sourceH : 0);
106
114
  const rightEdge = canvasWidth != null ? rightX : plotX + plotW;
107
115
  const available = Math.max(0, rightEdge - leftX);
108
- const sourceFont = (size) => `bold ${size}px ${LABEL_FONT}`;
116
+ const sourceFont = (size) => `bold ${size}px ${labelFont}`;
109
117
  if (source && !sourceBelow && !inlineSource) {
110
118
  ctx.fillStyle = "#555";
111
119
  const size = fitFontSize(ctx, source, fontSize, available, sourceFont);
@@ -122,7 +130,7 @@ function drawSourceAndFootnote({
122
130
  for (let i = 0; i < filtered.length; i++) {
123
131
  const text = "* " + filtered[i];
124
132
  ctx.fillStyle = "#555";
125
- const makeFont = (size2) => `${size2}px ${LABEL_FONT}`;
133
+ const makeFont = (size2) => `${size2}px ${labelFont}`;
126
134
  const size = fitFontSize(ctx, text, fontSize * 0.9, footnoteAvailable, makeFont);
127
135
  ctx.font = makeFont(size);
128
136
  ctx.textAlign = "left";
@@ -140,7 +148,7 @@ function drawSourceAndFootnote({
140
148
  ctx.fillText(source, rightX, lastFootnoteY, inlineSourceW > 0 ? inlineSourceW : void 0);
141
149
  }
142
150
  if (sourceBelow) {
143
- const makeFont = (size) => `bold ${size}px ${LABEL_FONT}`;
151
+ const makeFont = (size) => `bold ${size}px ${labelFont}`;
144
152
  const half = available > 0 ? available / 2 : 0;
145
153
  ctx.fillStyle = "#555";
146
154
  ctx.textBaseline = "bottom";
@@ -418,8 +426,7 @@ function drawYAxis({
418
426
  step,
419
427
  label,
420
428
  side,
421
- fontFamily,
422
- customFont,
429
+ fonts,
423
430
  tickFontSize,
424
431
  labelFontSize,
425
432
  drawGrid = false
@@ -433,7 +440,7 @@ function drawYAxis({
433
440
  ctx.lineTo(x, plot.y + plot.h);
434
441
  ctx.stroke();
435
442
  ctx.fillStyle = "#000";
436
- ctx.font = getFont(tickFontSize, fontFamily, customFont, "bold");
443
+ ctx.font = getFont(tickFontSize, fonts, "bold");
437
444
  ctx.textBaseline = "middle";
438
445
  ctx.textAlign = side === "left" ? "right" : "left";
439
446
  const ticks = [];
@@ -475,7 +482,7 @@ function drawYAxis({
475
482
  }
476
483
  if (label) {
477
484
  ctx.save();
478
- const makeFont = (size) => getFont(size, fontFamily, customFont, "bold");
485
+ const makeFont = (size) => getFont(size, fonts, "bold");
479
486
  ctx.fillStyle = "#000";
480
487
  ctx.textBaseline = "bottom";
481
488
  ctx.textAlign = side === "left" ? "right" : "left";
@@ -493,8 +500,7 @@ function drawXAxis({
493
500
  height,
494
501
  labels,
495
502
  indices,
496
- fontFamily,
497
- customFont,
503
+ fonts,
498
504
  tickFontSize
499
505
  }) {
500
506
  const plot = plotArea(padding, width, height);
@@ -506,7 +512,7 @@ function drawXAxis({
506
512
  ctx.lineTo(plot.x + plot.w, y);
507
513
  ctx.stroke();
508
514
  ctx.fillStyle = "#000";
509
- ctx.font = getFont(tickFontSize, fontFamily, customFont, "bold");
515
+ ctx.font = getFont(tickFontSize, fonts, "bold");
510
516
  ctx.textAlign = "center";
511
517
  ctx.textBaseline = "top";
512
518
  const showIndices = indices ?? labels.map((_, i) => i);
@@ -613,14 +619,17 @@ function drawInsideIcon(ctx, item, x, cy, size) {
613
619
  ctx.lineTo(x + w, cy);
614
620
  ctx.stroke();
615
621
  ctx.setLineDash([]);
616
- ctx.fillStyle = item.fillStyle;
622
+ ctx.fillStyle = item.hollow ? "#fff" : item.fillStyle;
623
+ if (item.hollow) ctx.lineWidth = 1.5;
617
624
  const r = size * 0.3;
618
625
  if (item.marker === "square") {
619
626
  ctx.fillRect(x + w / 2 - r, cy - r, r * 2, r * 2);
627
+ if (item.hollow) ctx.strokeRect(x + w / 2 - r, cy - r, r * 2, r * 2);
620
628
  } else {
621
629
  ctx.beginPath();
622
630
  ctx.arc(x + w / 2, cy, r, 0, Math.PI * 2);
623
631
  ctx.fill();
632
+ if (item.hollow) ctx.stroke();
624
633
  }
625
634
  return;
626
635
  }
@@ -631,14 +640,13 @@ function drawInsideIcon(ctx, item, x, cy, size) {
631
640
  ctx.strokeRect(x, cy - size / 2, size, size);
632
641
  }
633
642
  var LINE_ICON_SIZE = 36;
634
- var LEGEND_FONT = "'Noto Sans KR', sans-serif";
635
643
  var ICON_GAP = 10;
636
644
  var BOX_PADDING = 12;
637
645
  var ITEM_SPACING = 30;
638
646
  var ROW_GAP = 6;
639
- function measureLegendWidth(ctx, labels, fontSize, iconType = "rect") {
647
+ function measureLegendWidth(ctx, labels, fontSize, fonts, iconType = "rect") {
640
648
  ctx.save();
641
- ctx.font = `bold ${fontSize}px ${LEGEND_FONT}`;
649
+ ctx.font = `bold ${fontSize}px ${sansFont(fonts)}`;
642
650
  const iconSize = iconType === "line" ? LINE_ICON_SIZE : 16;
643
651
  const iconGap = 10;
644
652
  const padding = 12;
@@ -646,11 +654,12 @@ function measureLegendWidth(ctx, labels, fontSize, iconType = "rect") {
646
654
  ctx.restore();
647
655
  return maxW + padding * 2 + 30;
648
656
  }
649
- function layoutBottomLegend(ctx, labels, iconWidths, fontSize, boxW, opts = {}) {
657
+ function layoutBottomLegend(ctx, labels, iconWidths, fontSize, boxW, fonts, opts = {}) {
650
658
  const iconGap = opts.iconGap ?? ICON_GAP;
651
659
  const padding = opts.padding ?? BOX_PADDING;
652
660
  const spacing = opts.spacing ?? ITEM_SPACING;
653
- const fontOf = (fs2) => opts.font ? withFontSize(opts.font, fs2) : `bold ${fs2}px ${LEGEND_FONT}`;
661
+ const legendFont = sansFont(fonts);
662
+ const fontOf = (fs2) => opts.font ? withFontSize(opts.font, fs2) : `bold ${fs2}px ${legendFont}`;
654
663
  const inner = Math.max(1, boxW - padding * 2);
655
664
  ctx.save();
656
665
  const widthsAt = (fs2) => {
@@ -687,11 +696,11 @@ function layoutBottomLegend(ctx, labels, iconWidths, fontSize, boxW, opts = {})
687
696
  boxH: rows.length * lineHeight + (rows.length - 1) * ROW_GAP + padding * 2
688
697
  };
689
698
  }
690
- function measureBottomLegend(ctx, labels, fontSize, plotW, iconType = "rect", bottomOffset = 50) {
699
+ function measureBottomLegend(ctx, labels, fontSize, plotW, fonts, iconType = "rect", bottomOffset = 50) {
691
700
  if (labels.length === 0) return 0;
692
701
  const sizeOf = (t) => t === "line" ? LINE_ICON_SIZE : 16;
693
702
  const iconWidths = labels.map((_, i) => sizeOf(Array.isArray(iconType) ? iconType[i] ?? "rect" : iconType));
694
- const { boxH } = layoutBottomLegend(ctx, labels, iconWidths, fontSize, plotW);
703
+ const { boxH } = layoutBottomLegend(ctx, labels, iconWidths, fontSize, plotW, fonts);
695
704
  return bottomOffset + boxH + 2;
696
705
  }
697
706
  function drawLegend({
@@ -705,12 +714,14 @@ function drawLegend({
705
714
  canvasW,
706
715
  canvasH,
707
716
  fontSize,
717
+ fonts,
708
718
  bottomOffset = 50,
709
719
  rightGap = 20
710
720
  }) {
711
721
  if (items.length === 0) return 0;
722
+ const legendFont = sansFont(fonts);
712
723
  ctx.save();
713
- ctx.font = `bold ${fontSize}px ${LEGEND_FONT}`;
724
+ ctx.font = `bold ${fontSize}px ${legendFont}`;
714
725
  const iconGap = ICON_GAP;
715
726
  const padding = BOX_PADDING;
716
727
  const lineHeight = fontSize + 8;
@@ -723,7 +734,8 @@ function drawLegend({
723
734
  items.map((i) => i.label),
724
735
  items.map(iconWidthOf),
725
736
  fontSize,
726
- boxW
737
+ boxW,
738
+ fonts
727
739
  );
728
740
  const boxH = layout.boxH;
729
741
  const boxX = plotX;
@@ -744,7 +756,7 @@ function drawLegend({
744
756
  const item = items[index];
745
757
  const iSize = iconWidthOf(item);
746
758
  drawIcon(ctx, item, cx, cy, iSize);
747
- ctx.font = `bold ${layout.fontSize}px ${LEGEND_FONT}`;
759
+ ctx.font = `bold ${layout.fontSize}px ${legendFont}`;
748
760
  ctx.fillStyle = "#000";
749
761
  ctx.textAlign = "left";
750
762
  ctx.textBaseline = "middle";
@@ -773,7 +785,7 @@ function drawLegend({
773
785
  const ix = boxX + padding;
774
786
  const iSize = iconWidthOf(item);
775
787
  drawIcon(ctx, item, ix, cy, iSize);
776
- ctx.font = `bold ${fontSize}px ${LEGEND_FONT}`;
788
+ ctx.font = `bold ${fontSize}px ${legendFont}`;
777
789
  ctx.fillStyle = "#000";
778
790
  ctx.textAlign = "left";
779
791
  ctx.textBaseline = "middle";
@@ -974,10 +986,8 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
974
986
  clearCanvas(ctx, w, h);
975
987
  const showLegend = options.showLegend;
976
988
  const legendPos = options.legendPosition;
977
- const legendW = showLegend && legendPos === "right" && !data.insideLegend ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
989
+ const legendW = showLegend && legendPos === "right" && !data.insideLegend ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
978
990
  const isVertical = data.barDirection === "vertical";
979
- const font = options.fontFamily;
980
- const customFont = options.customFont;
981
991
  const n = data.categories.length;
982
992
  const sCount = data.seriesLabels.length;
983
993
  const hasGroups = !isVertical && !!data.groups && data.groups.length > 0;
@@ -986,18 +996,18 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
986
996
  let unitW = 0;
987
997
  if (hasGroups || data.unitAdjacent || data.insideLegend) {
988
998
  ctx.save();
989
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
999
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
990
1000
  if (hasGroups) {
991
1001
  groupLabelW = widestLabel(ctx, data.groups.map((g) => g.label));
992
1002
  catLabelW = widestLabel(ctx, data.categories.map((c) => c.label));
993
1003
  }
994
- ctx.font = getFont(options.fontSize.axisLabel, font, customFont, "bold");
1004
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
995
1005
  unitW = ctx.measureText(data.unit).width;
996
1006
  ctx.restore();
997
1007
  }
998
1008
  const padRight = isVertical ? 60 + legendW : data.unitAdjacent ? 40 + unitW : 160 + legendW;
999
1009
  const padLeft = isVertical ? 130 : hasGroups ? 20 + groupLabelW + 14 + catLabelW + 12 : 100;
1000
- const legendReserve = showLegend && legendPos === "bottom" && !data.insideLegend ? measureBottomLegend(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, w - padLeft - padRight) : 0;
1010
+ const legendReserve = showLegend && legendPos === "bottom" && !data.insideLegend ? measureBottomLegend(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, w - padLeft - padRight, options) : 0;
1001
1011
  const padding = {
1002
1012
  top: options.title ? 100 : 50,
1003
1013
  right: padRight,
@@ -1061,8 +1071,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1061
1071
  step: axis.step,
1062
1072
  label: data.unit,
1063
1073
  side: "left",
1064
- fontFamily: font,
1065
- customFont,
1074
+ fonts: options,
1066
1075
  tickFontSize: options.fontSize.tick,
1067
1076
  labelFontSize: options.fontSize.axisLabel,
1068
1077
  drawGrid: true
@@ -1092,7 +1101,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1092
1101
  ctx.strokeRect(cx - barW / 2, y, barW, barH);
1093
1102
  if (options.showDataLabels && barH > options.fontSize.dataLabel) {
1094
1103
  ctx.fillStyle = isLightFill(s) ? "#000" : "#fff";
1095
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
1104
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
1096
1105
  ctx.textAlign = "center";
1097
1106
  ctx.textBaseline = "middle";
1098
1107
  ctx.fillText(String(val), cx, y + barH / 2);
@@ -1118,7 +1127,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1118
1127
  ctx.strokeRect(bx, by, barW, barH);
1119
1128
  if (options.showDataLabels && barH > options.fontSize.dataLabel) {
1120
1129
  ctx.fillStyle = isLightFill(s) ? "#000" : "#fff";
1121
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
1130
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
1122
1131
  ctx.textAlign = "center";
1123
1132
  ctx.textBaseline = "bottom";
1124
1133
  ctx.fillText(String(val), bx + barW / 2, by - 4);
@@ -1138,7 +1147,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1138
1147
  }
1139
1148
  const labelY = data.categoryLabelAtBaseline ? baseY + 6 : plotY + plotH + 12;
1140
1149
  ctx.fillStyle = "#000";
1141
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
1150
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
1142
1151
  ctx.textAlign = "center";
1143
1152
  ctx.textBaseline = "top";
1144
1153
  const catStride = labelStride(catArea, widestLabel(ctx, data.categories.map((c) => c.label)));
@@ -1162,7 +1171,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1162
1171
  if (ticks.length === 0 || Math.abs(ticks[ticks.length - 1] - axis.max) > 1e-9) {
1163
1172
  ticks.push(axis.max);
1164
1173
  }
1165
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
1174
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
1166
1175
  ctx.fillStyle = "#000";
1167
1176
  ctx.textAlign = "center";
1168
1177
  ctx.textBaseline = "top";
@@ -1190,7 +1199,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1190
1199
  ctx.restore();
1191
1200
  }
1192
1201
  }
1193
- ctx.font = getFont(options.fontSize.axisLabel, font, customFont, "bold");
1202
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
1194
1203
  ctx.fillStyle = "#000";
1195
1204
  ctx.textAlign = "left";
1196
1205
  ctx.textBaseline = "top";
@@ -1230,7 +1239,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1230
1239
  ctx.strokeRect(bx, cy - barH / 2, bw, barH);
1231
1240
  if (options.showDataLabels && bw > options.fontSize.dataLabel * 2) {
1232
1241
  ctx.fillStyle = isLightFill(s) ? "#000" : "#fff";
1233
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
1242
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
1234
1243
  ctx.textAlign = "center";
1235
1244
  ctx.textBaseline = "middle";
1236
1245
  ctx.fillText(String(val), bx + bw / 2, cy);
@@ -1256,7 +1265,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1256
1265
  ctx.strokeRect(bx, by, bw, barH);
1257
1266
  if (options.showDataLabels && bw > options.fontSize.dataLabel * 2) {
1258
1267
  ctx.fillStyle = isLightFill(s) ? "#000" : "#fff";
1259
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
1268
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
1260
1269
  ctx.textAlign = "left";
1261
1270
  ctx.textBaseline = "middle";
1262
1271
  ctx.fillText(String(val), bx + bw + 4, by + barH / 2);
@@ -1268,7 +1277,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1268
1277
  const catLabelX = hasGroups ? plotX - 12 : plotX - 10;
1269
1278
  for (let c = 0; c < n; c++) {
1270
1279
  ctx.fillStyle = "#000";
1271
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
1280
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
1272
1281
  ctx.textAlign = "right";
1273
1282
  ctx.textBaseline = "middle";
1274
1283
  ctx.fillText(data.categories[c].label, catLabelX, centerY[c]);
@@ -1282,7 +1291,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1282
1291
  }
1283
1292
  }
1284
1293
  }
1285
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
1294
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
1286
1295
  if (showLegend && data.insideLegend) {
1287
1296
  drawInsideLegend({
1288
1297
  ctx,
@@ -1299,7 +1308,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1299
1308
  canvasW: w,
1300
1309
  canvasH: h,
1301
1310
  fontSize: options.fontSize.dataLabel * 0.9,
1302
- font: getFont(options.fontSize.dataLabel * 0.9, font, customFont, "bold"),
1311
+ font: getFont(options.fontSize.dataLabel * 0.9, options, "bold"),
1303
1312
  avoid: barRects
1304
1313
  });
1305
1314
  } else if (showLegend) {
@@ -1312,6 +1321,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1312
1321
  }));
1313
1322
  drawLegend({
1314
1323
  ctx,
1324
+ fonts: options,
1315
1325
  items,
1316
1326
  position: legendPos,
1317
1327
  plotX,
@@ -1323,7 +1333,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1323
1333
  fontSize: options.fontSize.dataLabel * 0.85 + 5
1324
1334
  });
1325
1335
  }
1326
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
1336
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
1327
1337
  }
1328
1338
  function formatTick2(val) {
1329
1339
  if (Number.isInteger(val)) return val.toString();
@@ -1338,6 +1348,7 @@ function createDefaultGraphOptions() {
1338
1348
  footnotes: [""],
1339
1349
  fontFamily: "serif",
1340
1350
  customFont: "",
1351
+ fontStack: {},
1341
1352
  fontSize: {
1342
1353
  title: 36,
1343
1354
  axisLabel: 28,
@@ -1520,6 +1531,60 @@ function createDefaultCubeData() {
1520
1531
  };
1521
1532
  }
1522
1533
 
1534
+ // src/core/types/econplane.ts
1535
+ function createSupplyDemandData() {
1536
+ return {
1537
+ quadrants: "first",
1538
+ xAxis: { label: "\uC218\uB7C9(kg)", min: 0, max: 9, ticks: [1, 2, 3, 4, 5, 6, 7, 8], broken: false },
1539
+ yAxis: { label: "\uAC00\uACA9(\uCC9C \uC6D0)", min: 0, max: 9, ticks: [1, 2, 3, 4, 5, 6, 7, 8], broken: false },
1540
+ grid: true,
1541
+ dash: "dashed",
1542
+ lines: [
1543
+ { label: "\uC218\uC694", from: { x: 0, y: 8 }, to: { x: 8, y: 0 }, labelAt: "to" },
1544
+ { label: "\uACF5\uAE09", from: { x: 0, y: 0 }, to: { x: 8, y: 8 }, labelAt: "to" }
1545
+ ],
1546
+ points: [{ x: 4, y: 4, label: "E", labelPos: "top", guide: "none", dot: true }],
1547
+ arrows: []
1548
+ };
1549
+ }
1550
+ function createAdAsData() {
1551
+ return {
1552
+ quadrants: "first",
1553
+ xAxis: { label: "\uC2E4\uC9C8 GDP", min: 0, max: 10, ticks: [], broken: false },
1554
+ yAxis: { label: "\uBB3C\uAC00", min: 0, max: 10, ticks: [], broken: false },
1555
+ grid: false,
1556
+ dash: "dashed",
1557
+ lines: [
1558
+ { label: "\uCD1D\uC218\uC694", from: { x: 1, y: 8.5 }, to: { x: 8.5, y: 1 }, labelAt: "to" },
1559
+ { label: "\uCD1D\uACF5\uAE09", from: { x: 1, y: 1 }, to: { x: 8.5, y: 8.5 }, labelAt: "to" }
1560
+ ],
1561
+ points: [{ x: 4.75, y: 4.75, label: "E", labelPos: "right", guide: "none", dot: true }],
1562
+ arrows: []
1563
+ };
1564
+ }
1565
+ function createPointShiftData() {
1566
+ return {
1567
+ quadrants: "first",
1568
+ xAxis: { label: "\uC218\uB7C9(\uB9CC \uAC1C)", min: 0, max: 18, ticks: [5, 10, 15], broken: false },
1569
+ yAxis: { label: "\uAC00\uACA9(\uB2EC\uB7EC)", min: 0, max: 18, ticks: [10, 15], broken: false },
1570
+ grid: false,
1571
+ dash: "dashed",
1572
+ lines: [],
1573
+ points: [
1574
+ { x: 5, y: 10, label: "E", labelPos: "top", guide: "both", dot: true },
1575
+ { x: 10, y: 10, label: "A", labelPos: "top", guide: "both", dot: true },
1576
+ { x: 15, y: 15, label: "B", labelPos: "top", guide: "both", dot: true }
1577
+ ],
1578
+ arrows: [
1579
+ { from: { x: 5, y: 10 }, to: { x: 10, y: 10 }, offset: 0, shorten: 10, label: "", labelPos: "top" },
1580
+ { from: { x: 10, y: 10 }, to: { x: 15, y: 15 }, offset: 0, shorten: 10, label: "", labelPos: "top" }
1581
+ ]
1582
+ };
1583
+ }
1584
+ function createDefaultEconPlaneData() {
1585
+ return createSupplyDemandData();
1586
+ }
1587
+
1523
1588
  // src/core/types/radar.ts
1524
1589
  function createDefaultRadarData() {
1525
1590
  return {
@@ -1640,15 +1705,13 @@ function drawMarker(ctx, marker, cx, cy, r) {
1640
1705
  }
1641
1706
  function renderCategoryDotGraph(ctx, w, h, data, options) {
1642
1707
  clearCanvas(ctx, w, h);
1643
- const font = options.fontFamily;
1644
- const customFont = options.customFont;
1645
1708
  const n = data.categories.length;
1646
1709
  const sCount = data.seriesLabels.length;
1647
1710
  const markers = data.seriesMarkers ?? DOT_MARKER_ORDER;
1648
1711
  const showLegend = options.showLegend && sCount > 1;
1649
1712
  const legendPos = options.legendPosition;
1650
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, "circle") : 0;
1651
- const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85, w - 130 - (60 + legendW), "circle") : 0;
1713
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, options, "circle") : 0;
1714
+ const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85, w - 130 - (60 + legendW), options, "circle") : 0;
1652
1715
  const padding = {
1653
1716
  top: options.title ? 100 : 50,
1654
1717
  right: 60 + legendW,
@@ -1693,8 +1756,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1693
1756
  step: axis.step,
1694
1757
  label: data.unit,
1695
1758
  side: "left",
1696
- fontFamily: font,
1697
- customFont,
1759
+ fonts: options,
1698
1760
  tickFontSize: options.fontSize.tick,
1699
1761
  labelFontSize: options.fontSize.axisLabel,
1700
1762
  drawGrid: true
@@ -1717,7 +1779,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1717
1779
  ctx.fillStyle = "#000";
1718
1780
  drawMarker(ctx, markers[s % markers.length], cx, cy, data.dotRadius);
1719
1781
  if (options.showDataLabels) {
1720
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
1782
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
1721
1783
  ctx.textAlign = "center";
1722
1784
  ctx.textBaseline = "bottom";
1723
1785
  ctx.fillText(String(val), cx, cy - data.dotRadius - 4);
@@ -1738,7 +1800,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1738
1800
  }
1739
1801
  }
1740
1802
  ctx.fillStyle = "#000";
1741
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
1803
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
1742
1804
  ctx.textAlign = "center";
1743
1805
  ctx.textBaseline = "top";
1744
1806
  for (let c = 0; c < n; c++) {
@@ -1746,7 +1808,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1746
1808
  ctx.fillText(data.categories[c].label, cx, plotY + plotH + 12);
1747
1809
  }
1748
1810
  if (options.title) {
1749
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title });
1811
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title });
1750
1812
  }
1751
1813
  if (showLegend) {
1752
1814
  const items = data.seriesLabels.map((label) => ({
@@ -1756,6 +1818,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1756
1818
  }));
1757
1819
  drawLegend({
1758
1820
  ctx,
1821
+ fonts: options,
1759
1822
  items,
1760
1823
  position: legendPos,
1761
1824
  plotX,
@@ -1769,6 +1832,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1769
1832
  }
1770
1833
  drawSourceAndFootnote({
1771
1834
  ctx,
1835
+ fonts: options,
1772
1836
  plotX,
1773
1837
  plotW,
1774
1838
  height: h,
@@ -1812,15 +1876,13 @@ function drawMarker2(ctx, marker, cx, cy, r, hollow) {
1812
1876
  }
1813
1877
  function renderLineGraph(ctx, w, h, data, options) {
1814
1878
  clearCanvas(ctx, w, h);
1815
- const font = options.fontFamily;
1816
- const customFont = options.customFont;
1817
1879
  const n = data.xLabels.length;
1818
1880
  const useLegend = data.labelPlacement === "legend" && options.showLegend;
1819
1881
  const legendPos = options.legendPosition;
1820
- const legendW = useLegend && legendPos === "right" ? measureLegendWidth(ctx, data.series.map((s) => s.label), options.fontSize.dataLabel * 0.85 + 5, "line") : 0;
1821
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
1882
+ const legendW = useLegend && legendPos === "right" ? measureLegendWidth(ctx, data.series.map((s) => s.label), options.fontSize.dataLabel * 0.85 + 5, options, "line") : 0;
1883
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
1822
1884
  const endLabelW = data.labelPlacement === "lineEnd" ? widestLabel(ctx, data.series.map((s) => s.label)) + 16 : 0;
1823
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
1885
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
1824
1886
  const xUnitW = data.xUnit ? ctx.measureText(data.xUnit).width + ctx.measureText(data.xLabels[n - 1] ?? "").width / 2 + 12 : 0;
1825
1887
  const padRight = 24 + legendW + Math.max(endLabelW, xUnitW);
1826
1888
  const legendReserve = useLegend && legendPos === "bottom" && !data.insideLegend ? measureBottomLegend(
@@ -1828,6 +1890,7 @@ function renderLineGraph(ctx, w, h, data, options) {
1828
1890
  data.series.map((s) => s.label),
1829
1891
  options.fontSize.dataLabel * 0.85,
1830
1892
  w - 130 - padRight,
1893
+ options,
1831
1894
  "line"
1832
1895
  ) : 0;
1833
1896
  const padding = {
@@ -1876,8 +1939,7 @@ function renderLineGraph(ctx, w, h, data, options) {
1876
1939
  step: axis.step,
1877
1940
  label: data.yUnit,
1878
1941
  side: "left",
1879
- fontFamily: font,
1880
- customFont,
1942
+ fonts: options,
1881
1943
  tickFontSize: options.fontSize.tick,
1882
1944
  labelFontSize: options.fontSize.axisLabel,
1883
1945
  drawGrid: true
@@ -1969,7 +2031,7 @@ function renderLineGraph(ctx, w, h, data, options) {
1969
2031
  }
1970
2032
  if (data.labelPlacement === "lineEnd") {
1971
2033
  ctx.fillStyle = "#000";
1972
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
2034
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
1973
2035
  const placer = new LabelPlacer();
1974
2036
  const lineHeight = options.fontSize.dataLabel * 1.1;
1975
2037
  const columnX = plotX + plotW + 10;
@@ -1999,7 +2061,7 @@ function renderLineGraph(ctx, w, h, data, options) {
1999
2061
  });
2000
2062
  }
2001
2063
  ctx.fillStyle = "#000";
2002
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
2064
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
2003
2065
  ctx.textAlign = "center";
2004
2066
  ctx.textBaseline = "top";
2005
2067
  const xStride = labelStride(stepX, widestLabel(ctx, data.xLabels));
@@ -2013,11 +2075,11 @@ function renderLineGraph(ctx, w, h, data, options) {
2013
2075
  ctx.fillText(data.xUnit, plotX + plotW + lastLabelHalf + 6, plotY + plotH + 10);
2014
2076
  }
2015
2077
  if (options.title) {
2016
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2078
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2017
2079
  }
2018
2080
  if (useLegend && data.insideLegend) {
2019
2081
  ctx.save();
2020
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
2082
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
2021
2083
  const rowH = options.fontSize.dataLabel * 1.15;
2022
2084
  const swatch = rowH * 0.75;
2023
2085
  const boxW = swatch + 6 + widestLabel(ctx, data.series.map((s) => s.label)) + 10;
@@ -2049,6 +2111,7 @@ function renderLineGraph(ctx, w, h, data, options) {
2049
2111
  }));
2050
2112
  drawLegend({
2051
2113
  ctx,
2114
+ fonts: options,
2052
2115
  items,
2053
2116
  position: legendPos,
2054
2117
  plotX,
@@ -2062,6 +2125,7 @@ function renderLineGraph(ctx, w, h, data, options) {
2062
2125
  }
2063
2126
  drawSourceAndFootnote({
2064
2127
  ctx,
2128
+ fonts: options,
2065
2129
  plotX,
2066
2130
  plotW,
2067
2131
  height: h,
@@ -2082,9 +2146,8 @@ function renderMatrixTable(ctx, w, h, data, options) {
2082
2146
  const n = data.names.length;
2083
2147
  if (n === 0) return;
2084
2148
  const font = options.fontFamily;
2085
- const customFont = options.customFont;
2086
2149
  const base = options.fontSize.tick;
2087
- ctx.font = getFont(base, font, customFont, "bold");
2150
+ ctx.font = getFont(base, options, "bold");
2088
2151
  const widest = Math.max(
2089
2152
  ...data.names.map((t) => ctx.measureText(t).width),
2090
2153
  ...cellValues(data).map((v) => ctx.measureText(formatValue(v, data.groupThousands)).width)
@@ -2111,7 +2174,7 @@ function renderMatrixTable(ctx, w, h, data, options) {
2111
2174
  const centered = top + unitH + Math.max(0, (h - top - unitH - footH - tableH) / 2);
2112
2175
  const tableY = Math.max(top + unitH, Math.min(centered, h - footH - tableH));
2113
2176
  if (data.unit) {
2114
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
2177
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
2115
2178
  ctx.fillStyle = "#000";
2116
2179
  ctx.textAlign = "right";
2117
2180
  ctx.textBaseline = "bottom";
@@ -2131,11 +2194,11 @@ function renderMatrixTable(ctx, w, h, data, options) {
2131
2194
  ctx.fillStyle = "#000";
2132
2195
  if (isName) {
2133
2196
  const family = data.nameIsSymbol && !data.nameIsSymbol[i] ? "sans" : font;
2134
- ctx.font = getFont(cellFontSize, family, customFont, "bold");
2197
+ ctx.font = getFont(cellFontSize, options, "bold", family);
2135
2198
  ctx.textAlign = "center";
2136
2199
  ctx.fillText(data.names[i], x + cellW / 2, y + cellH / 2);
2137
2200
  } else {
2138
- ctx.font = getFont(cellFontSize, font, customFont, "bold");
2201
+ ctx.font = getFont(cellFontSize, options, "bold");
2139
2202
  ctx.textAlign = "right";
2140
2203
  ctx.fillText(
2141
2204
  formatValue(data.values[i]?.[j] ?? 0, data.groupThousands),
@@ -2145,9 +2208,10 @@ function renderMatrixTable(ctx, w, h, data, options) {
2145
2208
  }
2146
2209
  }
2147
2210
  }
2148
- drawTitle({ ctx, plotX: tableX, plotW: tableW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2211
+ drawTitle({ ctx, fonts: options, plotX: tableX, plotW: tableW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2149
2212
  drawSourceAndFootnote({
2150
2213
  ctx,
2214
+ fonts: options,
2151
2215
  plotX: tableX,
2152
2216
  plotW: tableW,
2153
2217
  height: h,
@@ -2181,12 +2245,11 @@ function renderDataTable(ctx, w, h, data, options) {
2181
2245
  const rows = data.rows.length;
2182
2246
  if (cols === 0 || rows === 0) return;
2183
2247
  const font = options.fontFamily;
2184
- const customFont = options.customFont;
2185
2248
  const base = options.fontSize.tick;
2186
- ctx.font = getFont(base, font, customFont, "bold");
2249
+ ctx.font = getFont(base, options, "bold");
2187
2250
  const labelTextW = Math.max(
2188
2251
  ctx.measureText(data.cornerLabel).width,
2189
- ...data.rows.map((r) => rowLabelWidth(ctx, r, base, font, customFont))
2252
+ ...data.rows.map((r) => rowLabelWidth(ctx, r, base, options))
2190
2253
  );
2191
2254
  const valueTextW = Math.max(
2192
2255
  ...data.columns.map((c) => ctx.measureText(c).width),
@@ -2221,17 +2284,17 @@ function renderDataTable(ctx, w, h, data, options) {
2221
2284
  ctx.fillStyle = "#000";
2222
2285
  ctx.textBaseline = "middle";
2223
2286
  ctx.textAlign = "center";
2224
- ctx.font = getFont(cellFontSize, font, customFont, "bold");
2287
+ ctx.font = getFont(cellFontSize, options, "bold");
2225
2288
  ctx.fillText(data.cornerLabel, tableX + labelW / 2, tableY + cellH / 2, labelW - pad * 2);
2226
2289
  data.columns.forEach((name, j) => {
2227
2290
  const family = data.columnIsSymbol && !data.columnIsSymbol[j] ? "sans" : font;
2228
- ctx.font = getFont(cellFontSize, family, customFont, "bold");
2291
+ ctx.font = getFont(cellFontSize, options, "bold", family);
2229
2292
  ctx.fillText(name, columnX(j) + valueW / 2, tableY + cellH / 2, valueW - pad * 2);
2230
2293
  });
2231
2294
  data.rows.forEach((row, i) => {
2232
2295
  const y = tableY + cellH * (i + 1);
2233
- drawRowLabel(ctx, row, tableX + labelW / 2, y + cellH / 2, cellFontSize, font, customFont);
2234
- ctx.font = getFont(cellFontSize, font, customFont, "bold");
2296
+ drawRowLabel(ctx, row, tableX + labelW / 2, y + cellH / 2, cellFontSize, options);
2297
+ ctx.font = getFont(cellFontSize, options, "bold");
2235
2298
  ctx.textAlign = "right";
2236
2299
  for (let j = 0; j < cols; j++) {
2237
2300
  const v = row.values[j];
@@ -2257,9 +2320,10 @@ function renderDataTable(ctx, w, h, data, options) {
2257
2320
  }
2258
2321
  ctx.lineWidth = 2;
2259
2322
  ctx.strokeRect(tableX, tableY, tableW, tableH);
2260
- drawTitle({ ctx, plotX: tableX, plotW: tableW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2323
+ drawTitle({ ctx, fonts: options, plotX: tableX, plotW: tableW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2261
2324
  drawSourceAndFootnote({
2262
2325
  ctx,
2326
+ fonts: options,
2263
2327
  plotX: tableX,
2264
2328
  plotW: tableW,
2265
2329
  height: h,
@@ -2275,30 +2339,30 @@ function line(ctx, x1, y1, x2, y2) {
2275
2339
  ctx.lineTo(x2, y2);
2276
2340
  ctx.stroke();
2277
2341
  }
2278
- function rowLabelWidth(ctx, row, size, font, customFont) {
2279
- ctx.font = getFont(size, font, customFont, "bold");
2342
+ function rowLabelWidth(ctx, row, size, options) {
2343
+ ctx.font = getFont(size, options, "bold");
2280
2344
  const labelW = ctx.measureText(row.label).width;
2281
2345
  if (!row.unit) return labelW;
2282
- ctx.font = getFont(size * UNIT_RATIO, font, customFont, "bold");
2346
+ ctx.font = getFont(size * UNIT_RATIO, options, "bold");
2283
2347
  return labelW + ctx.measureText(row.unit).width;
2284
2348
  }
2285
- function drawRowLabel(ctx, row, cx, cy, size, font, customFont) {
2349
+ function drawRowLabel(ctx, row, cx, cy, size, options) {
2286
2350
  ctx.fillStyle = "#000";
2287
2351
  ctx.textBaseline = "middle";
2288
- ctx.font = getFont(size, font, customFont, "bold");
2352
+ ctx.font = getFont(size, options, "bold");
2289
2353
  const labelW = ctx.measureText(row.label).width;
2290
2354
  if (!row.unit) {
2291
2355
  ctx.textAlign = "center";
2292
2356
  ctx.fillText(row.label, cx, cy);
2293
2357
  return;
2294
2358
  }
2295
- ctx.font = getFont(size * UNIT_RATIO, font, customFont, "bold");
2359
+ ctx.font = getFont(size * UNIT_RATIO, options, "bold");
2296
2360
  const unitW = ctx.measureText(row.unit).width;
2297
2361
  const startX = cx - (labelW + unitW) / 2;
2298
2362
  ctx.textAlign = "left";
2299
- ctx.font = getFont(size, font, customFont, "bold");
2363
+ ctx.font = getFont(size, options, "bold");
2300
2364
  ctx.fillText(row.label, startX, cy);
2301
- ctx.font = getFont(size * UNIT_RATIO, font, customFont, "bold");
2365
+ ctx.font = getFont(size * UNIT_RATIO, options, "bold");
2302
2366
  ctx.fillText(row.unit, startX + labelW, cy);
2303
2367
  ctx.textAlign = "center";
2304
2368
  }
@@ -2311,6 +2375,571 @@ function formatValue2(v, decimals = 1, groupThousands = true) {
2311
2375
  return frac ? `${grouped}.${frac}` : grouped;
2312
2376
  }
2313
2377
 
2378
+ // src/core/canvas/subscript.ts
2379
+ var SUB_SCALE = 0.65;
2380
+ var SUB_DROP = 0.16;
2381
+ var SUB_CHARS = /[0-9A-Za-z]/;
2382
+ var HAS_SUB = /_[0-9A-Za-z]/;
2383
+ function parseRich(text) {
2384
+ if (!HAS_SUB.test(text)) return [{ text, sub: false }];
2385
+ const runs = [];
2386
+ let plain = "";
2387
+ for (let i = 0; i < text.length; i++) {
2388
+ if (text[i] === "_" && i + 1 < text.length && SUB_CHARS.test(text[i + 1])) {
2389
+ let j = i + 1;
2390
+ while (j < text.length && SUB_CHARS.test(text[j])) j++;
2391
+ if (plain) {
2392
+ runs.push({ text: plain, sub: false });
2393
+ plain = "";
2394
+ }
2395
+ runs.push({ text: text.slice(i + 1, j), sub: true });
2396
+ i = j - 1;
2397
+ continue;
2398
+ }
2399
+ plain += text[i];
2400
+ }
2401
+ if (plain) runs.push({ text: plain, sub: false });
2402
+ return runs;
2403
+ }
2404
+ function isPlain(runs) {
2405
+ return runs.length === 1 && !runs[0].sub;
2406
+ }
2407
+ function richWidth(ctx, text, size, makeFont) {
2408
+ const runs = parseRich(text);
2409
+ const base = makeFont(size);
2410
+ if (isPlain(runs)) {
2411
+ ctx.font = base;
2412
+ return ctx.measureText(text).width;
2413
+ }
2414
+ const sub = makeFont(size * SUB_SCALE);
2415
+ let w = 0;
2416
+ for (const r of runs) {
2417
+ ctx.font = r.sub ? sub : base;
2418
+ w += ctx.measureText(r.text).width;
2419
+ }
2420
+ ctx.font = base;
2421
+ return w;
2422
+ }
2423
+ function richExtent(ctx, text, size, makeFont) {
2424
+ const runs = parseRich(text);
2425
+ const base = makeFont(size);
2426
+ if (isPlain(runs)) {
2427
+ ctx.font = base;
2428
+ return textExtent(ctx, text);
2429
+ }
2430
+ const subFont = makeFont(size * SUB_SCALE);
2431
+ const drop = size * SUB_DROP;
2432
+ let w = 0;
2433
+ let up = 0;
2434
+ let down = 0;
2435
+ for (const r of runs) {
2436
+ ctx.font = r.sub ? subFont : base;
2437
+ const e = textExtent(ctx, r.text);
2438
+ w += e.left + e.right;
2439
+ up = Math.max(up, r.sub ? e.up - drop : e.up);
2440
+ down = Math.max(down, r.sub ? e.down + drop : e.down);
2441
+ }
2442
+ ctx.font = base;
2443
+ const left = ctx.textAlign === "right" || ctx.textAlign === "end" ? w : ctx.textAlign === "center" ? w / 2 : 0;
2444
+ return { left, right: w - left, up, down };
2445
+ }
2446
+ function fillRich(ctx, text, x, y, size, makeFont) {
2447
+ const runs = parseRich(text);
2448
+ const base = makeFont(size);
2449
+ if (isPlain(runs)) {
2450
+ ctx.font = base;
2451
+ ctx.fillText(text, x, y);
2452
+ return;
2453
+ }
2454
+ const subFont = makeFont(size * SUB_SCALE);
2455
+ const drop = size * SUB_DROP;
2456
+ let total = 0;
2457
+ for (const r of runs) {
2458
+ ctx.font = r.sub ? subFont : base;
2459
+ total += ctx.measureText(r.text).width;
2460
+ }
2461
+ const align = ctx.textAlign;
2462
+ let cx = align === "right" || align === "end" ? x - total : align === "center" ? x - total / 2 : x;
2463
+ ctx.textAlign = "left";
2464
+ for (const r of runs) {
2465
+ ctx.font = r.sub ? subFont : base;
2466
+ ctx.fillText(r.text, cx, r.sub ? y + drop : y);
2467
+ cx += ctx.measureText(r.text).width;
2468
+ }
2469
+ ctx.textAlign = align;
2470
+ ctx.font = base;
2471
+ }
2472
+ function shrinkRichToWidth(ctx, texts, size, available, makeFont, minScale = MIN_SCALE) {
2473
+ if (available <= 0 || texts.length === 0) return size;
2474
+ const saved = ctx.font;
2475
+ const w = Math.max(...texts.map((t) => richWidth(ctx, t, size, makeFont)));
2476
+ ctx.font = saved;
2477
+ if (w <= available) return size;
2478
+ return Math.max(size * minScale, size * (available / w));
2479
+ }
2480
+ function nudgeRichInside(ctx, text, x, y, canvasW, canvasH, size, makeFont, margin = EDGE) {
2481
+ const e = richExtent(ctx, text, size, makeFont);
2482
+ let nx = x;
2483
+ let ny = y;
2484
+ if (x - e.left < margin) nx = margin + e.left;
2485
+ else if (x + e.right > canvasW - margin) nx = canvasW - margin - e.right;
2486
+ if (y - e.up < margin) ny = margin + e.up;
2487
+ else if (y + e.down > canvasH - margin) ny = canvasH - margin - e.down;
2488
+ return { x: nx, y: ny };
2489
+ }
2490
+ function drawFloatingRich(ctx, text, x, y, canvasW, canvasH, fontSize, makeFont) {
2491
+ if (!text) return;
2492
+ if (!HAS_SUB.test(text)) {
2493
+ drawFloatingLabel(ctx, text, x, y, canvasW, canvasH, fontSize, makeFont);
2494
+ return;
2495
+ }
2496
+ const size = shrinkRichToWidth(ctx, [text], fontSize, canvasW - EDGE * 2, makeFont);
2497
+ const at = nudgeRichInside(ctx, text, x, y, canvasW, canvasH, size, makeFont);
2498
+ fillRich(ctx, text, at.x, at.y, size, makeFont);
2499
+ }
2500
+
2501
+ // src/core/graphs/EconPlane.ts
2502
+ var ARROW_EXT = 26;
2503
+ var AXIS_W = 2.5;
2504
+ var LINE_W = 3;
2505
+ var GUIDE_W = 1.5;
2506
+ var ARROW_W = 2.5;
2507
+ var DOT_R = 6;
2508
+ var HEAD_LEN = 14;
2509
+ var HEAD_HALF = 6;
2510
+ var LABEL_GAP = 13;
2511
+ var NAME_LINE_H = 1.15;
2512
+ var DASH_PATTERN = {
2513
+ dashed: [7, 5],
2514
+ dotted: [1.5, 4]
2515
+ };
2516
+ var THICK_DASH = [12, 8];
2517
+ var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
2518
+ function formatTick3(v) {
2519
+ const s = Number.isInteger(v) ? String(v) : v.toFixed(1);
2520
+ return s.startsWith("-") ? "\u2212" + s.slice(1) : s;
2521
+ }
2522
+ function tickText(axis, i) {
2523
+ const given = Array.isArray(axis.tickLabels) ? axis.tickLabels[i] : void 0;
2524
+ return typeof given === "string" && given !== "" ? given : formatTick3(axis.ticks[i]);
2525
+ }
2526
+ function splitLines(text) {
2527
+ return text ? text.split(/\\n|\n/) : [""];
2528
+ }
2529
+ function span(axis) {
2530
+ const d = axis.max - axis.min;
2531
+ return Number.isFinite(d) && Math.abs(d) > 1e-9 ? d : 1;
2532
+ }
2533
+ function seriesOf(data) {
2534
+ return Array.isArray(data.series) ? data.series.filter((s) => s && Array.isArray(s.points)) : [];
2535
+ }
2536
+ function drawOrder(series) {
2537
+ return series.length > 1 ? [...series].reverse() : series;
2538
+ }
2539
+ function drawSeriesMarker(ctx, s, cx, cy) {
2540
+ ctx.beginPath();
2541
+ if (s.marker === "square") ctx.rect(cx - DOT_R, cy - DOT_R, DOT_R * 2, DOT_R * 2);
2542
+ else ctx.arc(cx, cy, DOT_R, 0, Math.PI * 2);
2543
+ ctx.fillStyle = s.hollow ? "#fff" : "#000";
2544
+ ctx.fill();
2545
+ if (s.hollow) {
2546
+ ctx.strokeStyle = "#000";
2547
+ ctx.lineWidth = 2;
2548
+ ctx.setLineDash([]);
2549
+ ctx.stroke();
2550
+ }
2551
+ }
2552
+ function anchorOf(pos, gap) {
2553
+ const d = gap * 0.75;
2554
+ switch (pos) {
2555
+ case "left":
2556
+ return { dx: -gap, dy: 0, align: "right", baseline: "middle" };
2557
+ case "top":
2558
+ return { dx: 0, dy: -gap, align: "center", baseline: "bottom" };
2559
+ case "bottom":
2560
+ return { dx: 0, dy: gap, align: "center", baseline: "top" };
2561
+ case "top-left":
2562
+ return { dx: -d, dy: -d, align: "right", baseline: "bottom" };
2563
+ case "top-right":
2564
+ return { dx: d, dy: -d, align: "left", baseline: "bottom" };
2565
+ case "bottom-left":
2566
+ return { dx: -d, dy: d, align: "right", baseline: "top" };
2567
+ case "bottom-right":
2568
+ return { dx: d, dy: d, align: "left", baseline: "top" };
2569
+ default:
2570
+ return { dx: gap, dy: 0, align: "left", baseline: "middle" };
2571
+ }
2572
+ }
2573
+ function drawArrowHead(ctx, x1, y1, x2, y2) {
2574
+ const dx = x2 - x1;
2575
+ const dy = y2 - y1;
2576
+ const len = Math.hypot(dx, dy);
2577
+ if (!(len > 0)) return;
2578
+ const ux = dx / len;
2579
+ const uy = dy / len;
2580
+ ctx.beginPath();
2581
+ ctx.moveTo(x2, y2);
2582
+ ctx.lineTo(x2 - ux * HEAD_LEN - uy * HEAD_HALF, y2 - uy * HEAD_LEN + ux * HEAD_HALF);
2583
+ ctx.lineTo(x2 - ux * HEAD_LEN + uy * HEAD_HALF, y2 - uy * HEAD_LEN - ux * HEAD_HALF);
2584
+ ctx.closePath();
2585
+ ctx.fill();
2586
+ }
2587
+ function drawBreakMark(ctx, x, y, vertical) {
2588
+ const half = 8;
2589
+ const gap = 7;
2590
+ ctx.save();
2591
+ ctx.translate(x, y);
2592
+ if (vertical) ctx.rotate(-Math.PI / 2);
2593
+ ctx.fillStyle = "#fff";
2594
+ ctx.fillRect(-half - 2, -gap, (half + 2) * 2, gap * 2);
2595
+ ctx.strokeStyle = "#000";
2596
+ ctx.lineWidth = 1.6;
2597
+ for (const dy of [-gap * 0.5, gap * 0.5]) {
2598
+ ctx.beginPath();
2599
+ ctx.moveTo(-half, dy + 2.5);
2600
+ ctx.quadraticCurveTo(-half / 2, dy - 4.5, 0, dy);
2601
+ ctx.quadraticCurveTo(half / 2, dy + 4.5, half, dy - 2.5);
2602
+ ctx.stroke();
2603
+ }
2604
+ ctx.restore();
2605
+ }
2606
+ function renderEconPlane(ctx, w, h, data, options) {
2607
+ clearCanvas(ctx, w, h);
2608
+ const fs = options.fontSize;
2609
+ const four = data.quadrants === "all";
2610
+ const nameFont = (size) => getFont(size, options);
2611
+ const tickFont = getFont(fs.tick, options);
2612
+ const series = seriesOf(data);
2613
+ ctx.font = tickFont;
2614
+ const yTickW = data.yAxis.ticks.length > 0 ? Math.max(...data.yAxis.ticks.map((_, i) => richWidth(ctx, tickText(data.yAxis, i), fs.tick, nameFont))) : 0;
2615
+ ctx.font = nameFont(fs.axisLabel);
2616
+ const yNameLines = splitLines(data.yAxis.label);
2617
+ const xNameW = data.xAxis.label ? richWidth(ctx, data.xAxis.label, fs.axisLabel, nameFont) : 0;
2618
+ const yNameW = data.yAxis.label ? Math.max(...yNameLines.map((l) => l ? richWidth(ctx, l, fs.axisLabel, nameFont) : 0)) : 0;
2619
+ const lineLabelW = data.lines.length > 0 ? Math.max(...data.lines.map((l) => l.label ? richWidth(ctx, l.label, fs.axisLabel, nameFont) : 0)) : 0;
2620
+ const notes = options.footnotes.filter((f) => f.trim()).length;
2621
+ const inlineSource = !!options.sourceInline && !!options.source && !options.sourceLeft && notes > 0;
2622
+ const hasSourceLine = (!!options.source || !!options.sourceLeft) && !inlineSource;
2623
+ const bottomText = (hasSourceLine ? fs.dataLabel + 6 : 0) + notes * (fs.dataLabel * 0.9 + 6) + 12;
2624
+ const yNameExtra = (yNameLines.length - 1) * fs.axisLabel * NAME_LINE_H;
2625
+ const padding = {
2626
+ top: (options.title ? fs.title + 40 : 18) + fs.axisLabel + yNameExtra + 10 + ARROW_EXT,
2627
+ // 세로축 이름은 축 **왼쪽 위**에 오른쪽 맞춤으로 선다(네 사분면이면 가운데).
2628
+ // 자리가 모자라면 글꼴을 줄이고 밀어 넣지만, 흔한 길이는 여기서 비워 둔다.
2629
+ // 34% 를 넘겨 비우지는 않는다 — 이름 하나 때문에 그림이 사라지면 안 된다.
2630
+ left: four ? Math.max(46, ARROW_EXT + 20) : Math.max(46, yTickW + 14, Math.min(yNameW + 8, w * 0.34)),
2631
+ right: Math.min(
2632
+ w * 0.45,
2633
+ Math.max(56, lineLabelW + 16, ARROW_EXT + 12 + xNameW + 6)
2634
+ ),
2635
+ bottom: fs.tick + 24 + bottomText
2636
+ };
2637
+ const plotX = padding.left;
2638
+ const plotY = padding.top;
2639
+ const plotW = Math.max(40, w - padding.left - padding.right);
2640
+ const plotH = Math.max(40, h - padding.top - padding.bottom);
2641
+ const toX = (v) => plotX + (v - data.xAxis.min) / span(data.xAxis) * plotW;
2642
+ const toY = (v) => plotY + plotH - (v - data.yAxis.min) / span(data.yAxis) * plotH;
2643
+ const axX = four ? clamp(toX(0), plotX, plotX + plotW) : plotX;
2644
+ const axY = four ? clamp(toY(0), plotY, plotY + plotH) : plotY + plotH;
2645
+ const dash = DASH_PATTERN[data.dash] ?? DASH_PATTERN.dashed;
2646
+ if (data.grid) {
2647
+ const xEnd = data.xAxis.ticks.length > 0 ? toX(Math.max(...data.xAxis.ticks)) : plotX + plotW;
2648
+ const yEnd = data.yAxis.ticks.length > 0 ? toY(Math.max(...data.yAxis.ticks)) : plotY;
2649
+ ctx.save();
2650
+ ctx.strokeStyle = "#000";
2651
+ ctx.lineWidth = GUIDE_W;
2652
+ ctx.setLineDash(dash);
2653
+ for (const v of data.xAxis.ticks) {
2654
+ if (v === 0) continue;
2655
+ const x = toX(v);
2656
+ ctx.beginPath();
2657
+ ctx.moveTo(x, axY);
2658
+ ctx.lineTo(x, yEnd);
2659
+ ctx.stroke();
2660
+ }
2661
+ for (const v of data.yAxis.ticks) {
2662
+ if (v === 0) continue;
2663
+ const y = toY(v);
2664
+ ctx.beginPath();
2665
+ ctx.moveTo(axX, y);
2666
+ ctx.lineTo(xEnd, y);
2667
+ ctx.stroke();
2668
+ }
2669
+ ctx.restore();
2670
+ }
2671
+ ctx.save();
2672
+ ctx.strokeStyle = "#000";
2673
+ ctx.lineWidth = GUIDE_W;
2674
+ ctx.setLineDash(dash);
2675
+ for (const p of data.points) {
2676
+ if (p.guide === "none") continue;
2677
+ const px = toX(p.x);
2678
+ const py = toY(p.y);
2679
+ const cross = p.guide === "cross";
2680
+ if (cross || p.guide === "to-x" || p.guide === "both") {
2681
+ ctx.beginPath();
2682
+ ctx.moveTo(px, cross ? plotY : py);
2683
+ ctx.lineTo(px, cross ? plotY + plotH : axY);
2684
+ ctx.stroke();
2685
+ }
2686
+ if (cross || p.guide === "to-y" || p.guide === "both") {
2687
+ ctx.beginPath();
2688
+ ctx.moveTo(cross ? plotX : px, py);
2689
+ ctx.lineTo(cross ? plotX + plotW : axX, py);
2690
+ ctx.stroke();
2691
+ }
2692
+ }
2693
+ if (data.seriesGuides && series.length > 0) {
2694
+ const highest = /* @__PURE__ */ new Map();
2695
+ for (const s of series) {
2696
+ for (const p of s.points) {
2697
+ const prev = highest.get(p.x);
2698
+ if (prev == null || p.y > prev) highest.set(p.x, p.y);
2699
+ }
2700
+ }
2701
+ for (const [x, y] of highest) {
2702
+ ctx.beginPath();
2703
+ ctx.moveTo(toX(x), toY(y));
2704
+ ctx.lineTo(toX(x), axY);
2705
+ ctx.stroke();
2706
+ }
2707
+ }
2708
+ ctx.restore();
2709
+ ctx.save();
2710
+ ctx.strokeStyle = "#000";
2711
+ ctx.fillStyle = "#000";
2712
+ ctx.lineWidth = AXIS_W;
2713
+ ctx.lineCap = "butt";
2714
+ const xLeft = four ? plotX - ARROW_EXT : axX;
2715
+ const xRight = plotX + plotW + ARROW_EXT;
2716
+ ctx.beginPath();
2717
+ ctx.moveTo(xLeft, axY);
2718
+ ctx.lineTo(xRight, axY);
2719
+ ctx.stroke();
2720
+ drawArrowHead(ctx, xLeft, axY, xRight, axY);
2721
+ if (four) drawArrowHead(ctx, xRight, axY, xLeft, axY);
2722
+ const yBottom = four ? plotY + plotH + ARROW_EXT : axY;
2723
+ const yTop = plotY - ARROW_EXT;
2724
+ ctx.beginPath();
2725
+ ctx.moveTo(axX, yBottom);
2726
+ ctx.lineTo(axX, yTop);
2727
+ ctx.stroke();
2728
+ drawArrowHead(ctx, axX, yBottom, axX, yTop);
2729
+ if (four) drawArrowHead(ctx, axX, yTop, axX, yBottom);
2730
+ if (data.xAxis.broken) {
2731
+ const first = data.xAxis.ticks.find((v) => v !== 0);
2732
+ drawBreakMark(ctx, first == null ? axX + plotW * 0.12 : (axX + toX(first)) / 2, axY, false);
2733
+ }
2734
+ if (data.yAxis.broken) {
2735
+ const first = data.yAxis.ticks.find((v) => v !== 0);
2736
+ drawBreakMark(ctx, axX, first == null ? axY - plotH * 0.12 : (axY + toY(first)) / 2, true);
2737
+ }
2738
+ ctx.restore();
2739
+ ctx.save();
2740
+ ctx.strokeStyle = "#000";
2741
+ ctx.lineWidth = LINE_W;
2742
+ for (const line2 of data.lines) {
2743
+ ctx.lineCap = line2.dashed ? "butt" : "round";
2744
+ ctx.setLineDash(line2.dashed ? THICK_DASH : []);
2745
+ ctx.beginPath();
2746
+ ctx.moveTo(toX(line2.from.x), toY(line2.from.y));
2747
+ ctx.lineTo(toX(line2.to.x), toY(line2.to.y));
2748
+ ctx.stroke();
2749
+ }
2750
+ ctx.restore();
2751
+ ctx.save();
2752
+ ctx.strokeStyle = "#000";
2753
+ ctx.lineWidth = LINE_W;
2754
+ for (const s of drawOrder(series)) {
2755
+ if (s.points.length < 2) continue;
2756
+ ctx.lineCap = s.dashed ? "butt" : "round";
2757
+ ctx.setLineDash(s.dashed ? THICK_DASH : []);
2758
+ ctx.beginPath();
2759
+ s.points.forEach((p, i) => {
2760
+ if (i === 0) ctx.moveTo(toX(p.x), toY(p.y));
2761
+ else ctx.lineTo(toX(p.x), toY(p.y));
2762
+ });
2763
+ ctx.stroke();
2764
+ }
2765
+ ctx.restore();
2766
+ ctx.save();
2767
+ ctx.fillStyle = "#000";
2768
+ ctx.textAlign = "left";
2769
+ ctx.textBaseline = "middle";
2770
+ for (const line2 of data.lines) {
2771
+ if (!line2.label) continue;
2772
+ const end = line2.labelAt === "from" ? line2.from : line2.to;
2773
+ const lx = toX(end.x) + 12 + (line2.labelDx ?? 0);
2774
+ const raw = toY(end.y) + (line2.labelDy ?? 0);
2775
+ const ly = Math.min(raw, axY - 6 - fs.axisLabel * 0.5);
2776
+ drawFloatingRich(ctx, line2.label, lx, ly, w, h, fs.axisLabel, nameFont);
2777
+ }
2778
+ ctx.restore();
2779
+ ctx.save();
2780
+ ctx.strokeStyle = "#000";
2781
+ ctx.fillStyle = "#000";
2782
+ ctx.lineWidth = ARROW_W;
2783
+ for (const a of data.arrows) {
2784
+ const x1 = toX(a.from.x);
2785
+ const y1 = toY(a.from.y);
2786
+ const x2 = toX(a.to.x);
2787
+ const y2 = toY(a.to.y);
2788
+ const len = Math.hypot(x2 - x1, y2 - y1);
2789
+ if (!(len > 0)) continue;
2790
+ const ux = (x2 - x1) / len;
2791
+ const uy = (y2 - y1) / len;
2792
+ const ox = -uy * a.offset;
2793
+ const oy = ux * a.offset;
2794
+ const cut = Math.min(a.shorten, len * 0.4);
2795
+ const sx = x1 + ux * cut + ox;
2796
+ const sy = y1 + uy * cut + oy;
2797
+ const ex = x2 - ux * cut + ox;
2798
+ const ey = y2 - uy * cut + oy;
2799
+ ctx.beginPath();
2800
+ ctx.moveTo(sx, sy);
2801
+ ctx.lineTo(ex, ey);
2802
+ ctx.stroke();
2803
+ drawArrowHead(ctx, sx, sy, ex, ey);
2804
+ if (a.label) {
2805
+ const at = anchorOf(a.labelPos, LABEL_GAP + 4);
2806
+ ctx.textAlign = at.align;
2807
+ ctx.textBaseline = at.baseline;
2808
+ drawFloatingRich(ctx, a.label, (sx + ex) / 2 + at.dx, (sy + ey) / 2 + at.dy, w, h, fs.axisLabel, nameFont);
2809
+ }
2810
+ }
2811
+ ctx.restore();
2812
+ ctx.save();
2813
+ ctx.fillStyle = "#000";
2814
+ for (const p of data.points) {
2815
+ const px = toX(p.x);
2816
+ const py = toY(p.y);
2817
+ if (p.dot) {
2818
+ ctx.beginPath();
2819
+ ctx.arc(px, py, DOT_R, 0, Math.PI * 2);
2820
+ ctx.fill();
2821
+ }
2822
+ if (!p.label) continue;
2823
+ const at = anchorOf(p.labelPos, DOT_R + LABEL_GAP);
2824
+ ctx.textAlign = at.align;
2825
+ ctx.textBaseline = at.baseline;
2826
+ drawFloatingRich(ctx, p.label, px + at.dx, py + at.dy, w, h, fs.axisLabel, nameFont);
2827
+ }
2828
+ ctx.restore();
2829
+ ctx.save();
2830
+ for (const s of drawOrder(series)) {
2831
+ for (const p of s.points) drawSeriesMarker(ctx, s, toX(p.x), toY(p.y));
2832
+ }
2833
+ ctx.restore();
2834
+ const tickInk = (text, x, y) => {
2835
+ if (four) {
2836
+ const tw = richWidth(ctx, text, fs.tick, nameFont);
2837
+ const left = ctx.textAlign === "right" ? tw : ctx.textAlign === "center" ? tw / 2 : 0;
2838
+ const up = ctx.textBaseline === "top" ? 0 : fs.tick * 0.5;
2839
+ const rx = clamp(x - left - 3, 0, w);
2840
+ const ry = clamp(y - up - 2, 0, h);
2841
+ ctx.fillStyle = "#fff";
2842
+ ctx.fillRect(rx, ry, Math.min(tw + 6, w - rx), Math.min(fs.tick + 4, h - ry));
2843
+ ctx.fillStyle = "#000";
2844
+ }
2845
+ fillRich(ctx, text, x, y, fs.tick, nameFont);
2846
+ };
2847
+ ctx.save();
2848
+ ctx.fillStyle = "#000";
2849
+ ctx.font = tickFont;
2850
+ ctx.textAlign = "center";
2851
+ ctx.textBaseline = "top";
2852
+ data.xAxis.ticks.forEach((v, i) => {
2853
+ if (v === 0) return;
2854
+ const text = tickText(data.xAxis, i);
2855
+ const at = nudgeRichInside(ctx, text, toX(v), axY + 10, w, h, fs.tick, nameFont);
2856
+ tickInk(text, at.x, at.y);
2857
+ });
2858
+ ctx.textAlign = four ? "left" : "right";
2859
+ ctx.textBaseline = "middle";
2860
+ data.yAxis.ticks.forEach((v, i) => {
2861
+ if (v === 0) return;
2862
+ const text = tickText(data.yAxis, i);
2863
+ const at = nudgeRichInside(ctx, text, axX + (four ? 10 : -10), toY(v), w, h, fs.tick, nameFont);
2864
+ tickInk(text, at.x, at.y);
2865
+ });
2866
+ ctx.textAlign = four ? "left" : "center";
2867
+ ctx.textBaseline = "top";
2868
+ const zero = nudgeInside(ctx, "0", axX + (four ? 8 : 0), axY + 10, w, h);
2869
+ tickInk("0", zero.x, zero.y);
2870
+ ctx.restore();
2871
+ ctx.save();
2872
+ ctx.fillStyle = "#000";
2873
+ if (data.xAxis.label) {
2874
+ ctx.textAlign = "left";
2875
+ ctx.textBaseline = "middle";
2876
+ drawFloatingRich(ctx, data.xAxis.label, xRight + 10, axY + fs.tick * 0.6, w, h, fs.axisLabel, nameFont);
2877
+ }
2878
+ if (data.yAxis.label) {
2879
+ ctx.textAlign = four ? "center" : "right";
2880
+ ctx.textBaseline = "bottom";
2881
+ const baseY = yTop - (four ? 8 : -4);
2882
+ if (yNameLines.length === 1) {
2883
+ drawFloatingRich(ctx, data.yAxis.label, axX + (four ? 0 : -8), baseY, w, h, fs.axisLabel, nameFont);
2884
+ } else {
2885
+ const lh = fs.axisLabel * NAME_LINE_H;
2886
+ const cx = four ? axX : axX - 8 - yNameW / 2;
2887
+ ctx.textAlign = "center";
2888
+ yNameLines.forEach((lineText, i) => {
2889
+ const ly = baseY - (yNameLines.length - 1 - i) * lh;
2890
+ drawFloatingRich(ctx, lineText, cx, ly, w, h, fs.axisLabel, nameFont);
2891
+ });
2892
+ }
2893
+ }
2894
+ ctx.restore();
2895
+ if (data.legend && series.length > 0) {
2896
+ const items = series.map((s) => ({
2897
+ type: "line",
2898
+ fillStyle: "#000",
2899
+ strokeStyle: "#000",
2900
+ label: s.label,
2901
+ dash: s.dashed ? THICK_DASH : [],
2902
+ lineWidth: 2.5,
2903
+ marker: s.marker,
2904
+ hollow: s.hollow
2905
+ }));
2906
+ drawInsideLegend({
2907
+ ctx,
2908
+ items,
2909
+ corner: data.legend,
2910
+ plotX,
2911
+ plotY,
2912
+ plotW,
2913
+ plotH,
2914
+ canvasW: w,
2915
+ canvasH: h,
2916
+ fontSize: fs.axisLabel * 0.8,
2917
+ font: nameFont(fs.axisLabel * 0.8),
2918
+ // 기호가 상자에 덮이지 않게 꼭짓점 둘레를 피할 자리로 넘긴다
2919
+ avoid: series.flatMap((s) => s.points.map((p) => ({
2920
+ x0: toX(p.x) - DOT_R,
2921
+ y0: toY(p.y) - DOT_R,
2922
+ x1: toX(p.x) + DOT_R,
2923
+ y1: toY(p.y) + DOT_R
2924
+ })))
2925
+ });
2926
+ }
2927
+ drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: fs.title, fonts: options, canvasWidth: w });
2928
+ drawSourceAndFootnote({
2929
+ ctx,
2930
+ plotX,
2931
+ plotW,
2932
+ height: h,
2933
+ source: options.source,
2934
+ sourceLeft: options.sourceLeft,
2935
+ sourceInline: options.sourceInline,
2936
+ footnotes: options.footnotes,
2937
+ fontSize: fs.dataLabel,
2938
+ fonts: options,
2939
+ canvasWidth: w
2940
+ });
2941
+ }
2942
+
2314
2943
  // src/core/graphs/TreemapGraph.ts
2315
2944
  var PAD_RATIO3 = 0.4;
2316
2945
  var MIN_LABEL_W = 34;
@@ -2418,29 +3047,30 @@ function renderTreemapGraph(ctx, w, h, data, options) {
2418
3047
  ctx.fillStyle = "#000";
2419
3048
  ctx.textAlign = "center";
2420
3049
  ctx.textBaseline = "middle";
2421
- ctx.font = getFont(labelSize, options.fontFamily, options.customFont);
3050
+ ctx.font = getFont(labelSize, options);
2422
3051
  rects.forEach((r, i) => {
2423
3052
  if (shouldOmitLabel(r, minLabelW, labelSize)) return;
2424
3053
  const avail = r.w - pad * 2;
2425
- ctx.font = getFont(labelSize, options.fontFamily, options.customFont);
3054
+ ctx.font = getFont(labelSize, options);
2426
3055
  const layout = pickLabelLayout(ctx, data.cells[i].label, avail, r.h, labelSize);
2427
3056
  if (shouldOmitLayout(layout)) return;
2428
3057
  const drawSize = resolveDrawSize(labelSize, layout);
2429
3058
  if (drawSize !== labelSize) {
2430
- ctx.font = getFont(drawSize, options.fontFamily, options.customFont);
3059
+ ctx.font = getFont(drawSize, options);
2431
3060
  }
2432
3061
  const startY = r.y + r.h / 2 - (layout.lines.length - 1) * drawSize * 1.2 / 2;
2433
3062
  layout.lines.forEach((line2, k) => {
2434
3063
  ctx.fillText(line2, r.x + r.w / 2, startY + k * drawSize * 1.2, avail);
2435
3064
  });
2436
3065
  if (drawSize !== labelSize) {
2437
- ctx.font = getFont(labelSize, options.fontFamily, options.customFont);
3066
+ ctx.font = getFont(labelSize, options);
2438
3067
  }
2439
3068
  });
2440
3069
  ctx.lineWidth = 2;
2441
3070
  ctx.strokeRect(frame.x, frame.y, frame.w, frame.h);
2442
3071
  drawTitle({
2443
3072
  ctx,
3073
+ fonts: options,
2444
3074
  plotX: frame.x,
2445
3075
  plotW: frame.w,
2446
3076
  title: options.title,
@@ -2449,6 +3079,7 @@ function renderTreemapGraph(ctx, w, h, data, options) {
2449
3079
  });
2450
3080
  drawSourceAndFootnote({
2451
3081
  ctx,
3082
+ fonts: options,
2452
3083
  plotX: frame.x,
2453
3084
  plotW: frame.w,
2454
3085
  height: h,
@@ -2474,12 +3105,13 @@ function renderClimateGraph(ctx, w, h, data, options) {
2474
3105
  options.legendLabel1 || data.precipLabel,
2475
3106
  options.legendLabel2 || data.tempLabel
2476
3107
  ];
2477
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
3108
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
2478
3109
  const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(
2479
3110
  ctx,
2480
3111
  legendLabels,
2481
3112
  options.fontSize.dataLabel * 0.85 + 5,
2482
3113
  w - 130 - (130 + legendW),
3114
+ options,
2483
3115
  ["rect", data.monthInterval === 12 ? "line" : "circle"]
2484
3116
  ) : 0;
2485
3117
  const padding = {
@@ -2512,8 +3144,6 @@ function renderClimateGraph(ctx, w, h, data, options) {
2512
3144
  step: data.precipRange.step || Math.max(1, Math.round((data.precipRange.max - data.precipRange.min) / 6))
2513
3145
  };
2514
3146
  if (precipAxis.min < 0) precipAxis.min = 0;
2515
- const font = options.fontFamily;
2516
- const customFont = options.customFont;
2517
3147
  const indices = INTERVAL_INDICES[data.monthInterval] ?? INTERVAL_INDICES[12];
2518
3148
  drawYAxis({
2519
3149
  ctx,
@@ -2525,8 +3155,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2525
3155
  step: tempAxis.step,
2526
3156
  label: data.tempLabel,
2527
3157
  side: "left",
2528
- fontFamily: font,
2529
- customFont,
3158
+ fonts: options,
2530
3159
  tickFontSize: options.fontSize.tick,
2531
3160
  labelFontSize: options.fontSize.axisLabel,
2532
3161
  drawGrid: true
@@ -2541,8 +3170,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2541
3170
  step: precipAxis.step,
2542
3171
  label: data.precipLabel,
2543
3172
  side: "right",
2544
- fontFamily: font,
2545
- customFont,
3173
+ fonts: options,
2546
3174
  tickFontSize: options.fontSize.tick,
2547
3175
  labelFontSize: options.fontSize.axisLabel
2548
3176
  });
@@ -2554,8 +3182,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2554
3182
  height: h,
2555
3183
  labels: MONTH_LABELS,
2556
3184
  indices,
2557
- fontFamily: font,
2558
- customFont,
3185
+ fonts: options,
2559
3186
  tickFontSize: options.fontSize.tick,
2560
3187
  labelFontSize: options.fontSize.axisLabel
2561
3188
  });
@@ -2567,8 +3194,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2567
3194
  width: w,
2568
3195
  height: h,
2569
3196
  labels: filteredLabels,
2570
- fontFamily: font,
2571
- customFont,
3197
+ fonts: options,
2572
3198
  tickFontSize: options.fontSize.tick,
2573
3199
  labelFontSize: options.fontSize.axisLabel
2574
3200
  });
@@ -2593,7 +3219,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2593
3219
  }
2594
3220
  if (options.showDataLabels) {
2595
3221
  ctx.fillStyle = "#000";
2596
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
3222
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
2597
3223
  ctx.textAlign = "center";
2598
3224
  ctx.textBaseline = "bottom";
2599
3225
  for (let s = 0; s < indices.length; s++) {
@@ -2628,21 +3254,22 @@ function renderClimateGraph(ctx, w, h, data, options) {
2628
3254
  ctx.fill();
2629
3255
  if (options.showDataLabels) {
2630
3256
  ctx.fillStyle = "#000";
2631
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
3257
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
2632
3258
  ctx.textAlign = "center";
2633
3259
  ctx.textBaseline = "bottom";
2634
3260
  ctx.fillText(String(data.months[i].temp), cx, y - 8);
2635
3261
  }
2636
3262
  }
2637
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
3263
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2638
3264
  ctx.fillStyle = "#000";
2639
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
3265
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
2640
3266
  ctx.textAlign = "center";
2641
3267
  ctx.textBaseline = "top";
2642
3268
  ctx.fillText("(\uC6D4)", plotX + plotW + 30, plotY + plotH + 12);
2643
3269
  if (showLegend) {
2644
3270
  drawLegend({
2645
3271
  ctx,
3272
+ fonts: options,
2646
3273
  items: [
2647
3274
  { type: "rect", fillStyle: "#AAA", strokeStyle: "#666", label: legendLabels[0] },
2648
3275
  { type: data.monthInterval === 12 ? "line" : "circle", fillStyle: "#000", label: legendLabels[1] }
@@ -2658,7 +3285,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2658
3285
  rightGap: 80
2659
3286
  });
2660
3287
  }
2661
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel });
3288
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel });
2662
3289
  }
2663
3290
 
2664
3291
  // src/core/graphs/CubeGraph.ts
@@ -2710,8 +3337,6 @@ var EDGES = [
2710
3337
  ];
2711
3338
  function renderCubeGraph(ctx, w, h, data, options) {
2712
3339
  clearCanvas(ctx, w, h);
2713
- const font = options.fontFamily;
2714
- const cf = options.customFont;
2715
3340
  const fs = options.fontSize;
2716
3341
  const topPad = options.title ? 100 : 60;
2717
3342
  let bottomPad = 40;
@@ -2728,8 +3353,7 @@ function renderCubeGraph(ctx, w, h, data, options) {
2728
3353
  availH,
2729
3354
  Math.min(availW * 0.5, availH * 0.55),
2730
3355
  fs,
2731
- font,
2732
- cf
3356
+ options
2733
3357
  );
2734
3358
  const scale = fit.scale;
2735
3359
  const cubeCenter = project(0.5, 0.5, 0.5, 0, 0, scale);
@@ -2757,7 +3381,7 @@ function renderCubeGraph(ctx, w, h, data, options) {
2757
3381
  ctx.lineTo(v2[b][0], v2[b][1]);
2758
3382
  ctx.stroke();
2759
3383
  }
2760
- drawAxes(ctx, data, cx, cy, scale, w, h, font, cf, fs, fit.names, fit.nameSize);
3384
+ drawAxes(ctx, data, cx, cy, scale, w, h, options, fs, fit.names, fit.nameSize);
2761
3385
  const cubeCenter2D = project(0.5, 0.5, 0.5, cx, cy, scale);
2762
3386
  for (const pt of data.points) {
2763
3387
  const [px, py] = project(pt.z, pt.y, pt.x, cx, cy, scale);
@@ -2772,7 +3396,7 @@ function renderCubeGraph(ctx, w, h, data, options) {
2772
3396
  autoDy = autoDy / len * 40;
2773
3397
  const dx = autoDx + pt.labelDx;
2774
3398
  const dy = autoDy + pt.labelDy;
2775
- ctx.font = getFont(fs.dataLabel + 10, font, cf, "bold");
3399
+ ctx.font = getFont(fs.dataLabel + 10, options, "bold");
2776
3400
  ctx.textAlign = dx >= 0 ? "left" : "right";
2777
3401
  ctx.textBaseline = "middle";
2778
3402
  const anchor = nudgeInside(ctx, pt.label, px + dx + (dx >= 0 ? 4 : -4), py + dy, w, h);
@@ -2793,9 +3417,10 @@ function renderCubeGraph(ctx, w, h, data, options) {
2793
3417
  const plotX = cubeLeft;
2794
3418
  const plotW = cubeRight - cubeLeft;
2795
3419
  if (options.title) {
3420
+ const titleFont = sansFont(options);
2796
3421
  const yAxisTop = project(0, 1.25, 0, cx, cy, scale);
2797
3422
  ctx.fillStyle = "#000";
2798
- ctx.font = `bold ${fs.title}px 'Noto Sans KR', sans-serif`;
3423
+ ctx.font = `bold ${fs.title}px ${titleFont}`;
2799
3424
  ctx.textAlign = "center";
2800
3425
  ctx.textBaseline = "bottom";
2801
3426
  drawFloatingLabel(
@@ -2806,13 +3431,13 @@ function renderCubeGraph(ctx, w, h, data, options) {
2806
3431
  w,
2807
3432
  h,
2808
3433
  fs.title,
2809
- (size) => `bold ${size}px 'Noto Sans KR', sans-serif`
3434
+ (size) => `bold ${size}px ${titleFont}`
2810
3435
  );
2811
3436
  }
2812
3437
  const zAxisEnd = project(1.25, 0, 0, cx, cy, scale);
2813
3438
  const zHighRef = project(1, 0, 0, cx, cy, scale);
2814
3439
  const sourceY = Math.max(zAxisEnd[1] + 20 + fs.axisLabel, zHighRef[1] + 10 + fs.axisLabel * 0.9) + 47;
2815
- drawSourceAndFootnote({ ctx, plotX, plotW, height: sourceY, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel });
3440
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: sourceY, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel });
2816
3441
  }
2817
3442
  function drawArrow(ctx, x1, y1, x2, y2) {
2818
3443
  const headLen = 10;
@@ -2859,11 +3484,11 @@ function centerAt(w, topPad, availH, s) {
2859
3484
  const c = project(0.5, 0.5, 0.5, 0, 0, s);
2860
3485
  return [w / 2 - c[0], topPad + availH / 2 - c[1]];
2861
3486
  }
2862
- function fitCubeScale(ctx, data, w, h, topPad, availH, maxScale, fs, font, cf) {
3487
+ function fitCubeScale(ctx, data, w, h, topPad, availH, maxScale, fs, options) {
2863
3488
  ctx.save();
2864
3489
  let nameSize = fs.axisLabel;
2865
- const makeNameFont = (size) => getFont(size, font, cf, "bold");
2866
- const dirFont = getFont(fs.axisLabel * 0.9, font, cf, "normal");
3490
+ const makeNameFont = (size) => getFont(size, options, "bold");
3491
+ const dirFont = getFont(fs.axisLabel * 0.9, options, "normal");
2867
3492
  let names = { x: [data.xAxis.name], y: [data.yAxis.name], z: [data.zAxis.name] };
2868
3493
  const fits = (s) => {
2869
3494
  const [cx, cy] = centerAt(w, topPad, availH, s);
@@ -2903,7 +3528,7 @@ function fitCubeScale(ctx, data, w, h, topPad, availH, maxScale, fs, font, cf) {
2903
3528
  ctx.restore();
2904
3529
  return { scale, names, nameSize };
2905
3530
  }
2906
- function drawAxes(ctx, data, cx, cy, scale, w, h, font, cf, fs, names, nameSize) {
3531
+ function drawAxes(ctx, data, cx, cy, scale, w, h, options, fs, names, nameSize) {
2907
3532
  const ext = 1.25;
2908
3533
  ctx.strokeStyle = "#000";
2909
3534
  ctx.fillStyle = "#000";
@@ -2917,8 +3542,8 @@ function drawAxes(ctx, data, cx, cy, scale, w, h, font, cf, fs, names, nameSize)
2917
3542
  const zStart = project(0, 0, 1, cx, cy, scale);
2918
3543
  const zEnd = project(0, 0, ext, cx, cy, scale);
2919
3544
  drawArrow(ctx, zStart[0], zStart[1], zEnd[0], zEnd[1]);
2920
- const nameFont = getFont(nameSize, font, cf, "bold");
2921
- const dirFont = getFont(fs.axisLabel * 0.9, font, cf, "normal");
3545
+ const nameFont = getFont(nameSize, options, "bold");
3546
+ const dirFont = getFont(fs.axisLabel * 0.9, options, "normal");
2922
3547
  ctx.fillStyle = "#000";
2923
3548
  for (const t of axisTexts(data, cx, cy, scale, names)) {
2924
3549
  if (t.lines.every((l) => !l)) continue;
@@ -2946,11 +3571,11 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
2946
3571
  options.legendLabel1 || data.precipLabel,
2947
3572
  options.legendLabel2 || data.tempLabel
2948
3573
  ];
2949
- const legendW = showLegend && legendPos === "right" && !data.insideLegend ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
3574
+ const legendW = showLegend && legendPos === "right" && !data.insideLegend ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
2950
3575
  let nameW = 0;
2951
3576
  if (data.tempAxisName || data.precipAxisName) {
2952
3577
  ctx.save();
2953
- ctx.font = getFont(options.fontSize.axisLabel, options.fontFamily, options.customFont, "bold");
3578
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
2954
3579
  nameW = ctx.measureText("\uAC00").width + 12;
2955
3580
  ctx.restore();
2956
3581
  }
@@ -2961,6 +3586,7 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
2961
3586
  legendLabels,
2962
3587
  options.fontSize.dataLabel * 0.85 + 5,
2963
3588
  w - padLeft - padRight,
3589
+ options,
2964
3590
  ["rect", data.monthInterval === 12 ? "line" : "circle"]
2965
3591
  ) : 0;
2966
3592
  const padding = {
@@ -2980,8 +3606,6 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
2980
3606
  const plotY = padding.top;
2981
3607
  const plotW = w - padding.left - padding.right;
2982
3608
  const plotH = h - padding.top - padding.bottom;
2983
- const font = options.fontFamily;
2984
- const customFont = options.customFont;
2985
3609
  const indices = INTERVAL_INDICES2[data.monthInterval] ?? INTERVAL_INDICES2[12];
2986
3610
  const tempDevs = data.months.map((m, i) => m.temp - data.baseMonths[i].temp);
2987
3611
  const precipDevs = data.months.map((m, i) => m.precip - data.baseMonths[i].precip);
@@ -3004,8 +3628,8 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3004
3628
  if (tempAxis.step === 0) {
3005
3629
  tempAxis.step = Math.max(0.5, Math.round((tempAxis.max - tempAxis.min) / 6 * 10) / 10);
3006
3630
  }
3007
- const tempTickW = drawDeviationYAxis(ctx, padding, w, h, tempAxis, "left", data.tempLabel, font, customFont, options.fontSize);
3008
- const precipTickW = drawDeviationYAxis(ctx, padding, w, h, precipAxis, "right", data.precipLabel, font, customFont, options.fontSize);
3631
+ const tempTickW = drawDeviationYAxis(ctx, padding, w, h, tempAxis, "left", data.tempLabel, options, options.fontSize);
3632
+ const precipTickW = drawDeviationYAxis(ctx, padding, w, h, precipAxis, "right", data.precipLabel, options, options.fontSize);
3009
3633
  const totalSlots = indices.length;
3010
3634
  const slotW = plotW / totalSlots;
3011
3635
  ctx.strokeStyle = "#000";
@@ -3021,7 +3645,7 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3021
3645
  ctx.stroke();
3022
3646
  }
3023
3647
  ctx.fillStyle = "#000";
3024
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
3648
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3025
3649
  ctx.textAlign = "center";
3026
3650
  ctx.textBaseline = "top";
3027
3651
  for (let s = 0; s < totalSlots; s++) {
@@ -3091,7 +3715,7 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3091
3715
  inkRects.push({ x0: cx - 6, y0: y - 6, x1: cx + 6, y1: y + 6 });
3092
3716
  }
3093
3717
  ctx.fillStyle = "#000";
3094
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
3718
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3095
3719
  ctx.textAlign = "center";
3096
3720
  ctx.textBaseline = "top";
3097
3721
  ctx.fillText("(\uC6D4)", plotX + plotW + 30, plotY + plotH + 12);
@@ -3120,12 +3744,13 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3120
3744
  canvasW: w,
3121
3745
  canvasH: h,
3122
3746
  fontSize: options.fontSize.dataLabel * 0.9,
3123
- font: getFont(options.fontSize.dataLabel * 0.9, font, customFont, "bold"),
3747
+ font: getFont(options.fontSize.dataLabel * 0.9, options, "bold"),
3124
3748
  avoid: inkRects
3125
3749
  });
3126
3750
  } else if (showLegend) {
3127
3751
  drawLegend({
3128
3752
  ctx,
3753
+ fonts: options,
3129
3754
  items: [
3130
3755
  { type: "rect", fillStyle: "#888", strokeStyle: "#444", label: legendLabels[0] },
3131
3756
  { type: data.monthInterval === 12 ? "line" : "circle", fillStyle: "#000", label: legendLabels[1] }
@@ -3150,8 +3775,7 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3150
3775
  plotH,
3151
3776
  h,
3152
3777
  options.fontSize.axisLabel,
3153
- font,
3154
- customFont
3778
+ options
3155
3779
  );
3156
3780
  }
3157
3781
  if (data.precipAxisName) {
@@ -3163,14 +3787,13 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3163
3787
  plotH,
3164
3788
  h,
3165
3789
  options.fontSize.axisLabel,
3166
- font,
3167
- customFont
3790
+ options
3168
3791
  );
3169
3792
  }
3170
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
3171
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
3793
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
3794
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
3172
3795
  }
3173
- function drawDeviationYAxis(ctx, padding, width, height, axis, side, label, fontFamily, customFont, fontSize) {
3796
+ function drawDeviationYAxis(ctx, padding, width, height, axis, side, label, fonts, fontSize) {
3174
3797
  const plotX = padding.left;
3175
3798
  const plotY = padding.top;
3176
3799
  const plotW = width - padding.left - padding.right;
@@ -3183,7 +3806,7 @@ function drawDeviationYAxis(ctx, padding, width, height, axis, side, label, font
3183
3806
  ctx.lineTo(x, plotY + plotH);
3184
3807
  ctx.stroke();
3185
3808
  ctx.fillStyle = "#000";
3186
- ctx.font = getFont(fontSize.tick, fontFamily, customFont, "bold");
3809
+ ctx.font = getFont(fontSize.tick, fonts, "bold");
3187
3810
  ctx.textBaseline = "middle";
3188
3811
  ctx.textAlign = side === "left" ? "right" : "left";
3189
3812
  let maxTickW = 0;
@@ -3208,7 +3831,7 @@ function drawDeviationYAxis(ctx, padding, width, height, axis, side, label, font
3208
3831
  }
3209
3832
  if (label) {
3210
3833
  ctx.save();
3211
- const makeFont = (size) => getFont(size, fontFamily, customFont, "bold");
3834
+ const makeFont = (size) => getFont(size, fonts, "bold");
3212
3835
  ctx.fillStyle = "#000";
3213
3836
  ctx.textBaseline = "bottom";
3214
3837
  ctx.textAlign = side === "left" ? "right" : "left";
@@ -3220,7 +3843,7 @@ function drawDeviationYAxis(ctx, padding, width, height, axis, side, label, font
3220
3843
  }
3221
3844
  return maxTickW;
3222
3845
  }
3223
- function drawVerticalAxisName(ctx, name, cx, plotY, plotH, canvasH, fontSize, fontFamily, customFont) {
3846
+ function drawVerticalAxisName(ctx, name, cx, plotY, plotH, canvasH, fontSize, fonts) {
3224
3847
  const chars = [...name].filter((c) => c.trim() !== "");
3225
3848
  if (chars.length === 0) return;
3226
3849
  const stack = (size2) => (chars.length - 1) * size2 * 1.15 + size2;
@@ -3230,7 +3853,7 @@ function drawVerticalAxisName(ctx, name, cx, plotY, plotH, canvasH, fontSize, fo
3230
3853
  }
3231
3854
  if (stack(size) > canvasH - EDGE * 2) size = (canvasH - EDGE * 2) / stack(1);
3232
3855
  ctx.save();
3233
- ctx.font = getFont(size, fontFamily, customFont, "bold");
3856
+ ctx.font = getFont(size, fonts, "bold");
3234
3857
  ctx.fillStyle = "#000";
3235
3858
  ctx.textAlign = "center";
3236
3859
  ctx.textBaseline = "middle";
@@ -3258,12 +3881,13 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3258
3881
  options.legendLabel1 || data.precipDiffLabel,
3259
3882
  options.legendLabel2 || data.tempDiffLabel
3260
3883
  ];
3261
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
3884
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
3262
3885
  const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(
3263
3886
  ctx,
3264
3887
  legendLabels,
3265
3888
  options.fontSize.dataLabel * 0.85 + 5,
3266
3889
  w - 130 - (130 + legendW),
3890
+ options,
3267
3891
  ["rect", "circle"]
3268
3892
  ) : 0;
3269
3893
  const padding = {
@@ -3283,8 +3907,6 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3283
3907
  const plotY = padding.top;
3284
3908
  const plotW = w - padding.left - padding.right;
3285
3909
  const plotH = h - padding.top - padding.bottom;
3286
- const font = options.fontFamily;
3287
- const customFont = options.customFont;
3288
3910
  const regions = data.regions;
3289
3911
  const n = regions.length;
3290
3912
  const precipDiffs = regions.map((r) => r.precip - data.basePrecip);
@@ -3305,8 +3927,8 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3305
3927
  tempAxis.min = -tempAbs;
3306
3928
  tempAxis.max = tempAbs;
3307
3929
  }
3308
- drawDevBYAxis(ctx, padding, w, h, tempAxis, "left", data.tempUnit, font, customFont, options.fontSize);
3309
- drawDevBYAxis(ctx, padding, w, h, precipAxis, "right", data.precipUnit, font, customFont, options.fontSize);
3930
+ drawDevBYAxis(ctx, padding, w, h, tempAxis, "left", data.tempUnit, options, options.fontSize);
3931
+ drawDevBYAxis(ctx, padding, w, h, precipAxis, "right", data.precipUnit, options, options.fontSize);
3310
3932
  ctx.strokeStyle = "#000";
3311
3933
  ctx.lineWidth = 2;
3312
3934
  ctx.beginPath();
@@ -3323,10 +3945,10 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3323
3945
  const slotW = plotW / n;
3324
3946
  const barWidth = Math.min(slotW * 0.5, 80);
3325
3947
  ctx.fillStyle = "#000";
3326
- ctx.font = getFont(options.fontSize.tick * 1.2, font, customFont, "bold");
3948
+ ctx.font = getFont(options.fontSize.tick * 1.2, options, "bold");
3327
3949
  ctx.textAlign = "center";
3328
3950
  ctx.textBaseline = "top";
3329
- const regionFont = (size) => getFont(size, font, customFont, "bold");
3951
+ const regionFont = (size) => getFont(size, options, "bold");
3330
3952
  for (let i = 0; i < n; i++) {
3331
3953
  const cx = plotX + slotW * i + slotW / 2;
3332
3954
  drawFloatingLabel(
@@ -3362,7 +3984,7 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3362
3984
  ctx.fill();
3363
3985
  }
3364
3986
  if (options.showDataLabels) {
3365
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
3987
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
3366
3988
  ctx.textAlign = "center";
3367
3989
  for (let i = 0; i < n; i++) {
3368
3990
  const cx = plotX + slotW * i + slotW / 2;
@@ -3380,6 +4002,7 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3380
4002
  if (showLegend) {
3381
4003
  drawLegend({
3382
4004
  ctx,
4005
+ fonts: options,
3383
4006
  items: [
3384
4007
  { type: "rect", fillStyle: "#888", strokeStyle: "#444", label: legendLabels[0] },
3385
4008
  { type: "circle", fillStyle: "#000", label: legendLabels[1] }
@@ -3395,10 +4018,10 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3395
4018
  rightGap: 80
3396
4019
  });
3397
4020
  }
3398
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
3399
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel });
4021
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
4022
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel });
3400
4023
  }
3401
- function drawDevBYAxis(ctx, padding, width, height, axis, side, label, fontFamily, customFont, fontSize) {
4024
+ function drawDevBYAxis(ctx, padding, width, height, axis, side, label, fonts, fontSize) {
3402
4025
  const plotX = padding.left;
3403
4026
  const plotY = padding.top;
3404
4027
  const plotW = width - padding.left - padding.right;
@@ -3411,7 +4034,7 @@ function drawDevBYAxis(ctx, padding, width, height, axis, side, label, fontFamil
3411
4034
  ctx.lineTo(x, plotY + plotH);
3412
4035
  ctx.stroke();
3413
4036
  ctx.fillStyle = "#000";
3414
- ctx.font = getFont(fontSize.tick, fontFamily, customFont, "bold");
4037
+ ctx.font = getFont(fontSize.tick, fonts, "bold");
3415
4038
  ctx.textBaseline = "middle";
3416
4039
  ctx.textAlign = side === "left" ? "right" : "left";
3417
4040
  const tickCount = Math.round((axis.max - axis.min) / axis.step);
@@ -3433,7 +4056,7 @@ function drawDevBYAxis(ctx, padding, width, height, axis, side, label, fontFamil
3433
4056
  ctx.fillText(valStr, tx, y);
3434
4057
  }
3435
4058
  ctx.save();
3436
- ctx.font = getFont(fontSize.axisLabel, fontFamily, customFont, "bold");
4059
+ ctx.font = getFont(fontSize.axisLabel, fonts, "bold");
3437
4060
  ctx.fillStyle = "#000";
3438
4061
  ctx.textBaseline = "bottom";
3439
4062
  const labelX = side === "left" ? x - 12 : x + 12;
@@ -3499,19 +4122,18 @@ function drawMarkerLegendIcon(ctx, type, cx, cy) {
3499
4122
  }
3500
4123
  function renderHythergraph(ctx, w, h, data, options) {
3501
4124
  clearCanvas(ctx, w, h);
3502
- const font = options.fontFamily;
3503
- const cf = options.customFont;
3504
4125
  const fs = options.fontSize;
3505
4126
  const showLegend = options.showLegend && data.series.length > 0;
3506
4127
  const legendPos = options.legendPosition;
3507
4128
  const legendLabels = data.series.map((s) => s.label);
3508
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, fs.dataLabel * 0.85 + 5) : 0;
4129
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, fs.dataLabel * 0.85 + 5, options) : 0;
3509
4130
  const legendReserve = showLegend && legendPos === "bottom" ? 65 + layoutBottomLegend(
3510
4131
  ctx,
3511
4132
  legendLabels,
3512
4133
  data.series.map(() => 36),
3513
4134
  fs.dataLabel * 0.85 + 5,
3514
4135
  w - 80 - (80 + legendW),
4136
+ options,
3515
4137
  { iconGap: 8 }
3516
4138
  ).boxH + 2 : 0;
3517
4139
  const padding = {
@@ -3570,7 +4192,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3570
4192
  ctx.lineTo(plotX + plotW, plotY + plotH);
3571
4193
  ctx.stroke();
3572
4194
  ctx.fillStyle = "#000";
3573
- ctx.font = getFont(fs.tick, font, cf, "bold");
4195
+ ctx.font = getFont(fs.tick, options, "bold");
3574
4196
  ctx.textAlign = "center";
3575
4197
  ctx.textBaseline = "top";
3576
4198
  for (let v = xMin; v <= xMax + xStep * 0.01; v += xStep) {
@@ -3582,7 +4204,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3582
4204
  ctx.stroke();
3583
4205
  const isLast = v + xStep > xMax + xStep * 0.01;
3584
4206
  if (!(isLast && data.xUnit)) {
3585
- ctx.fillText(formatTick3(v), x, plotY + plotH + 10);
4207
+ ctx.fillText(formatTick4(v), x, plotY + plotH + 10);
3586
4208
  }
3587
4209
  }
3588
4210
  ctx.textAlign = "right";
@@ -3596,12 +4218,12 @@ function renderHythergraph(ctx, w, h, data, options) {
3596
4218
  ctx.stroke();
3597
4219
  const isLast = v + yStep > yMax + yStep * 0.01;
3598
4220
  if (!(isLast && data.yUnit)) {
3599
- ctx.fillText(formatTick3(v), plotX - 10, y);
4221
+ ctx.fillText(formatTick4(v), plotX - 10, y);
3600
4222
  }
3601
4223
  }
3602
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
4224
+ ctx.font = getFont(fs.axisLabel, options, "bold");
3603
4225
  ctx.fillStyle = "#000";
3604
- const unitFont = (size) => getFont(size, font, cf, "bold");
4226
+ const unitFont = (size) => getFont(size, options, "bold");
3605
4227
  if (data.xUnit) {
3606
4228
  ctx.textAlign = "right";
3607
4229
  ctx.textBaseline = "top";
@@ -3643,7 +4265,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3643
4265
  drawMarker3(ctx, marker, cx, cy, 5);
3644
4266
  {
3645
4267
  ctx.fillStyle = "#000";
3646
- ctx.font = getFont(fs.dataLabel, font, cf, "bold");
4268
+ ctx.font = getFont(fs.dataLabel, options, "bold");
3647
4269
  ctx.textAlign = "left";
3648
4270
  ctx.textBaseline = "bottom";
3649
4271
  ctx.fillText(mLabels[i], cx + 9, cy - 5);
@@ -3653,9 +4275,9 @@ function renderHythergraph(ctx, w, h, data, options) {
3653
4275
  }
3654
4276
  if (showLegend) {
3655
4277
  const lfSize = fs.dataLabel * 0.85 + 5;
3656
- const LEGEND_FONT2 = "'Noto Sans KR', sans-serif";
4278
+ const LEGEND_FONT = sansFont(options);
3657
4279
  ctx.save();
3658
- ctx.font = `bold ${lfSize}px ${LEGEND_FONT2}`;
4280
+ ctx.font = `bold ${lfSize}px ${LEGEND_FONT}`;
3659
4281
  const iconW = 36;
3660
4282
  const iconGap = 8;
3661
4283
  const pad = 12;
@@ -3671,6 +4293,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3671
4293
  data.series.map(() => iconW),
3672
4294
  lfSize,
3673
4295
  boxW,
4296
+ options,
3674
4297
  { iconGap, padding: pad, spacing }
3675
4298
  );
3676
4299
  const boxH = layout.boxH;
@@ -3699,7 +4322,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3699
4322
  ctx.stroke();
3700
4323
  ctx.setLineDash([]);
3701
4324
  drawMarkerLegendIcon(ctx, MARKERS[i % MARKERS.length], cx + iconW / 2, cy);
3702
- ctx.font = `bold ${layout.fontSize}px ${LEGEND_FONT2}`;
4325
+ ctx.font = `bold ${layout.fontSize}px ${LEGEND_FONT}`;
3703
4326
  ctx.fillStyle = "#000";
3704
4327
  ctx.textAlign = "left";
3705
4328
  ctx.textBaseline = "middle";
@@ -3734,7 +4357,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3734
4357
  ctx.stroke();
3735
4358
  ctx.setLineDash([]);
3736
4359
  drawMarkerLegendIcon(ctx, MARKERS[i % MARKERS.length], ix + iconW / 2, cy);
3737
- ctx.font = `bold ${lfSize}px ${LEGEND_FONT2}`;
4360
+ ctx.font = `bold ${lfSize}px ${LEGEND_FONT}`;
3738
4361
  ctx.fillStyle = "#000";
3739
4362
  ctx.textAlign = "left";
3740
4363
  ctx.textBaseline = "middle";
@@ -3744,10 +4367,10 @@ function renderHythergraph(ctx, w, h, data, options) {
3744
4367
  }
3745
4368
  ctx.restore();
3746
4369
  }
3747
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
3748
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
4370
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
4371
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
3749
4372
  }
3750
- function formatTick3(v) {
4373
+ function formatTick4(v) {
3751
4374
  return Math.abs(v) < 1e-4 ? "0" : Number(v.toFixed(2)).toString();
3752
4375
  }
3753
4376
 
@@ -3778,12 +4401,13 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3778
4401
  options.legendLabel1 || data.maleLabel,
3779
4402
  options.legendLabel2 || data.femaleLabel
3780
4403
  ];
3781
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
4404
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
3782
4405
  const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(
3783
4406
  ctx,
3784
4407
  legendLabels,
3785
4408
  options.fontSize.dataLabel * 0.85 + 5,
3786
- w - 60 - (80 + legendW)
4409
+ w - 60 - (80 + legendW),
4410
+ options
3787
4411
  ) : 0;
3788
4412
  const padding = {
3789
4413
  top: options.title ? 100 : 50,
@@ -3802,8 +4426,6 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3802
4426
  const plotY = padding.top;
3803
4427
  const plotW = w - padding.left - padding.right;
3804
4428
  const plotH = h - padding.top - padding.bottom;
3805
- const font = options.fontFamily;
3806
- const customFont = options.customFont;
3807
4429
  const displayAges = data.ages;
3808
4430
  const n = data.numericAgeAxis && displayAges.length > 0 ? displayAges.length : AGE_GROUPS.length;
3809
4431
  const isPercent = data.unit === "percent";
@@ -3848,7 +4470,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3848
4470
  ctx.lineTo(plotX + plotW, plotY + plotH);
3849
4471
  ctx.stroke();
3850
4472
  ctx.save();
3851
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
4473
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3852
4474
  const tickStep = pickTickStep(maxVal, halfW, ctx.measureText(fmtTick(maxVal)).width);
3853
4475
  ctx.restore();
3854
4476
  for (let v = tickStep; v <= maxVal; v += tickStep) {
@@ -3885,7 +4507,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3885
4507
  ctx.fillStyle = "#000";
3886
4508
  ctx.textBaseline = "middle";
3887
4509
  if (data.numericAgeAxis) {
3888
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
4510
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3889
4511
  ctx.textAlign = "right";
3890
4512
  const ageStep = 20;
3891
4513
  const topAge = n * 5;
@@ -3909,13 +4531,13 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3909
4531
  w,
3910
4532
  h,
3911
4533
  options.fontSize.tick,
3912
- (size) => getFont(size, font, customFont, "bold")
4534
+ (size) => getFont(size, options, "bold")
3913
4535
  );
3914
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
4536
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3915
4537
  ctx.textBaseline = "middle";
3916
4538
  }
3917
4539
  } else {
3918
- ctx.font = getFont(options.fontSize.tick * 0.75, font, customFont, "bold");
4540
+ ctx.font = getFont(options.fontSize.tick * 0.75, options, "bold");
3919
4541
  for (let i = 0; i < n; i++) {
3920
4542
  const y = plotY + plotH - (i + 1) * barH + barH / 2;
3921
4543
  if (side === "center") {
@@ -3930,7 +4552,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3930
4552
  }
3931
4553
  }
3932
4554
  }
3933
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
4555
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3934
4556
  ctx.textBaseline = "top";
3935
4557
  ctx.fillStyle = "#000";
3936
4558
  const xTickValues = [];
@@ -3972,7 +4594,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3972
4594
  ctx.fillText(fmtTick(rv), rx, plotY + plotH + 10);
3973
4595
  }
3974
4596
  }
3975
- ctx.font = getFont(options.fontSize.axisLabel, font, customFont, "bold");
4597
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
3976
4598
  ctx.textBaseline = "bottom";
3977
4599
  ctx.fillStyle = "#000";
3978
4600
  ctx.textAlign = "center";
@@ -3980,7 +4602,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3980
4602
  ctx.fillText(data.femaleLabel, centerX + halfW / 2, plotY - 16);
3981
4603
  ctx.save();
3982
4604
  if (data.axisLabelInline) {
3983
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
4605
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3984
4606
  const lastTickHalfW = ctx.measureText(fmtTick(maxVal)).width / 2;
3985
4607
  ctx.textAlign = "left";
3986
4608
  ctx.textBaseline = "top";
@@ -3992,10 +4614,10 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3992
4614
  w,
3993
4615
  h,
3994
4616
  options.fontSize.tick,
3995
- (size) => getFont(size, font, customFont, "bold")
4617
+ (size) => getFont(size, options, "bold")
3996
4618
  );
3997
4619
  } else {
3998
- ctx.font = getFont(options.fontSize.axisLabel * 0.85, font, customFont, "bold");
4620
+ ctx.font = getFont(options.fontSize.axisLabel * 0.85, options, "bold");
3999
4621
  const unitY = plotY + plotH + 10 + options.fontSize.tick + 22;
4000
4622
  ctx.textAlign = "left";
4001
4623
  drawFloatingLabel(
@@ -4006,12 +4628,12 @@ function renderPyramidGraph(ctx, w, h, data, options) {
4006
4628
  w,
4007
4629
  h,
4008
4630
  options.fontSize.axisLabel * 0.85,
4009
- (size) => getFont(size, font, customFont, "bold")
4631
+ (size) => getFont(size, options, "bold")
4010
4632
  );
4011
4633
  }
4012
4634
  ctx.restore();
4013
4635
  if (options.showDataLabels) {
4014
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
4636
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
4015
4637
  ctx.fillStyle = "#000";
4016
4638
  for (let i = 0; i < n; i++) {
4017
4639
  const y = plotY + plotH - (i + 1) * barH + barH / 2;
@@ -4029,10 +4651,11 @@ function renderPyramidGraph(ctx, w, h, data, options) {
4029
4651
  }
4030
4652
  }
4031
4653
  }
4032
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
4654
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
4033
4655
  if (showLegend) {
4034
4656
  drawLegend({
4035
4657
  ctx,
4658
+ fonts: options,
4036
4659
  items: [
4037
4660
  { type: "rect", fillStyle: "#666", strokeStyle: "#444", label: legendLabels[0] },
4038
4661
  { type: "rect", fillStyle: "#BBB", strokeStyle: "#888", label: legendLabels[1] }
@@ -4047,7 +4670,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
4047
4670
  fontSize: options.fontSize.dataLabel * 0.85 + 5
4048
4671
  });
4049
4672
  }
4050
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
4673
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
4051
4674
  }
4052
4675
 
4053
4676
  // src/core/graphs/RadarChart.ts
@@ -4061,15 +4684,13 @@ var LINE_STYLES2 = [
4061
4684
  var GRAY_SHADES = ["#000", "#444", "#777", "#AAA", "#CCC"];
4062
4685
  function renderRadarChart(ctx, w, h, data, options) {
4063
4686
  clearCanvas(ctx, w, h);
4064
- const font = options.fontFamily;
4065
- const cf = options.customFont;
4066
4687
  const fs = options.fontSize;
4067
4688
  const n = data.axisLabels.length;
4068
4689
  if (n < 3) return;
4069
4690
  const showLegend = options.showLegend && data.series.length > 1;
4070
4691
  const legendPos = options.legendPosition;
4071
4692
  const legendLabels = data.series.map((s) => s.label);
4072
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, fs.dataLabel * 0.85 + 5, "line") : 0;
4693
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, fs.dataLabel * 0.85 + 5, options, "line") : 0;
4073
4694
  const topPad = options.title ? 60 : 10;
4074
4695
  const rightPad = 60 + legendW;
4075
4696
  const leftPad = 60;
@@ -4081,6 +4702,7 @@ function renderRadarChart(ctx, w, h, data, options) {
4081
4702
  legendLabels,
4082
4703
  fs.dataLabel * 0.85 + 5,
4083
4704
  w - leftPad - rightPad,
4705
+ options,
4084
4706
  "line",
4085
4707
  30
4086
4708
  ));
@@ -4093,7 +4715,7 @@ function renderRadarChart(ctx, w, h, data, options) {
4093
4715
  const cy = topPad + availH / 2;
4094
4716
  const angles = Array.from({ length: n }, (_, i) => -Math.PI / 2 + 2 * Math.PI * i / n);
4095
4717
  const labelFontSize = fs.axisLabel * 0.85;
4096
- const makeLabelFont = (size) => getFont(size, font, cf, "bold");
4718
+ const makeLabelFont = (size) => getFont(size, options, "bold");
4097
4719
  const { radius, labelLines, labelSize } = fitAxisLabels(
4098
4720
  ctx,
4099
4721
  data.axisLabels,
@@ -4137,13 +4759,13 @@ function renderRadarChart(ctx, w, h, data, options) {
4137
4759
  ctx.stroke();
4138
4760
  }
4139
4761
  ctx.fillStyle = "#888";
4140
- ctx.font = getFont(fs.tick * 0.8, font, cf, "normal");
4762
+ ctx.font = getFont(fs.tick * 0.8, options, "normal");
4141
4763
  ctx.textAlign = "left";
4142
4764
  ctx.textBaseline = "middle";
4143
4765
  for (let step = 1; step <= data.gridSteps; step++) {
4144
4766
  const val = maxVal / data.gridSteps * step;
4145
4767
  const r = step / data.gridSteps * radius;
4146
- ctx.fillText(formatTick4(val), cx + 4, cy - r - 2);
4768
+ ctx.fillText(formatTick5(val), cx + 4, cy - r - 2);
4147
4769
  }
4148
4770
  ctx.fillStyle = "#000";
4149
4771
  ctx.font = makeLabelFont(labelSize);
@@ -4222,6 +4844,7 @@ function renderRadarChart(ctx, w, h, data, options) {
4222
4844
  });
4223
4845
  drawLegend({
4224
4846
  ctx,
4847
+ fonts: options,
4225
4848
  items,
4226
4849
  position: legendPos,
4227
4850
  plotX: plotX2,
@@ -4236,10 +4859,10 @@ function renderRadarChart(ctx, w, h, data, options) {
4236
4859
  }
4237
4860
  const plotX = leftPad;
4238
4861
  const plotW = availW;
4239
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
4240
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
4862
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
4863
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
4241
4864
  }
4242
- function formatTick4(v) {
4865
+ function formatTick5(v) {
4243
4866
  if (Number.isInteger(v)) return v.toString();
4244
4867
  return v.toFixed(1);
4245
4868
  }
@@ -4305,17 +4928,15 @@ function renderScatterGraph(ctx, w, h, data, options) {
4305
4928
  }
4306
4929
  }
4307
4930
  function renderNormal(ctx, w, h, data, options) {
4308
- const font = options.fontFamily;
4309
- const cf = options.customFont;
4310
4931
  const fs = options.fontSize;
4311
4932
  const outsideLegend = data.bubbleLegendPosition === "outside-right" && data.showBubble && data.points.length > 0;
4312
- const bubbleM = outsideLegend ? bubbleLegendMetrics(ctx, data, fs, font, cf) : null;
4313
- const fillM = outsideLegend ? measureFillLegend(ctx, data, fs, font, cf) : { boxW: 0, boxH: 0 };
4933
+ const bubbleM = outsideLegend ? bubbleLegendMetrics(ctx, data, fs, options) : null;
4934
+ const fillM = outsideLegend ? measureFillLegend(ctx, data, fs, options) : { boxW: 0, boxH: 0 };
4314
4935
  const legendW = Math.max(bubbleM?.boxW ?? 0, fillM.boxW);
4315
4936
  const examUnitW = (() => {
4316
4937
  if (data.examFrame !== true || !data.xUnit) return 0;
4317
4938
  ctx.save();
4318
- ctx.font = getFont(fs.tick, font, cf, "bold");
4939
+ ctx.font = getFont(fs.tick, options, "bold");
4319
4940
  const width = ctx.measureText(data.xUnit).width;
4320
4941
  ctx.restore();
4321
4942
  return width;
@@ -4330,7 +4951,7 @@ function renderNormal(ctx, w, h, data, options) {
4330
4951
  const yMax = data.yRange.auto ? yAuto.max : data.yRange.max;
4331
4952
  const xStep = data.xRange.step ?? (data.xRange.auto ? xAuto.step : (xMax - xMin) / 5);
4332
4953
  const yStep = data.yRange.step ?? (data.yRange.auto ? yAuto.step : (yMax - yMin) / 5);
4333
- const yName = measureYAxisName(ctx, data, w, yMin, yMax, yStep, fs, font, cf);
4954
+ const yName = measureYAxisName(ctx, data, w, yMin, yMax, yStep, fs, options);
4334
4955
  const padding = {
4335
4956
  top: options.title ? 100 : 50,
4336
4957
  right: legendW > 0 ? legendW + 40 : Math.max(60, examUnitW + 34),
@@ -4384,14 +5005,14 @@ function renderNormal(ctx, w, h, data, options) {
4384
5005
  ctx.stroke();
4385
5006
  }
4386
5007
  ctx.fillStyle = "#000";
4387
- ctx.font = getFont(fs.tick, font, cf, "bold");
5008
+ ctx.font = getFont(fs.tick, options, "bold");
4388
5009
  ctx.textAlign = "center";
4389
5010
  ctx.textBaseline = "top";
4390
5011
  const xTicks = [];
4391
5012
  for (let v = xMin; v <= xMax + xStep * 0.01; v += xStep) xTicks.push(v);
4392
5013
  const xStride = labelStride(
4393
5014
  plotW / Math.max(1, xTicks.length - 1),
4394
- widestLabel(ctx, xTicks.map(formatTick5))
5015
+ widestLabel(ctx, xTicks.map(formatTick6))
4395
5016
  );
4396
5017
  let lastXLabelRight = plotX + plotW;
4397
5018
  xTicks.forEach((v, i) => {
@@ -4402,7 +5023,7 @@ function renderNormal(ctx, w, h, data, options) {
4402
5023
  ctx.lineTo(x, plotY + plotH + 6);
4403
5024
  ctx.stroke();
4404
5025
  if (i % xStride === 0) {
4405
- const text = formatTick5(v);
5026
+ const text = formatTick6(v);
4406
5027
  ctx.fillText(text, x, plotY + plotH + 10);
4407
5028
  lastXLabelRight = Math.max(lastXLabelRight, x + ctx.measureText(text).width / 2);
4408
5029
  }
@@ -4420,9 +5041,9 @@ function renderNormal(ctx, w, h, data, options) {
4420
5041
  ctx.moveTo(plotX - 6, y);
4421
5042
  ctx.lineTo(plotX, y);
4422
5043
  ctx.stroke();
4423
- if (i % yStride === 0) ctx.fillText(formatTick5(v), plotX - 10, y);
5044
+ if (i % yStride === 0) ctx.fillText(formatTick6(v), plotX - 10, y);
4424
5045
  });
4425
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
5046
+ ctx.font = getFont(fs.axisLabel, options, "bold");
4426
5047
  ctx.textAlign = "center";
4427
5048
  ctx.textBaseline = "top";
4428
5049
  drawFloatingLabel(
@@ -4433,16 +5054,16 @@ function renderNormal(ctx, w, h, data, options) {
4433
5054
  w,
4434
5055
  h,
4435
5056
  fs.axisLabel,
4436
- (size) => getFont(size, font, cf, "bold")
5057
+ (size) => getFont(size, options, "bold")
4437
5058
  );
4438
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
5059
+ ctx.font = getFont(fs.axisLabel, options, "bold");
4439
5060
  if (data.xUnit) {
4440
5061
  if (exam) {
4441
- ctx.font = getFont(fs.tick, font, cf, "bold");
5062
+ ctx.font = getFont(fs.tick, options, "bold");
4442
5063
  ctx.textAlign = "left";
4443
5064
  ctx.textBaseline = "top";
4444
5065
  ctx.fillText(data.xUnit, lastXLabelRight + 6, plotY + plotH + 10);
4445
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
5066
+ ctx.font = getFont(fs.axisLabel, options, "bold");
4446
5067
  } else {
4447
5068
  ctx.textAlign = "right";
4448
5069
  ctx.textBaseline = "top";
@@ -4460,10 +5081,10 @@ function renderNormal(ctx, w, h, data, options) {
4460
5081
  w,
4461
5082
  h,
4462
5083
  fs.axisLabel,
4463
- (size) => getFont(size, font, cf, "bold")
5084
+ (size) => getFont(size, options, "bold")
4464
5085
  );
4465
5086
  }
4466
- ctx.font = getFont(yName.size, font, cf, "bold");
5087
+ ctx.font = getFont(yName.size, options, "bold");
4467
5088
  ctx.textAlign = "right";
4468
5089
  ctx.textBaseline = "middle";
4469
5090
  const yNameLineH = yName.size * 1.3;
@@ -4474,15 +5095,14 @@ function renderNormal(ctx, w, h, data, options) {
4474
5095
  clampLinesMiddle(ctx, yName.lines, plotY + plotH / 2, yNameLineH, h),
4475
5096
  yNameLineH
4476
5097
  );
4477
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
5098
+ ctx.font = getFont(fs.axisLabel, options, "bold");
4478
5099
  drawPoints(
4479
5100
  ctx,
4480
5101
  data,
4481
5102
  toCanvasX,
4482
5103
  toCanvasY,
4483
5104
  fs,
4484
- font,
4485
- cf,
5105
+ options,
4486
5106
  options.showDataLabels,
4487
5107
  { left: plotX, right: plotX + plotW, top: plotY, bottom: plotY + plotH },
4488
5108
  w,
@@ -4492,22 +5112,20 @@ function renderNormal(ctx, w, h, data, options) {
4492
5112
  const x = plotX + plotW + 20;
4493
5113
  let y = plotY;
4494
5114
  if (bubbleM) {
4495
- drawBubbleLegendAt(ctx, bubbleM, x, y, font, cf);
5115
+ drawBubbleLegendAt(ctx, bubbleM, x, y, options);
4496
5116
  y += bubbleM.boxH + 8;
4497
5117
  }
4498
- if (fillM.boxH > 0) drawFillLegendAt(ctx, data, x, y, fillM, fs, font, cf);
5118
+ if (fillM.boxH > 0) drawFillLegendAt(ctx, data, x, y, fillM, fs, options);
4499
5119
  } else if (data.showBubble && data.points.length > 0) {
4500
5120
  const avoid = bubbleRects(data, toCanvasX, toCanvasY);
4501
- const fill = measureFillLegend(ctx, data, fs, font, cf);
4502
- const used = drawBubbleLegend(ctx, data, plotX, plotY, plotW, plotH, fs, font, cf, avoid, fill.boxH);
4503
- drawFillLegend(ctx, data, plotX, plotY, plotW, plotH, used.bottom, fs, font, cf, used.corner);
5121
+ const fill = measureFillLegend(ctx, data, fs, options);
5122
+ const used = drawBubbleLegend(ctx, data, plotX, plotY, plotW, plotH, fs, options, avoid, fill.boxH);
5123
+ drawFillLegend(ctx, data, plotX, plotY, plotW, plotH, used.bottom, fs, options, used.corner);
4504
5124
  }
4505
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
4506
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, sourceInline: options.sourceInline, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
5125
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
5126
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, sourceInline: options.sourceInline, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
4507
5127
  }
4508
5128
  function renderDeviation(ctx, w, h, data, options) {
4509
- const font = options.fontFamily;
4510
- const cf = options.customFont;
4511
5129
  const fs = options.fontSize;
4512
5130
  const padding = {
4513
5131
  // 상자 축이름은 그래프 위에 놓이고 그 아래에 y 단위가 들어가므로 자리를 더 준다
@@ -4584,7 +5202,7 @@ function renderDeviation(ctx, w, h, data, options) {
4584
5202
  ctx.lineTo(plotX + plotW, originY);
4585
5203
  ctx.stroke();
4586
5204
  ctx.fillStyle = "#000";
4587
- ctx.font = getFont(fs.tick, font, cf, "bold");
5205
+ ctx.font = getFont(fs.tick, options, "bold");
4588
5206
  ctx.textAlign = "right";
4589
5207
  ctx.textBaseline = "top";
4590
5208
  ctx.fillText("0", originX - 6, originY + 4);
@@ -4596,7 +5214,7 @@ function renderDeviation(ctx, w, h, data, options) {
4596
5214
  for (let v = -xLimit; v <= xLimit + xStep * 0.01; v += xStep) devXTicks.push(v);
4597
5215
  const devXStride = labelStride(
4598
5216
  plotW / Math.max(1, devXTicks.length - 1),
4599
- widestLabel(ctx, devXTicks.map(formatTick5))
5217
+ widestLabel(ctx, devXTicks.map(formatTick6))
4600
5218
  );
4601
5219
  devXTicks.forEach((v, i) => {
4602
5220
  if (Math.abs(v) < xStep * 0.01) return;
@@ -4606,14 +5224,14 @@ function renderDeviation(ctx, w, h, data, options) {
4606
5224
  ctx.moveTo(x, xTickBase);
4607
5225
  ctx.lineTo(x, xTickBase + 6);
4608
5226
  ctx.stroke();
4609
- if (i % devXStride === 0) ctx.fillText(formatTick5(v), x, xTickBase + 10);
5227
+ if (i % devXStride === 0) ctx.fillText(formatTick6(v), x, xTickBase + 10);
4610
5228
  });
4611
5229
  ctx.textAlign = "right";
4612
5230
  ctx.textBaseline = "middle";
4613
5231
  const devYTicks = [];
4614
5232
  for (let v = -yLimit; v <= yLimit + yStep * 0.01; v += yStep) devYTicks.push(v);
4615
5233
  const devYStride = labelStride(plotH / Math.max(1, devYTicks.length - 1), fs.tick * 1.1);
4616
- const yTickTextW = widestLabel(ctx, devYTicks.map(formatTick5));
5234
+ const yTickTextW = widestLabel(ctx, devYTicks.map(formatTick6));
4617
5235
  devYTicks.forEach((v, i) => {
4618
5236
  if (Math.abs(v) < yStep * 0.01) return;
4619
5237
  const y = toCanvasY(v);
@@ -4622,9 +5240,9 @@ function renderDeviation(ctx, w, h, data, options) {
4622
5240
  ctx.moveTo(yTickBase - 6, y);
4623
5241
  ctx.lineTo(yTickBase, y);
4624
5242
  ctx.stroke();
4625
- if (i % devYStride === 0) ctx.fillText(formatTick5(v), yTickBase - 10, y);
5243
+ if (i % devYStride === 0) ctx.fillText(formatTick6(v), yTickBase - 10, y);
4626
5244
  });
4627
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
5245
+ ctx.font = getFont(fs.axisLabel, options, "bold");
4628
5246
  if (!data.boxedAxisLabels) {
4629
5247
  ctx.textAlign = "center";
4630
5248
  ctx.textBaseline = "top";
@@ -4663,8 +5281,7 @@ function renderDeviation(ctx, w, h, data, options) {
4663
5281
  toCanvasX,
4664
5282
  toCanvasY,
4665
5283
  fs,
4666
- font,
4667
- cf,
5284
+ options,
4668
5285
  options.showDataLabels,
4669
5286
  { left: plotX, right: plotX + plotW, top: plotY, bottom: plotY + plotH },
4670
5287
  w,
@@ -4679,15 +5296,14 @@ function renderDeviation(ctx, w, h, data, options) {
4679
5296
  plotW,
4680
5297
  plotH,
4681
5298
  fs,
4682
- font,
4683
- cf,
5299
+ options,
4684
5300
  bubbleRects(data, toCanvasX, toCanvasY)
4685
5301
  );
4686
5302
  }
4687
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
4688
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, sourceInline: options.sourceInline, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
5303
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
5304
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, sourceInline: options.sourceInline, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
4689
5305
  }
4690
- function drawPoints(ctx, data, toX, toY, fs, font, cf, showLabels, bounds, canvasW, canvasH) {
5306
+ function drawPoints(ctx, data, toX, toY, fs, options, showLabels, bounds, canvasW, canvasH) {
4691
5307
  const maxSize = data.points.length > 0 ? Math.max(...data.points.map((p) => p.size), 1) : 1;
4692
5308
  const placer = new LabelPlacer();
4693
5309
  if (bounds) {
@@ -4696,8 +5312,8 @@ function drawPoints(ctx, data, toX, toY, fs, font, cf, showLabels, bounds, canva
4696
5312
  placer.reserveCircle(toX(pt.x), toY(pt.y), r);
4697
5313
  }
4698
5314
  }
4699
- const drawOrder = [...data.points].sort((a, b) => b.size - a.size);
4700
- for (const pt of drawOrder) {
5315
+ const drawOrder2 = [...data.points].sort((a, b) => b.size - a.size);
5316
+ for (const pt of drawOrder2) {
4701
5317
  const cx = toX(pt.x);
4702
5318
  const cy = toY(pt.y);
4703
5319
  if (data.showBubble && pt.size > 0) {
@@ -4721,7 +5337,7 @@ function drawPoints(ctx, data, toX, toY, fs, font, cf, showLabels, bounds, canva
4721
5337
  }
4722
5338
  if (pt.label) {
4723
5339
  ctx.fillStyle = "#000";
4724
- ctx.font = getFont(fs.dataLabel, font, cf, "bold");
5340
+ ctx.font = getFont(fs.dataLabel, options, "bold");
4725
5341
  const offset = data.showBubble && pt.size > 0 ? pt.size / maxSize * data.bubbleScale + 4 : 8;
4726
5342
  if (bounds) {
4727
5343
  placer.place(ctx, pt.label, cx, cy, {
@@ -4737,7 +5353,7 @@ function drawPoints(ctx, data, toX, toY, fs, font, cf, showLabels, bounds, canva
4737
5353
  }
4738
5354
  if (showLabels) {
4739
5355
  ctx.fillStyle = "#555";
4740
- ctx.font = getFont(fs.dataLabel * 0.8, font, cf, "normal");
5356
+ ctx.font = getFont(fs.dataLabel * 0.8, options, "normal");
4741
5357
  ctx.textAlign = "left";
4742
5358
  ctx.textBaseline = "top";
4743
5359
  const offset = data.showBubble && pt.size > 0 ? pt.size / maxSize * data.bubbleScale + 4 : 8;
@@ -4763,16 +5379,16 @@ function symmetricRange(absMax) {
4763
5379
  const step = limit / 4;
4764
5380
  return { limit, step };
4765
5381
  }
4766
- function formatTick5(v) {
5382
+ function formatTick6(v) {
4767
5383
  return Math.abs(v) < 1e-4 ? "0" : Number(v.toFixed(2)).toString();
4768
5384
  }
4769
- function measureYAxisName(ctx, data, w, yMin, yMax, yStep, fs, font, cf) {
5385
+ function measureYAxisName(ctx, data, w, yMin, yMax, yStep, fs, options) {
4770
5386
  ctx.save();
4771
- ctx.font = getFont(fs.tick, font, cf, "bold");
5387
+ ctx.font = getFont(fs.tick, options, "bold");
4772
5388
  const ticks = [];
4773
5389
  for (let v = yMin; v <= yMax + yStep * 0.01; v += yStep) ticks.push(v);
4774
- const tickW = widestLabel(ctx, ticks.map(formatTick5));
4775
- const makeFont = (size2) => getFont(size2, font, cf, "bold");
5390
+ const tickW = widestLabel(ctx, ticks.map(formatTick6));
5391
+ const makeFont = (size2) => getFont(size2, options, "bold");
4776
5392
  ctx.font = makeFont(fs.axisLabel);
4777
5393
  const given = (data.yLabel || "").split("\\n");
4778
5394
  const budget = Math.max(40, w * 0.3 - 18 - tickW - 12);
@@ -4821,12 +5437,12 @@ function pickLegendCorner(preferred, boxW, boxH, plotX, plotY, plotW, plotH, avo
4821
5437
  }
4822
5438
  return { corner: best.corner, x: best.x, y: best.y };
4823
5439
  }
4824
- function measureFillLegend(ctx, data, fs, font, cf) {
5440
+ function measureFillLegend(ctx, data, fs, options) {
4825
5441
  const legend = data.fillLegend;
4826
5442
  if (!legend || legend.items.length === 0) return { boxW: 0, boxH: 0 };
4827
5443
  const fontSize = fs.dataLabel * 0.8;
4828
5444
  ctx.save();
4829
- ctx.font = getFont(fontSize, font, cf, "bold");
5445
+ ctx.font = getFont(fontSize, options, "bold");
4830
5446
  const rowH = fontSize * 1.5;
4831
5447
  const swatch = fontSize * 0.95;
4832
5448
  const pad = 8;
@@ -4838,7 +5454,7 @@ function measureFillLegend(ctx, data, fs, font, cf) {
4838
5454
  ctx.restore();
4839
5455
  return { boxW: textW + pad * 2, boxH: titleH + legend.items.length * rowH + pad * 2 };
4840
5456
  }
4841
- function bubbleLegendMetrics(ctx, data, fs, font, cf) {
5457
+ function bubbleLegendMetrics(ctx, data, fs, options) {
4842
5458
  const sizes = data.points.map((p) => p.size).filter((s) => s > 0);
4843
5459
  if (sizes.length === 0) return null;
4844
5460
  const maxSize = Math.max(...sizes);
@@ -4851,7 +5467,7 @@ function bubbleLegendMetrics(ctx, data, fs, font, cf) {
4851
5467
  const maxR = radiusOf(uniqueSteps[0]);
4852
5468
  const labelFontSize = fs.dataLabel * 0.85;
4853
5469
  ctx.save();
4854
- ctx.font = getFont(labelFontSize, font, cf, "bold");
5470
+ ctx.font = getFont(labelFontSize, options, "bold");
4855
5471
  const labelW = Math.max(50, ...uniqueSteps.map((v) => ctx.measureText(labelOf(v)).width));
4856
5472
  ctx.restore();
4857
5473
  const pad = 14;
@@ -4868,7 +5484,7 @@ function bubbleLegendMetrics(ctx, data, fs, font, cf) {
4868
5484
  const boxH = maxR * 2 + lowest + pad * 2 + 10;
4869
5485
  return { uniqueSteps, labelOf, radiusOf, maxR, boxW, boxH, labelOffsets, lowest, labelFontSize, pad };
4870
5486
  }
4871
- function drawBubbleLegendAt(ctx, m, boxX, boxY, font, cf) {
5487
+ function drawBubbleLegendAt(ctx, m, boxX, boxY, options) {
4872
5488
  ctx.fillStyle = "#fff";
4873
5489
  ctx.strokeStyle = "#666";
4874
5490
  ctx.lineWidth = 1.5;
@@ -4897,18 +5513,18 @@ function drawBubbleLegendAt(ctx, m, boxX, boxY, font, cf) {
4897
5513
  ctx.stroke();
4898
5514
  ctx.restore();
4899
5515
  ctx.fillStyle = "#000";
4900
- ctx.font = getFont(m.labelFontSize, font, cf, "bold");
5516
+ ctx.font = getFont(m.labelFontSize, options, "bold");
4901
5517
  ctx.textAlign = "left";
4902
5518
  ctx.textBaseline = "middle";
4903
5519
  ctx.fillText(m.labelOf(size), circleX + m.maxR + 16, bottomCircleY + m.labelOffsets[si]);
4904
5520
  });
4905
5521
  }
4906
- function drawBubbleLegend(ctx, data, plotX, plotY, plotW, plotH, fs, font, cf, avoid = [], extraH = 0) {
5522
+ function drawBubbleLegend(ctx, data, plotX, plotY, plotW, plotH, fs, options, avoid = [], extraH = 0) {
4907
5523
  const preferred = insideCorner(data.bubbleLegendPosition);
4908
- const m = bubbleLegendMetrics(ctx, data, fs, font, cf);
5524
+ const m = bubbleLegendMetrics(ctx, data, fs, options);
4909
5525
  if (!m) return { height: 0, corner: preferred, bottom: 0 };
4910
5526
  const spot = pickLegendCorner(preferred, m.boxW, m.boxH, plotX, plotY, plotW, plotH, avoid, extraH);
4911
- drawBubbleLegendAt(ctx, m, spot.x, spot.y, font, cf);
5527
+ drawBubbleLegendAt(ctx, m, spot.x, spot.y, options);
4912
5528
  return { height: m.boxH, corner: spot.corner, bottom: spot.y + m.boxH };
4913
5529
  }
4914
5530
  function drawBoxedLabel(ctx, text, x, y, fontSize, place) {
@@ -4936,12 +5552,12 @@ function drawBoxedLabel(ctx, text, x, y, fontSize, place) {
4936
5552
  }
4937
5553
  ctx.restore();
4938
5554
  }
4939
- function drawFillLegendAt(ctx, data, boxX, boxY, m, fs, font, cf) {
5555
+ function drawFillLegendAt(ctx, data, boxX, boxY, m, fs, options) {
4940
5556
  const legend = data.fillLegend;
4941
5557
  if (!legend || legend.items.length === 0) return;
4942
5558
  const fontSize = fs.dataLabel * 0.8;
4943
5559
  ctx.save();
4944
- ctx.font = getFont(fontSize, font, cf, "bold");
5560
+ ctx.font = getFont(fontSize, options, "bold");
4945
5561
  const rowH = fontSize * 1.5;
4946
5562
  const swatch = fontSize * 0.95;
4947
5563
  const pad = 8;
@@ -4973,13 +5589,13 @@ function drawFillLegendAt(ctx, data, boxX, boxY, m, fs, font, cf) {
4973
5589
  }
4974
5590
  ctx.restore();
4975
5591
  }
4976
- function drawFillLegend(ctx, data, plotX, plotY, plotW, plotH, stackBelow, fs, font, cf, corner = "bottom-right") {
5592
+ function drawFillLegend(ctx, data, plotX, plotY, plotW, plotH, stackBelow, fs, options, corner = "bottom-right") {
4977
5593
  const legend = data.fillLegend;
4978
5594
  if (!legend || legend.items.length === 0) return;
4979
- const m = measureFillLegend(ctx, data, fs, font, cf);
5595
+ const m = measureFillLegend(ctx, data, fs, options);
4980
5596
  const boxX = corner.endsWith("right") ? plotX + plotW - m.boxW - 10 : plotX + 10;
4981
5597
  const boxY = stackBelow > 0 ? stackBelow + 6 : corner.startsWith("top") ? plotY + 10 : plotY + plotH - m.boxH - 10;
4982
- drawFillLegendAt(ctx, data, boxX, boxY, m, fs, font, cf);
5598
+ drawFillLegendAt(ctx, data, boxX, boxY, m, fs, options);
4983
5599
  }
4984
5600
 
4985
5601
  // src/core/graphs/StackedBarPie.ts
@@ -4994,13 +5610,14 @@ function renderStackedGraph(ctx, w, h, data, options) {
4994
5610
  function renderStackedBar(ctx, w, h, data, options) {
4995
5611
  const showLegend = options.showLegend;
4996
5612
  const legendPos = options.legendPosition;
4997
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
5613
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
4998
5614
  const isVertical = data.barDirection === "vertical";
4999
5615
  const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(
5000
5616
  ctx,
5001
5617
  data.seriesLabels,
5002
5618
  options.fontSize.dataLabel * 0.85 + 5,
5003
- w - (isVertical ? 80 : 100) - (isVertical ? 60 + legendW : 160 + legendW)
5619
+ w - (isVertical ? 80 : 100) - (isVertical ? 60 + legendW : 160 + legendW),
5620
+ options
5004
5621
  ) : 0;
5005
5622
  const padding = {
5006
5623
  top: options.title ? 100 : 50,
@@ -5019,8 +5636,6 @@ function renderStackedBar(ctx, w, h, data, options) {
5019
5636
  const plotY = padding.top;
5020
5637
  const plotW = w - padding.left - padding.right;
5021
5638
  const plotH = h - padding.top - padding.bottom;
5022
- const font = options.fontFamily;
5023
- const customFont = options.customFont;
5024
5639
  const n = data.categories.length;
5025
5640
  const sCount = data.seriesLabels.length;
5026
5641
  ctx.strokeStyle = "#000";
@@ -5034,7 +5649,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5034
5649
  ctx.lineTo(plotX, plotY);
5035
5650
  ctx.stroke();
5036
5651
  const stepV = data.axisStep ?? 20;
5037
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
5652
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5038
5653
  ctx.fillStyle = "#000";
5039
5654
  ctx.textAlign = "right";
5040
5655
  ctx.textBaseline = "middle";
@@ -5058,7 +5673,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5058
5673
  ctx.restore();
5059
5674
  }
5060
5675
  }
5061
- ctx.font = getFont(options.fontSize.axisLabel, font, customFont, "bold");
5676
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
5062
5677
  ctx.textAlign = "right";
5063
5678
  ctx.textBaseline = "bottom";
5064
5679
  ctx.fillText(data.unit, plotX - 10, plotY - 16);
@@ -5079,10 +5694,10 @@ function renderStackedBar(ctx, w, h, data, options) {
5079
5694
  ctx.lineWidth = 0.8;
5080
5695
  ctx.strokeRect(cx - barW / 2, y, barW, barH);
5081
5696
  if (data.labelInSegment) {
5082
- drawSegmentLabel(ctx, data, options, s, cx, y + barH / 2, barW, barH, lightAt(data, s), font, customFont);
5697
+ drawSegmentLabel(ctx, data, options, s, cx, y + barH / 2, barW, barH, lightAt(data, s));
5083
5698
  } else if (options.showDataLabels && barH > options.fontSize.dataLabel) {
5084
5699
  ctx.fillStyle = lightAt(data, s) ? "#000" : "#fff";
5085
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
5700
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
5086
5701
  ctx.textAlign = "center";
5087
5702
  ctx.textBaseline = "middle";
5088
5703
  ctx.fillText(String(val), cx, y + barH / 2);
@@ -5090,7 +5705,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5090
5705
  cumY += barH;
5091
5706
  }
5092
5707
  ctx.fillStyle = "#000";
5093
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
5708
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5094
5709
  ctx.textAlign = "center";
5095
5710
  ctx.textBaseline = "top";
5096
5711
  ctx.fillText(data.categories[c].label, cx, plotY + plotH + 12);
@@ -5104,7 +5719,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5104
5719
  ctx.lineTo(plotX, plotY);
5105
5720
  ctx.stroke();
5106
5721
  const stepH = data.axisStep ?? 20;
5107
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
5722
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5108
5723
  ctx.fillStyle = "#000";
5109
5724
  ctx.textAlign = "center";
5110
5725
  ctx.textBaseline = "top";
@@ -5128,7 +5743,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5128
5743
  ctx.restore();
5129
5744
  }
5130
5745
  }
5131
- ctx.font = getFont(options.fontSize.axisLabel, font, customFont, "bold");
5746
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
5132
5747
  ctx.textAlign = "left";
5133
5748
  ctx.textBaseline = "top";
5134
5749
  ctx.fillText(data.unit, plotX + plotW + 30, plotY + plotH + 10);
@@ -5150,7 +5765,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5150
5765
  ctx.strokeRect(x, cy - barH / 2, bw, barH);
5151
5766
  if (options.showDataLabels && bw > options.fontSize.dataLabel * 2) {
5152
5767
  ctx.fillStyle = lightAt(data, s) ? "#000" : "#fff";
5153
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
5768
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
5154
5769
  ctx.textAlign = "center";
5155
5770
  ctx.textBaseline = "middle";
5156
5771
  ctx.fillText(String(val), x + bw / 2, cy);
@@ -5158,13 +5773,13 @@ function renderStackedBar(ctx, w, h, data, options) {
5158
5773
  cumX += bw;
5159
5774
  }
5160
5775
  ctx.fillStyle = "#000";
5161
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
5776
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5162
5777
  ctx.textAlign = "right";
5163
5778
  ctx.textBaseline = "middle";
5164
5779
  ctx.fillText(data.categories[c].label, plotX - 10, cy);
5165
5780
  }
5166
5781
  }
5167
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
5782
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
5168
5783
  if (showLegend) {
5169
5784
  const items = data.seriesLabels.map((label, i) => ({
5170
5785
  type: "rect",
@@ -5174,6 +5789,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5174
5789
  }));
5175
5790
  drawLegend({
5176
5791
  ctx,
5792
+ fonts: options,
5177
5793
  items,
5178
5794
  position: legendPos,
5179
5795
  plotX,
@@ -5185,17 +5801,18 @@ function renderStackedBar(ctx, w, h, data, options) {
5185
5801
  fontSize: options.fontSize.dataLabel * 0.85 + 5
5186
5802
  });
5187
5803
  }
5188
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, sourceLeft: options.sourceLeft, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
5804
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, sourceLeft: options.sourceLeft, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
5189
5805
  }
5190
5806
  function renderPieChart(ctx, w, h, data, options) {
5191
5807
  const showLegend = options.showLegend;
5192
5808
  const legendPos = options.legendPosition;
5193
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
5809
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
5194
5810
  const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(
5195
5811
  ctx,
5196
5812
  data.seriesLabels,
5197
5813
  options.fontSize.dataLabel * 0.85 + 5,
5198
5814
  w - 60 - (60 + legendW),
5815
+ options,
5199
5816
  "rect",
5200
5817
  16
5201
5818
  ) : 0;
@@ -5216,8 +5833,6 @@ function renderPieChart(ctx, w, h, data, options) {
5216
5833
  const plotY = padding.top;
5217
5834
  const plotW = w - padding.left - padding.right;
5218
5835
  const plotH = h - padding.top - padding.bottom;
5219
- const font = options.fontFamily;
5220
- const customFont = options.customFont;
5221
5836
  const n = data.categories.length;
5222
5837
  const sCount = data.seriesLabels.length;
5223
5838
  const pieScale = (data.pieScale ?? 100) / 100;
@@ -5257,7 +5872,7 @@ function renderPieChart(ctx, w, h, data, options) {
5257
5872
  const lx = cx + Math.cos(midAngle) * maxR * 0.65;
5258
5873
  const ly = cy + Math.sin(midAngle) * maxR * 0.65;
5259
5874
  ctx.fillStyle = lightAt(data, s) ? "#000" : "#fff";
5260
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
5875
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
5261
5876
  ctx.textAlign = "center";
5262
5877
  ctx.textBaseline = "middle";
5263
5878
  ctx.fillText(String(val), lx, ly);
@@ -5270,12 +5885,12 @@ function renderPieChart(ctx, w, h, data, options) {
5270
5885
  ctx.arc(cx, cy, maxR, 0, Math.PI * 2);
5271
5886
  ctx.stroke();
5272
5887
  ctx.fillStyle = "#000";
5273
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
5888
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5274
5889
  ctx.textAlign = "center";
5275
5890
  ctx.textBaseline = "top";
5276
5891
  ctx.fillText(data.categories[c].label, cx, cy + maxR + 12);
5277
5892
  }
5278
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
5893
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
5279
5894
  if (showLegend) {
5280
5895
  const items = data.seriesLabels.map((label, i) => ({
5281
5896
  type: "rect",
@@ -5293,6 +5908,7 @@ function renderPieChart(ctx, w, h, data, options) {
5293
5908
  const effectivePlotH = pieBottom - plotY;
5294
5909
  drawLegend({
5295
5910
  ctx,
5911
+ fonts: options,
5296
5912
  items,
5297
5913
  position: legendPos,
5298
5914
  plotX,
@@ -5310,6 +5926,7 @@ function renderPieChart(ctx, w, h, data, options) {
5310
5926
  const effectivePlotH = pieBottom - plotY;
5311
5927
  drawLegend({
5312
5928
  ctx,
5929
+ fonts: options,
5313
5930
  items,
5314
5931
  position: legendPos,
5315
5932
  plotX,
@@ -5323,7 +5940,7 @@ function renderPieChart(ctx, w, h, data, options) {
5323
5940
  });
5324
5941
  }
5325
5942
  }
5326
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, sourceLeft: options.sourceLeft, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
5943
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, sourceLeft: options.sourceLeft, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
5327
5944
  }
5328
5945
  function stackOrder(cat, sCount) {
5329
5946
  const given = cat.segmentOrder;
@@ -5341,12 +5958,12 @@ function lightAt(data, i) {
5341
5958
  const v = data.seriesFills?.[i];
5342
5959
  return v === void 0 || v === null ? isLightFill(i) : isLightFillValue(v);
5343
5960
  }
5344
- function drawSegmentLabel(ctx, data, options, s, cx, cy, barW, barH, light, font, customFont) {
5961
+ function drawSegmentLabel(ctx, data, options, s, cx, cy, barW, barH, light) {
5345
5962
  const text = data.seriesLabels[s] ?? "";
5346
5963
  const size = options.fontSize.dataLabel * 0.9;
5347
5964
  if (!text || barH < size * 1.1) return;
5348
- const family = data.seriesIsSymbol && !data.seriesIsSymbol[s] ? "sans" : font;
5349
- ctx.font = getFont(size, family, customFont, "bold");
5965
+ const family = data.seriesIsSymbol && !data.seriesIsSymbol[s] ? "sans" : options.fontFamily;
5966
+ ctx.font = getFont(size, options, "bold", family);
5350
5967
  ctx.textAlign = "center";
5351
5968
  ctx.textBaseline = "middle";
5352
5969
  const maxW = Math.max(1, barW - 6);
@@ -5411,8 +6028,6 @@ function renderTernaryGraph(ctx, w, h, data, options) {
5411
6028
  const triH = triSize * Math.sqrt(3) / 2;
5412
6029
  const cx = plotX + plotW / 2;
5413
6030
  const cy = padding.top + 11 + triH * 2 / 3;
5414
- const font = options.fontFamily;
5415
- const customFont = options.customFont;
5416
6031
  const topPt = { x: cx, y: cy - triH * 2 / 3 };
5417
6032
  const leftPt = { x: cx - triSize / 2, y: cy + triH / 3 };
5418
6033
  const rightPt = { x: cx + triSize / 2, y: cy + triH / 3 };
@@ -5456,7 +6071,7 @@ function renderTernaryGraph(ctx, w, h, data, options) {
5456
6071
  const bTick = { x: Math.cos(Math.PI / 3), y: Math.sin(Math.PI / 3) };
5457
6072
  const cTick = { x: Math.cos(Math.PI / 3), y: -Math.sin(Math.PI / 3) };
5458
6073
  ctx.fillStyle = "#000";
5459
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
6074
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5460
6075
  ctx.strokeStyle = "#000";
5461
6076
  ctx.lineWidth = 1.5;
5462
6077
  for (let i = 0; i <= steps; i++) {
@@ -5488,7 +6103,7 @@ function renderTernaryGraph(ctx, w, h, data, options) {
5488
6103
  const cAt = nudgeInside(ctx, cLabel, cPos.x + cTick.x * tickLen + 4, cPos.y + cTick.y * tickLen, w, h);
5489
6104
  ctx.fillText(cLabel, cAt.x, cAt.y);
5490
6105
  }
5491
- ctx.font = getFont(nameFit.fontSize, font, customFont, "bold");
6106
+ ctx.font = getFont(nameFit.fontSize, options, "bold");
5492
6107
  ctx.fillStyle = "#000";
5493
6108
  const labelLineH = nameFit.fontSize * 1.3;
5494
6109
  const spots = namePlaces(w, plotX + plotW / 2, padding.top, triSize, nameFit.lines, labelLineH);
@@ -5506,21 +6121,21 @@ function renderTernaryGraph(ctx, w, h, data, options) {
5506
6121
  ctx.fill();
5507
6122
  if (p.label) {
5508
6123
  ctx.fillStyle = "#000";
5509
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
6124
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
5510
6125
  ctx.textAlign = "left";
5511
6126
  ctx.textBaseline = "bottom";
5512
6127
  ctx.fillText(p.label, x + 10, y - 4);
5513
6128
  }
5514
6129
  if (options.showDataLabels) {
5515
6130
  ctx.fillStyle = "#555";
5516
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont);
6131
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options);
5517
6132
  ctx.textAlign = "left";
5518
6133
  ctx.textBaseline = "top";
5519
6134
  ctx.fillText(`(${p.a}, ${p.b}, ${p.c})`, x + 10, y + 4);
5520
6135
  }
5521
6136
  }
5522
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
5523
- drawSourceAndFootnote({ ctx, plotX: leftPt.x, plotW: rightPt.x - leftPt.x, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
6137
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
6138
+ drawSourceAndFootnote({ ctx, fonts: options, plotX: leftPt.x, plotW: rightPt.x - leftPt.x, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
5524
6139
  }
5525
6140
  var SHRINK_LIMIT3 = 0.8;
5526
6141
  function namePlaces(_w, cx, padTop, triSize, lines, lineH) {
@@ -5540,7 +6155,7 @@ function namePlaces(_w, cx, padTop, triSize, lines, lineH) {
5540
6155
  }
5541
6156
  function fitAxisNames(ctx, labels, options, w, h, cx, padTop, maxSize) {
5542
6157
  ctx.save();
5543
- const makeFont = (size2) => getFont(size2, options.fontFamily, options.customFont, "bold");
6158
+ const makeFont = (size2) => getFont(size2, options, "bold");
5544
6159
  let fontSize = options.fontSize.axisLabel * 1.3;
5545
6160
  const given = labels.map((l) => (l || "").split("\\n"));
5546
6161
  let lines = given.map((g) => g.slice());
@@ -5594,6 +6209,7 @@ var REGISTRY = {
5594
6209
  // 위 climate 주석 참고 — 이 칸의 렌더러만 바꾼 오배선은 컴파일러가 잡지 못한다.
5595
6210
  "deviation-a": { render: renderDeviationAGraph, createDefaultData: createDefaultDeviationAData },
5596
6211
  "deviation-b": { render: renderDeviationBGraph, createDefaultData: createDefaultDeviationBData },
6212
+ "econ-plane": { render: renderEconPlane, createDefaultData: createDefaultEconPlaneData },
5597
6213
  hythergraph: { render: renderHythergraph, createDefaultData: createDefaultHythergraphData },
5598
6214
  line: { render: renderLineGraph, createDefaultData: createDefaultLineData },
5599
6215
  "matrix-table": { render: renderMatrixTable, createDefaultData: createDefaultMatrixTableData },
@@ -5854,6 +6470,7 @@ function mergeOptions(base, patch) {
5854
6470
  ...base,
5855
6471
  ...patch,
5856
6472
  fontSize: { ...base.fontSize, ...patch?.fontSize },
6473
+ fontStack: { ...base.fontStack, ...patch?.fontStack },
5857
6474
  footnotes: [...patch?.footnotes ?? base.footnotes]
5858
6475
  };
5859
6476
  }
@@ -5881,7 +6498,7 @@ var CsatChart = class {
5881
6498
  *
5882
6499
  * 필드가 아니라 **메서드**로 둔다. `static readonly ensureFonts = ensureFonts`
5883
6500
  * 로 적으면 ES2020 로 낮출 때 클래스 «뒤» 의 대입문이 되는데, 그것은 지울 수
5884
- * 없는 부수효과라 이 클래스와 레지스트리와 렌더러 16종이 모든 번들에 박힌다.
6501
+ * 없는 부수효과라 이 클래스와 레지스트리와 렌더러 17종이 모든 번들에 박힌다.
5885
6502
  * 메서드는 클래스 본문의 일부라 어느 목표에서도 그런 일이 없다 — 실제로
5886
6503
  * 겪은 문제이고, `test/bundle.test.ts` 가 이걸 회귀로 잡지는 않으니 여기
5887
6504
  * 적어 둔다.
@@ -6056,6 +6673,7 @@ export {
6056
6673
  MONTH_LABELS_EN2 as MONTH_LABELS_EN,
6057
6674
  MONTH_LABELS_NUM2 as MONTH_LABELS_NUM,
6058
6675
  autoRange,
6676
+ createAdAsData,
6059
6677
  createDefaultAbsBarData,
6060
6678
  createDefaultCategoryDotData,
6061
6679
  createDefaultClimateData,
@@ -6063,6 +6681,7 @@ export {
6063
6681
  createDefaultDataTableData,
6064
6682
  createDefaultDeviationAData,
6065
6683
  createDefaultDeviationBData,
6684
+ createDefaultEconPlaneData,
6066
6685
  createDefaultGraphOptions,
6067
6686
  createDefaultHythergraphData,
6068
6687
  createDefaultLineData,
@@ -6073,6 +6692,8 @@ export {
6073
6692
  createDefaultStackedData,
6074
6693
  createDefaultTernaryData,
6075
6694
  createDefaultTreemapData,
6695
+ createPointShiftData,
6696
+ createSupplyDemandData,
6076
6697
  ensureFonts,
6077
6698
  installRoundRectPolyfill,
6078
6699
  isCsatChartType,
@@ -6084,6 +6705,7 @@ export {
6084
6705
  renderDataTable,
6085
6706
  renderDeviationAGraph,
6086
6707
  renderDeviationBGraph,
6708
+ renderEconPlane,
6087
6709
  renderHythergraph,
6088
6710
  renderLineGraph,
6089
6711
  renderMatrixTable,