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.
@@ -31,6 +31,7 @@ __export(src_exports, {
31
31
  MONTH_LABELS_EN: () => MONTH_LABELS_EN2,
32
32
  MONTH_LABELS_NUM: () => MONTH_LABELS_NUM2,
33
33
  autoRange: () => autoRange,
34
+ createAdAsData: () => createAdAsData,
34
35
  createDefaultAbsBarData: () => createDefaultAbsBarData,
35
36
  createDefaultCategoryDotData: () => createDefaultCategoryDotData,
36
37
  createDefaultClimateData: () => createDefaultClimateData,
@@ -38,6 +39,7 @@ __export(src_exports, {
38
39
  createDefaultDataTableData: () => createDefaultDataTableData,
39
40
  createDefaultDeviationAData: () => createDefaultDeviationAData,
40
41
  createDefaultDeviationBData: () => createDefaultDeviationBData,
42
+ createDefaultEconPlaneData: () => createDefaultEconPlaneData,
41
43
  createDefaultGraphOptions: () => createDefaultGraphOptions,
42
44
  createDefaultHythergraphData: () => createDefaultHythergraphData,
43
45
  createDefaultLineData: () => createDefaultLineData,
@@ -48,6 +50,8 @@ __export(src_exports, {
48
50
  createDefaultStackedData: () => createDefaultStackedData,
49
51
  createDefaultTernaryData: () => createDefaultTernaryData,
50
52
  createDefaultTreemapData: () => createDefaultTreemapData,
53
+ createPointShiftData: () => createPointShiftData,
54
+ createSupplyDemandData: () => createSupplyDemandData,
51
55
  ensureFonts: () => ensureFonts,
52
56
  installRoundRectPolyfill: () => installRoundRectPolyfill,
53
57
  isCsatChartType: () => isCsatChartType,
@@ -59,6 +63,7 @@ __export(src_exports, {
59
63
  renderDataTable: () => renderDataTable,
60
64
  renderDeviationAGraph: () => renderDeviationAGraph,
61
65
  renderDeviationBGraph: () => renderDeviationBGraph,
66
+ renderEconPlane: () => renderEconPlane,
62
67
  renderHythergraph: () => renderHythergraph,
63
68
  renderLineGraph: () => renderLineGraph,
64
69
  renderMatrixTable: () => renderMatrixTable,
@@ -72,13 +77,19 @@ __export(src_exports, {
72
77
  module.exports = __toCommonJS(src_exports);
73
78
 
74
79
  // src/core/canvas/renderer.ts
75
- function getFont(size, family = "serif", customFont, weight = "normal") {
76
- const familyMap = {
77
- serif: "'Noto Serif KR', 'NanumMyeongjo', serif",
78
- sans: "'Noto Sans KR', sans-serif",
79
- custom: customFont || "'Noto Serif KR', serif"
80
- };
81
- return `${weight} ${size}px ${familyMap[family]}`;
80
+ var DEFAULT_SERIF_STACK = "'Noto Serif KR', 'NanumMyeongjo', serif";
81
+ var DEFAULT_SANS_STACK = "'Noto Sans KR', sans-serif";
82
+ var CUSTOM_FALLBACK_STACK = "'Noto Serif KR', serif";
83
+ function fontStackOf(fonts, role) {
84
+ if (role === "custom") return fonts.customFont || CUSTOM_FALLBACK_STACK;
85
+ if (role === "sans") return fonts.fontStack?.sans || DEFAULT_SANS_STACK;
86
+ return fonts.fontStack?.serif || DEFAULT_SERIF_STACK;
87
+ }
88
+ function sansFont(fonts) {
89
+ return fontStackOf(fonts, "sans");
90
+ }
91
+ function getFont(size, fonts, weight = "normal", role = fonts.fontFamily ?? "serif") {
92
+ return `${weight} ${size}px ${fontStackOf(fonts, role)}`;
82
93
  }
83
94
  function clearCanvas(ctx, w, h) {
84
95
  ctx.fillStyle = "#fff";
@@ -126,7 +137,6 @@ function autoRange(values, maxTicks = 8) {
126
137
  }
127
138
 
128
139
  // src/core/canvas/labels.ts
129
- var LABEL_FONT = "'Noto Sans KR', sans-serif";
130
140
  var MIN_SHRINK = 0.6;
131
141
  function fitFontSize(ctx, text, fontSize, available, makeFont) {
132
142
  if (available <= 0) return fontSize;
@@ -136,11 +146,12 @@ function fitFontSize(ctx, text, fontSize, available, makeFont) {
136
146
  const scaled = Math.max(fontSize * MIN_SHRINK, fontSize * (available / w));
137
147
  return Math.floor(scaled);
138
148
  }
139
- function drawTitle({ ctx, plotX, plotW, title, fontSize, canvasWidth }) {
149
+ function drawTitle({ ctx, plotX, plotW, title, fontSize, fonts, canvasWidth }) {
140
150
  if (!title) return;
141
151
  ctx.save();
142
152
  ctx.fillStyle = "#000";
143
- const makeFont = (size2) => `bold ${size2}px ${LABEL_FONT}`;
153
+ const labelFont = sansFont(fonts);
154
+ const makeFont = (size2) => `bold ${size2}px ${labelFont}`;
144
155
  const centerX = plotX + plotW / 2;
145
156
  const available = canvasWidth != null ? Math.max(0, Math.min(centerX, canvasWidth - centerX) * 2 - 12) : plotW;
146
157
  const size = fitFontSize(ctx, title, fontSize, available, makeFont);
@@ -161,9 +172,11 @@ function drawSourceAndFootnote({
161
172
  sourceInline,
162
173
  footnotes,
163
174
  fontSize,
175
+ fonts,
164
176
  canvasWidth
165
177
  }) {
166
178
  ctx.save();
179
+ const labelFont = sansFont(fonts);
167
180
  let leftX = plotX;
168
181
  let rightX = plotX + plotW;
169
182
  if (canvasWidth != null) {
@@ -178,7 +191,7 @@ function drawSourceAndFootnote({
178
191
  let y = height - 6 - totalFootnoteH - (sourceBelow ? sourceH : 0);
179
192
  const rightEdge = canvasWidth != null ? rightX : plotX + plotW;
180
193
  const available = Math.max(0, rightEdge - leftX);
181
- const sourceFont = (size) => `bold ${size}px ${LABEL_FONT}`;
194
+ const sourceFont = (size) => `bold ${size}px ${labelFont}`;
182
195
  if (source && !sourceBelow && !inlineSource) {
183
196
  ctx.fillStyle = "#555";
184
197
  const size = fitFontSize(ctx, source, fontSize, available, sourceFont);
@@ -195,7 +208,7 @@ function drawSourceAndFootnote({
195
208
  for (let i = 0; i < filtered.length; i++) {
196
209
  const text = "* " + filtered[i];
197
210
  ctx.fillStyle = "#555";
198
- const makeFont = (size2) => `${size2}px ${LABEL_FONT}`;
211
+ const makeFont = (size2) => `${size2}px ${labelFont}`;
199
212
  const size = fitFontSize(ctx, text, fontSize * 0.9, footnoteAvailable, makeFont);
200
213
  ctx.font = makeFont(size);
201
214
  ctx.textAlign = "left";
@@ -213,7 +226,7 @@ function drawSourceAndFootnote({
213
226
  ctx.fillText(source, rightX, lastFootnoteY, inlineSourceW > 0 ? inlineSourceW : void 0);
214
227
  }
215
228
  if (sourceBelow) {
216
- const makeFont = (size) => `bold ${size}px ${LABEL_FONT}`;
229
+ const makeFont = (size) => `bold ${size}px ${labelFont}`;
217
230
  const half = available > 0 ? available / 2 : 0;
218
231
  ctx.fillStyle = "#555";
219
232
  ctx.textBaseline = "bottom";
@@ -491,8 +504,7 @@ function drawYAxis({
491
504
  step,
492
505
  label,
493
506
  side,
494
- fontFamily,
495
- customFont,
507
+ fonts,
496
508
  tickFontSize,
497
509
  labelFontSize,
498
510
  drawGrid = false
@@ -506,7 +518,7 @@ function drawYAxis({
506
518
  ctx.lineTo(x, plot.y + plot.h);
507
519
  ctx.stroke();
508
520
  ctx.fillStyle = "#000";
509
- ctx.font = getFont(tickFontSize, fontFamily, customFont, "bold");
521
+ ctx.font = getFont(tickFontSize, fonts, "bold");
510
522
  ctx.textBaseline = "middle";
511
523
  ctx.textAlign = side === "left" ? "right" : "left";
512
524
  const ticks = [];
@@ -548,7 +560,7 @@ function drawYAxis({
548
560
  }
549
561
  if (label) {
550
562
  ctx.save();
551
- const makeFont = (size) => getFont(size, fontFamily, customFont, "bold");
563
+ const makeFont = (size) => getFont(size, fonts, "bold");
552
564
  ctx.fillStyle = "#000";
553
565
  ctx.textBaseline = "bottom";
554
566
  ctx.textAlign = side === "left" ? "right" : "left";
@@ -566,8 +578,7 @@ function drawXAxis({
566
578
  height,
567
579
  labels,
568
580
  indices,
569
- fontFamily,
570
- customFont,
581
+ fonts,
571
582
  tickFontSize
572
583
  }) {
573
584
  const plot = plotArea(padding, width, height);
@@ -579,7 +590,7 @@ function drawXAxis({
579
590
  ctx.lineTo(plot.x + plot.w, y);
580
591
  ctx.stroke();
581
592
  ctx.fillStyle = "#000";
582
- ctx.font = getFont(tickFontSize, fontFamily, customFont, "bold");
593
+ ctx.font = getFont(tickFontSize, fonts, "bold");
583
594
  ctx.textAlign = "center";
584
595
  ctx.textBaseline = "top";
585
596
  const showIndices = indices ?? labels.map((_, i) => i);
@@ -686,14 +697,17 @@ function drawInsideIcon(ctx, item, x, cy, size) {
686
697
  ctx.lineTo(x + w, cy);
687
698
  ctx.stroke();
688
699
  ctx.setLineDash([]);
689
- ctx.fillStyle = item.fillStyle;
700
+ ctx.fillStyle = item.hollow ? "#fff" : item.fillStyle;
701
+ if (item.hollow) ctx.lineWidth = 1.5;
690
702
  const r = size * 0.3;
691
703
  if (item.marker === "square") {
692
704
  ctx.fillRect(x + w / 2 - r, cy - r, r * 2, r * 2);
705
+ if (item.hollow) ctx.strokeRect(x + w / 2 - r, cy - r, r * 2, r * 2);
693
706
  } else {
694
707
  ctx.beginPath();
695
708
  ctx.arc(x + w / 2, cy, r, 0, Math.PI * 2);
696
709
  ctx.fill();
710
+ if (item.hollow) ctx.stroke();
697
711
  }
698
712
  return;
699
713
  }
@@ -704,14 +718,13 @@ function drawInsideIcon(ctx, item, x, cy, size) {
704
718
  ctx.strokeRect(x, cy - size / 2, size, size);
705
719
  }
706
720
  var LINE_ICON_SIZE = 36;
707
- var LEGEND_FONT = "'Noto Sans KR', sans-serif";
708
721
  var ICON_GAP = 10;
709
722
  var BOX_PADDING = 12;
710
723
  var ITEM_SPACING = 30;
711
724
  var ROW_GAP = 6;
712
- function measureLegendWidth(ctx, labels, fontSize, iconType = "rect") {
725
+ function measureLegendWidth(ctx, labels, fontSize, fonts, iconType = "rect") {
713
726
  ctx.save();
714
- ctx.font = `bold ${fontSize}px ${LEGEND_FONT}`;
727
+ ctx.font = `bold ${fontSize}px ${sansFont(fonts)}`;
715
728
  const iconSize = iconType === "line" ? LINE_ICON_SIZE : 16;
716
729
  const iconGap = 10;
717
730
  const padding = 12;
@@ -719,11 +732,12 @@ function measureLegendWidth(ctx, labels, fontSize, iconType = "rect") {
719
732
  ctx.restore();
720
733
  return maxW + padding * 2 + 30;
721
734
  }
722
- function layoutBottomLegend(ctx, labels, iconWidths, fontSize, boxW, opts = {}) {
735
+ function layoutBottomLegend(ctx, labels, iconWidths, fontSize, boxW, fonts, opts = {}) {
723
736
  const iconGap = opts.iconGap ?? ICON_GAP;
724
737
  const padding = opts.padding ?? BOX_PADDING;
725
738
  const spacing = opts.spacing ?? ITEM_SPACING;
726
- const fontOf = (fs2) => opts.font ? withFontSize(opts.font, fs2) : `bold ${fs2}px ${LEGEND_FONT}`;
739
+ const legendFont = sansFont(fonts);
740
+ const fontOf = (fs2) => opts.font ? withFontSize(opts.font, fs2) : `bold ${fs2}px ${legendFont}`;
727
741
  const inner = Math.max(1, boxW - padding * 2);
728
742
  ctx.save();
729
743
  const widthsAt = (fs2) => {
@@ -760,11 +774,11 @@ function layoutBottomLegend(ctx, labels, iconWidths, fontSize, boxW, opts = {})
760
774
  boxH: rows.length * lineHeight + (rows.length - 1) * ROW_GAP + padding * 2
761
775
  };
762
776
  }
763
- function measureBottomLegend(ctx, labels, fontSize, plotW, iconType = "rect", bottomOffset = 50) {
777
+ function measureBottomLegend(ctx, labels, fontSize, plotW, fonts, iconType = "rect", bottomOffset = 50) {
764
778
  if (labels.length === 0) return 0;
765
779
  const sizeOf = (t) => t === "line" ? LINE_ICON_SIZE : 16;
766
780
  const iconWidths = labels.map((_, i) => sizeOf(Array.isArray(iconType) ? iconType[i] ?? "rect" : iconType));
767
- const { boxH } = layoutBottomLegend(ctx, labels, iconWidths, fontSize, plotW);
781
+ const { boxH } = layoutBottomLegend(ctx, labels, iconWidths, fontSize, plotW, fonts);
768
782
  return bottomOffset + boxH + 2;
769
783
  }
770
784
  function drawLegend({
@@ -778,12 +792,14 @@ function drawLegend({
778
792
  canvasW,
779
793
  canvasH,
780
794
  fontSize,
795
+ fonts,
781
796
  bottomOffset = 50,
782
797
  rightGap = 20
783
798
  }) {
784
799
  if (items.length === 0) return 0;
800
+ const legendFont = sansFont(fonts);
785
801
  ctx.save();
786
- ctx.font = `bold ${fontSize}px ${LEGEND_FONT}`;
802
+ ctx.font = `bold ${fontSize}px ${legendFont}`;
787
803
  const iconGap = ICON_GAP;
788
804
  const padding = BOX_PADDING;
789
805
  const lineHeight = fontSize + 8;
@@ -796,7 +812,8 @@ function drawLegend({
796
812
  items.map((i) => i.label),
797
813
  items.map(iconWidthOf),
798
814
  fontSize,
799
- boxW
815
+ boxW,
816
+ fonts
800
817
  );
801
818
  const boxH = layout.boxH;
802
819
  const boxX = plotX;
@@ -817,7 +834,7 @@ function drawLegend({
817
834
  const item = items[index];
818
835
  const iSize = iconWidthOf(item);
819
836
  drawIcon(ctx, item, cx, cy, iSize);
820
- ctx.font = `bold ${layout.fontSize}px ${LEGEND_FONT}`;
837
+ ctx.font = `bold ${layout.fontSize}px ${legendFont}`;
821
838
  ctx.fillStyle = "#000";
822
839
  ctx.textAlign = "left";
823
840
  ctx.textBaseline = "middle";
@@ -846,7 +863,7 @@ function drawLegend({
846
863
  const ix = boxX + padding;
847
864
  const iSize = iconWidthOf(item);
848
865
  drawIcon(ctx, item, ix, cy, iSize);
849
- ctx.font = `bold ${fontSize}px ${LEGEND_FONT}`;
866
+ ctx.font = `bold ${fontSize}px ${legendFont}`;
850
867
  ctx.fillStyle = "#000";
851
868
  ctx.textAlign = "left";
852
869
  ctx.textBaseline = "middle";
@@ -1047,10 +1064,8 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1047
1064
  clearCanvas(ctx, w, h);
1048
1065
  const showLegend = options.showLegend;
1049
1066
  const legendPos = options.legendPosition;
1050
- const legendW = showLegend && legendPos === "right" && !data.insideLegend ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
1067
+ const legendW = showLegend && legendPos === "right" && !data.insideLegend ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
1051
1068
  const isVertical = data.barDirection === "vertical";
1052
- const font = options.fontFamily;
1053
- const customFont = options.customFont;
1054
1069
  const n = data.categories.length;
1055
1070
  const sCount = data.seriesLabels.length;
1056
1071
  const hasGroups = !isVertical && !!data.groups && data.groups.length > 0;
@@ -1059,18 +1074,18 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1059
1074
  let unitW = 0;
1060
1075
  if (hasGroups || data.unitAdjacent || data.insideLegend) {
1061
1076
  ctx.save();
1062
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
1077
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
1063
1078
  if (hasGroups) {
1064
1079
  groupLabelW = widestLabel(ctx, data.groups.map((g) => g.label));
1065
1080
  catLabelW = widestLabel(ctx, data.categories.map((c) => c.label));
1066
1081
  }
1067
- ctx.font = getFont(options.fontSize.axisLabel, font, customFont, "bold");
1082
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
1068
1083
  unitW = ctx.measureText(data.unit).width;
1069
1084
  ctx.restore();
1070
1085
  }
1071
1086
  const padRight = isVertical ? 60 + legendW : data.unitAdjacent ? 40 + unitW : 160 + legendW;
1072
1087
  const padLeft = isVertical ? 130 : hasGroups ? 20 + groupLabelW + 14 + catLabelW + 12 : 100;
1073
- const legendReserve = showLegend && legendPos === "bottom" && !data.insideLegend ? measureBottomLegend(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, w - padLeft - padRight) : 0;
1088
+ const legendReserve = showLegend && legendPos === "bottom" && !data.insideLegend ? measureBottomLegend(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, w - padLeft - padRight, options) : 0;
1074
1089
  const padding = {
1075
1090
  top: options.title ? 100 : 50,
1076
1091
  right: padRight,
@@ -1134,8 +1149,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1134
1149
  step: axis.step,
1135
1150
  label: data.unit,
1136
1151
  side: "left",
1137
- fontFamily: font,
1138
- customFont,
1152
+ fonts: options,
1139
1153
  tickFontSize: options.fontSize.tick,
1140
1154
  labelFontSize: options.fontSize.axisLabel,
1141
1155
  drawGrid: true
@@ -1165,7 +1179,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1165
1179
  ctx.strokeRect(cx - barW / 2, y, barW, barH);
1166
1180
  if (options.showDataLabels && barH > options.fontSize.dataLabel) {
1167
1181
  ctx.fillStyle = isLightFill(s) ? "#000" : "#fff";
1168
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
1182
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
1169
1183
  ctx.textAlign = "center";
1170
1184
  ctx.textBaseline = "middle";
1171
1185
  ctx.fillText(String(val), cx, y + barH / 2);
@@ -1191,7 +1205,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1191
1205
  ctx.strokeRect(bx, by, barW, barH);
1192
1206
  if (options.showDataLabels && barH > options.fontSize.dataLabel) {
1193
1207
  ctx.fillStyle = isLightFill(s) ? "#000" : "#fff";
1194
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
1208
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
1195
1209
  ctx.textAlign = "center";
1196
1210
  ctx.textBaseline = "bottom";
1197
1211
  ctx.fillText(String(val), bx + barW / 2, by - 4);
@@ -1211,7 +1225,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1211
1225
  }
1212
1226
  const labelY = data.categoryLabelAtBaseline ? baseY + 6 : plotY + plotH + 12;
1213
1227
  ctx.fillStyle = "#000";
1214
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
1228
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
1215
1229
  ctx.textAlign = "center";
1216
1230
  ctx.textBaseline = "top";
1217
1231
  const catStride = labelStride(catArea, widestLabel(ctx, data.categories.map((c) => c.label)));
@@ -1235,7 +1249,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1235
1249
  if (ticks.length === 0 || Math.abs(ticks[ticks.length - 1] - axis.max) > 1e-9) {
1236
1250
  ticks.push(axis.max);
1237
1251
  }
1238
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
1252
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
1239
1253
  ctx.fillStyle = "#000";
1240
1254
  ctx.textAlign = "center";
1241
1255
  ctx.textBaseline = "top";
@@ -1263,7 +1277,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1263
1277
  ctx.restore();
1264
1278
  }
1265
1279
  }
1266
- ctx.font = getFont(options.fontSize.axisLabel, font, customFont, "bold");
1280
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
1267
1281
  ctx.fillStyle = "#000";
1268
1282
  ctx.textAlign = "left";
1269
1283
  ctx.textBaseline = "top";
@@ -1303,7 +1317,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1303
1317
  ctx.strokeRect(bx, cy - barH / 2, bw, barH);
1304
1318
  if (options.showDataLabels && bw > options.fontSize.dataLabel * 2) {
1305
1319
  ctx.fillStyle = isLightFill(s) ? "#000" : "#fff";
1306
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
1320
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
1307
1321
  ctx.textAlign = "center";
1308
1322
  ctx.textBaseline = "middle";
1309
1323
  ctx.fillText(String(val), bx + bw / 2, cy);
@@ -1329,7 +1343,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1329
1343
  ctx.strokeRect(bx, by, bw, barH);
1330
1344
  if (options.showDataLabels && bw > options.fontSize.dataLabel * 2) {
1331
1345
  ctx.fillStyle = isLightFill(s) ? "#000" : "#fff";
1332
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
1346
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
1333
1347
  ctx.textAlign = "left";
1334
1348
  ctx.textBaseline = "middle";
1335
1349
  ctx.fillText(String(val), bx + bw + 4, by + barH / 2);
@@ -1341,7 +1355,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1341
1355
  const catLabelX = hasGroups ? plotX - 12 : plotX - 10;
1342
1356
  for (let c = 0; c < n; c++) {
1343
1357
  ctx.fillStyle = "#000";
1344
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
1358
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
1345
1359
  ctx.textAlign = "right";
1346
1360
  ctx.textBaseline = "middle";
1347
1361
  ctx.fillText(data.categories[c].label, catLabelX, centerY[c]);
@@ -1355,7 +1369,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1355
1369
  }
1356
1370
  }
1357
1371
  }
1358
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
1372
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
1359
1373
  if (showLegend && data.insideLegend) {
1360
1374
  drawInsideLegend({
1361
1375
  ctx,
@@ -1372,7 +1386,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1372
1386
  canvasW: w,
1373
1387
  canvasH: h,
1374
1388
  fontSize: options.fontSize.dataLabel * 0.9,
1375
- font: getFont(options.fontSize.dataLabel * 0.9, font, customFont, "bold"),
1389
+ font: getFont(options.fontSize.dataLabel * 0.9, options, "bold"),
1376
1390
  avoid: barRects
1377
1391
  });
1378
1392
  } else if (showLegend) {
@@ -1385,6 +1399,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1385
1399
  }));
1386
1400
  drawLegend({
1387
1401
  ctx,
1402
+ fonts: options,
1388
1403
  items,
1389
1404
  position: legendPos,
1390
1405
  plotX,
@@ -1396,7 +1411,7 @@ function renderAbsBarGraph(ctx, w, h, data, options) {
1396
1411
  fontSize: options.fontSize.dataLabel * 0.85 + 5
1397
1412
  });
1398
1413
  }
1399
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
1414
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
1400
1415
  }
1401
1416
  function formatTick2(val) {
1402
1417
  if (Number.isInteger(val)) return val.toString();
@@ -1411,6 +1426,7 @@ function createDefaultGraphOptions() {
1411
1426
  footnotes: [""],
1412
1427
  fontFamily: "serif",
1413
1428
  customFont: "",
1429
+ fontStack: {},
1414
1430
  fontSize: {
1415
1431
  title: 36,
1416
1432
  axisLabel: 28,
@@ -1593,6 +1609,60 @@ function createDefaultCubeData() {
1593
1609
  };
1594
1610
  }
1595
1611
 
1612
+ // src/core/types/econplane.ts
1613
+ function createSupplyDemandData() {
1614
+ return {
1615
+ quadrants: "first",
1616
+ xAxis: { label: "\uC218\uB7C9(kg)", min: 0, max: 9, ticks: [1, 2, 3, 4, 5, 6, 7, 8], broken: false },
1617
+ yAxis: { label: "\uAC00\uACA9(\uCC9C \uC6D0)", min: 0, max: 9, ticks: [1, 2, 3, 4, 5, 6, 7, 8], broken: false },
1618
+ grid: true,
1619
+ dash: "dashed",
1620
+ lines: [
1621
+ { label: "\uC218\uC694", from: { x: 0, y: 8 }, to: { x: 8, y: 0 }, labelAt: "to" },
1622
+ { label: "\uACF5\uAE09", from: { x: 0, y: 0 }, to: { x: 8, y: 8 }, labelAt: "to" }
1623
+ ],
1624
+ points: [{ x: 4, y: 4, label: "E", labelPos: "top", guide: "none", dot: true }],
1625
+ arrows: []
1626
+ };
1627
+ }
1628
+ function createAdAsData() {
1629
+ return {
1630
+ quadrants: "first",
1631
+ xAxis: { label: "\uC2E4\uC9C8 GDP", min: 0, max: 10, ticks: [], broken: false },
1632
+ yAxis: { label: "\uBB3C\uAC00", min: 0, max: 10, ticks: [], broken: false },
1633
+ grid: false,
1634
+ dash: "dashed",
1635
+ lines: [
1636
+ { label: "\uCD1D\uC218\uC694", from: { x: 1, y: 8.5 }, to: { x: 8.5, y: 1 }, labelAt: "to" },
1637
+ { label: "\uCD1D\uACF5\uAE09", from: { x: 1, y: 1 }, to: { x: 8.5, y: 8.5 }, labelAt: "to" }
1638
+ ],
1639
+ points: [{ x: 4.75, y: 4.75, label: "E", labelPos: "right", guide: "none", dot: true }],
1640
+ arrows: []
1641
+ };
1642
+ }
1643
+ function createPointShiftData() {
1644
+ return {
1645
+ quadrants: "first",
1646
+ xAxis: { label: "\uC218\uB7C9(\uB9CC \uAC1C)", min: 0, max: 18, ticks: [5, 10, 15], broken: false },
1647
+ yAxis: { label: "\uAC00\uACA9(\uB2EC\uB7EC)", min: 0, max: 18, ticks: [10, 15], broken: false },
1648
+ grid: false,
1649
+ dash: "dashed",
1650
+ lines: [],
1651
+ points: [
1652
+ { x: 5, y: 10, label: "E", labelPos: "top", guide: "both", dot: true },
1653
+ { x: 10, y: 10, label: "A", labelPos: "top", guide: "both", dot: true },
1654
+ { x: 15, y: 15, label: "B", labelPos: "top", guide: "both", dot: true }
1655
+ ],
1656
+ arrows: [
1657
+ { from: { x: 5, y: 10 }, to: { x: 10, y: 10 }, offset: 0, shorten: 10, label: "", labelPos: "top" },
1658
+ { from: { x: 10, y: 10 }, to: { x: 15, y: 15 }, offset: 0, shorten: 10, label: "", labelPos: "top" }
1659
+ ]
1660
+ };
1661
+ }
1662
+ function createDefaultEconPlaneData() {
1663
+ return createSupplyDemandData();
1664
+ }
1665
+
1596
1666
  // src/core/types/radar.ts
1597
1667
  function createDefaultRadarData() {
1598
1668
  return {
@@ -1713,15 +1783,13 @@ function drawMarker(ctx, marker, cx, cy, r) {
1713
1783
  }
1714
1784
  function renderCategoryDotGraph(ctx, w, h, data, options) {
1715
1785
  clearCanvas(ctx, w, h);
1716
- const font = options.fontFamily;
1717
- const customFont = options.customFont;
1718
1786
  const n = data.categories.length;
1719
1787
  const sCount = data.seriesLabels.length;
1720
1788
  const markers = data.seriesMarkers ?? DOT_MARKER_ORDER;
1721
1789
  const showLegend = options.showLegend && sCount > 1;
1722
1790
  const legendPos = options.legendPosition;
1723
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, "circle") : 0;
1724
- const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85, w - 130 - (60 + legendW), "circle") : 0;
1791
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, options, "circle") : 0;
1792
+ const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85, w - 130 - (60 + legendW), options, "circle") : 0;
1725
1793
  const padding = {
1726
1794
  top: options.title ? 100 : 50,
1727
1795
  right: 60 + legendW,
@@ -1766,8 +1834,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1766
1834
  step: axis.step,
1767
1835
  label: data.unit,
1768
1836
  side: "left",
1769
- fontFamily: font,
1770
- customFont,
1837
+ fonts: options,
1771
1838
  tickFontSize: options.fontSize.tick,
1772
1839
  labelFontSize: options.fontSize.axisLabel,
1773
1840
  drawGrid: true
@@ -1790,7 +1857,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1790
1857
  ctx.fillStyle = "#000";
1791
1858
  drawMarker(ctx, markers[s % markers.length], cx, cy, data.dotRadius);
1792
1859
  if (options.showDataLabels) {
1793
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
1860
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
1794
1861
  ctx.textAlign = "center";
1795
1862
  ctx.textBaseline = "bottom";
1796
1863
  ctx.fillText(String(val), cx, cy - data.dotRadius - 4);
@@ -1811,7 +1878,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1811
1878
  }
1812
1879
  }
1813
1880
  ctx.fillStyle = "#000";
1814
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
1881
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
1815
1882
  ctx.textAlign = "center";
1816
1883
  ctx.textBaseline = "top";
1817
1884
  for (let c = 0; c < n; c++) {
@@ -1819,7 +1886,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1819
1886
  ctx.fillText(data.categories[c].label, cx, plotY + plotH + 12);
1820
1887
  }
1821
1888
  if (options.title) {
1822
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title });
1889
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title });
1823
1890
  }
1824
1891
  if (showLegend) {
1825
1892
  const items = data.seriesLabels.map((label) => ({
@@ -1829,6 +1896,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1829
1896
  }));
1830
1897
  drawLegend({
1831
1898
  ctx,
1899
+ fonts: options,
1832
1900
  items,
1833
1901
  position: legendPos,
1834
1902
  plotX,
@@ -1842,6 +1910,7 @@ function renderCategoryDotGraph(ctx, w, h, data, options) {
1842
1910
  }
1843
1911
  drawSourceAndFootnote({
1844
1912
  ctx,
1913
+ fonts: options,
1845
1914
  plotX,
1846
1915
  plotW,
1847
1916
  height: h,
@@ -1885,15 +1954,13 @@ function drawMarker2(ctx, marker, cx, cy, r, hollow) {
1885
1954
  }
1886
1955
  function renderLineGraph(ctx, w, h, data, options) {
1887
1956
  clearCanvas(ctx, w, h);
1888
- const font = options.fontFamily;
1889
- const customFont = options.customFont;
1890
1957
  const n = data.xLabels.length;
1891
1958
  const useLegend = data.labelPlacement === "legend" && options.showLegend;
1892
1959
  const legendPos = options.legendPosition;
1893
- const legendW = useLegend && legendPos === "right" ? measureLegendWidth(ctx, data.series.map((s) => s.label), options.fontSize.dataLabel * 0.85 + 5, "line") : 0;
1894
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
1960
+ const legendW = useLegend && legendPos === "right" ? measureLegendWidth(ctx, data.series.map((s) => s.label), options.fontSize.dataLabel * 0.85 + 5, options, "line") : 0;
1961
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
1895
1962
  const endLabelW = data.labelPlacement === "lineEnd" ? widestLabel(ctx, data.series.map((s) => s.label)) + 16 : 0;
1896
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
1963
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
1897
1964
  const xUnitW = data.xUnit ? ctx.measureText(data.xUnit).width + ctx.measureText(data.xLabels[n - 1] ?? "").width / 2 + 12 : 0;
1898
1965
  const padRight = 24 + legendW + Math.max(endLabelW, xUnitW);
1899
1966
  const legendReserve = useLegend && legendPos === "bottom" && !data.insideLegend ? measureBottomLegend(
@@ -1901,6 +1968,7 @@ function renderLineGraph(ctx, w, h, data, options) {
1901
1968
  data.series.map((s) => s.label),
1902
1969
  options.fontSize.dataLabel * 0.85,
1903
1970
  w - 130 - padRight,
1971
+ options,
1904
1972
  "line"
1905
1973
  ) : 0;
1906
1974
  const padding = {
@@ -1949,8 +2017,7 @@ function renderLineGraph(ctx, w, h, data, options) {
1949
2017
  step: axis.step,
1950
2018
  label: data.yUnit,
1951
2019
  side: "left",
1952
- fontFamily: font,
1953
- customFont,
2020
+ fonts: options,
1954
2021
  tickFontSize: options.fontSize.tick,
1955
2022
  labelFontSize: options.fontSize.axisLabel,
1956
2023
  drawGrid: true
@@ -2042,7 +2109,7 @@ function renderLineGraph(ctx, w, h, data, options) {
2042
2109
  }
2043
2110
  if (data.labelPlacement === "lineEnd") {
2044
2111
  ctx.fillStyle = "#000";
2045
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
2112
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
2046
2113
  const placer = new LabelPlacer();
2047
2114
  const lineHeight = options.fontSize.dataLabel * 1.1;
2048
2115
  const columnX = plotX + plotW + 10;
@@ -2072,7 +2139,7 @@ function renderLineGraph(ctx, w, h, data, options) {
2072
2139
  });
2073
2140
  }
2074
2141
  ctx.fillStyle = "#000";
2075
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
2142
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
2076
2143
  ctx.textAlign = "center";
2077
2144
  ctx.textBaseline = "top";
2078
2145
  const xStride = labelStride(stepX, widestLabel(ctx, data.xLabels));
@@ -2086,11 +2153,11 @@ function renderLineGraph(ctx, w, h, data, options) {
2086
2153
  ctx.fillText(data.xUnit, plotX + plotW + lastLabelHalf + 6, plotY + plotH + 10);
2087
2154
  }
2088
2155
  if (options.title) {
2089
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2156
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2090
2157
  }
2091
2158
  if (useLegend && data.insideLegend) {
2092
2159
  ctx.save();
2093
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
2160
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
2094
2161
  const rowH = options.fontSize.dataLabel * 1.15;
2095
2162
  const swatch = rowH * 0.75;
2096
2163
  const boxW = swatch + 6 + widestLabel(ctx, data.series.map((s) => s.label)) + 10;
@@ -2122,6 +2189,7 @@ function renderLineGraph(ctx, w, h, data, options) {
2122
2189
  }));
2123
2190
  drawLegend({
2124
2191
  ctx,
2192
+ fonts: options,
2125
2193
  items,
2126
2194
  position: legendPos,
2127
2195
  plotX,
@@ -2135,6 +2203,7 @@ function renderLineGraph(ctx, w, h, data, options) {
2135
2203
  }
2136
2204
  drawSourceAndFootnote({
2137
2205
  ctx,
2206
+ fonts: options,
2138
2207
  plotX,
2139
2208
  plotW,
2140
2209
  height: h,
@@ -2155,9 +2224,8 @@ function renderMatrixTable(ctx, w, h, data, options) {
2155
2224
  const n = data.names.length;
2156
2225
  if (n === 0) return;
2157
2226
  const font = options.fontFamily;
2158
- const customFont = options.customFont;
2159
2227
  const base = options.fontSize.tick;
2160
- ctx.font = getFont(base, font, customFont, "bold");
2228
+ ctx.font = getFont(base, options, "bold");
2161
2229
  const widest = Math.max(
2162
2230
  ...data.names.map((t) => ctx.measureText(t).width),
2163
2231
  ...cellValues(data).map((v) => ctx.measureText(formatValue(v, data.groupThousands)).width)
@@ -2184,7 +2252,7 @@ function renderMatrixTable(ctx, w, h, data, options) {
2184
2252
  const centered = top + unitH + Math.max(0, (h - top - unitH - footH - tableH) / 2);
2185
2253
  const tableY = Math.max(top + unitH, Math.min(centered, h - footH - tableH));
2186
2254
  if (data.unit) {
2187
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
2255
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
2188
2256
  ctx.fillStyle = "#000";
2189
2257
  ctx.textAlign = "right";
2190
2258
  ctx.textBaseline = "bottom";
@@ -2204,11 +2272,11 @@ function renderMatrixTable(ctx, w, h, data, options) {
2204
2272
  ctx.fillStyle = "#000";
2205
2273
  if (isName) {
2206
2274
  const family = data.nameIsSymbol && !data.nameIsSymbol[i] ? "sans" : font;
2207
- ctx.font = getFont(cellFontSize, family, customFont, "bold");
2275
+ ctx.font = getFont(cellFontSize, options, "bold", family);
2208
2276
  ctx.textAlign = "center";
2209
2277
  ctx.fillText(data.names[i], x + cellW / 2, y + cellH / 2);
2210
2278
  } else {
2211
- ctx.font = getFont(cellFontSize, font, customFont, "bold");
2279
+ ctx.font = getFont(cellFontSize, options, "bold");
2212
2280
  ctx.textAlign = "right";
2213
2281
  ctx.fillText(
2214
2282
  formatValue(data.values[i]?.[j] ?? 0, data.groupThousands),
@@ -2218,9 +2286,10 @@ function renderMatrixTable(ctx, w, h, data, options) {
2218
2286
  }
2219
2287
  }
2220
2288
  }
2221
- drawTitle({ ctx, plotX: tableX, plotW: tableW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2289
+ drawTitle({ ctx, fonts: options, plotX: tableX, plotW: tableW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2222
2290
  drawSourceAndFootnote({
2223
2291
  ctx,
2292
+ fonts: options,
2224
2293
  plotX: tableX,
2225
2294
  plotW: tableW,
2226
2295
  height: h,
@@ -2254,12 +2323,11 @@ function renderDataTable(ctx, w, h, data, options) {
2254
2323
  const rows = data.rows.length;
2255
2324
  if (cols === 0 || rows === 0) return;
2256
2325
  const font = options.fontFamily;
2257
- const customFont = options.customFont;
2258
2326
  const base = options.fontSize.tick;
2259
- ctx.font = getFont(base, font, customFont, "bold");
2327
+ ctx.font = getFont(base, options, "bold");
2260
2328
  const labelTextW = Math.max(
2261
2329
  ctx.measureText(data.cornerLabel).width,
2262
- ...data.rows.map((r) => rowLabelWidth(ctx, r, base, font, customFont))
2330
+ ...data.rows.map((r) => rowLabelWidth(ctx, r, base, options))
2263
2331
  );
2264
2332
  const valueTextW = Math.max(
2265
2333
  ...data.columns.map((c) => ctx.measureText(c).width),
@@ -2294,17 +2362,17 @@ function renderDataTable(ctx, w, h, data, options) {
2294
2362
  ctx.fillStyle = "#000";
2295
2363
  ctx.textBaseline = "middle";
2296
2364
  ctx.textAlign = "center";
2297
- ctx.font = getFont(cellFontSize, font, customFont, "bold");
2365
+ ctx.font = getFont(cellFontSize, options, "bold");
2298
2366
  ctx.fillText(data.cornerLabel, tableX + labelW / 2, tableY + cellH / 2, labelW - pad * 2);
2299
2367
  data.columns.forEach((name, j) => {
2300
2368
  const family = data.columnIsSymbol && !data.columnIsSymbol[j] ? "sans" : font;
2301
- ctx.font = getFont(cellFontSize, family, customFont, "bold");
2369
+ ctx.font = getFont(cellFontSize, options, "bold", family);
2302
2370
  ctx.fillText(name, columnX(j) + valueW / 2, tableY + cellH / 2, valueW - pad * 2);
2303
2371
  });
2304
2372
  data.rows.forEach((row, i) => {
2305
2373
  const y = tableY + cellH * (i + 1);
2306
- drawRowLabel(ctx, row, tableX + labelW / 2, y + cellH / 2, cellFontSize, font, customFont);
2307
- ctx.font = getFont(cellFontSize, font, customFont, "bold");
2374
+ drawRowLabel(ctx, row, tableX + labelW / 2, y + cellH / 2, cellFontSize, options);
2375
+ ctx.font = getFont(cellFontSize, options, "bold");
2308
2376
  ctx.textAlign = "right";
2309
2377
  for (let j = 0; j < cols; j++) {
2310
2378
  const v = row.values[j];
@@ -2330,9 +2398,10 @@ function renderDataTable(ctx, w, h, data, options) {
2330
2398
  }
2331
2399
  ctx.lineWidth = 2;
2332
2400
  ctx.strokeRect(tableX, tableY, tableW, tableH);
2333
- drawTitle({ ctx, plotX: tableX, plotW: tableW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2401
+ drawTitle({ ctx, fonts: options, plotX: tableX, plotW: tableW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2334
2402
  drawSourceAndFootnote({
2335
2403
  ctx,
2404
+ fonts: options,
2336
2405
  plotX: tableX,
2337
2406
  plotW: tableW,
2338
2407
  height: h,
@@ -2348,30 +2417,30 @@ function line(ctx, x1, y1, x2, y2) {
2348
2417
  ctx.lineTo(x2, y2);
2349
2418
  ctx.stroke();
2350
2419
  }
2351
- function rowLabelWidth(ctx, row, size, font, customFont) {
2352
- ctx.font = getFont(size, font, customFont, "bold");
2420
+ function rowLabelWidth(ctx, row, size, options) {
2421
+ ctx.font = getFont(size, options, "bold");
2353
2422
  const labelW = ctx.measureText(row.label).width;
2354
2423
  if (!row.unit) return labelW;
2355
- ctx.font = getFont(size * UNIT_RATIO, font, customFont, "bold");
2424
+ ctx.font = getFont(size * UNIT_RATIO, options, "bold");
2356
2425
  return labelW + ctx.measureText(row.unit).width;
2357
2426
  }
2358
- function drawRowLabel(ctx, row, cx, cy, size, font, customFont) {
2427
+ function drawRowLabel(ctx, row, cx, cy, size, options) {
2359
2428
  ctx.fillStyle = "#000";
2360
2429
  ctx.textBaseline = "middle";
2361
- ctx.font = getFont(size, font, customFont, "bold");
2430
+ ctx.font = getFont(size, options, "bold");
2362
2431
  const labelW = ctx.measureText(row.label).width;
2363
2432
  if (!row.unit) {
2364
2433
  ctx.textAlign = "center";
2365
2434
  ctx.fillText(row.label, cx, cy);
2366
2435
  return;
2367
2436
  }
2368
- ctx.font = getFont(size * UNIT_RATIO, font, customFont, "bold");
2437
+ ctx.font = getFont(size * UNIT_RATIO, options, "bold");
2369
2438
  const unitW = ctx.measureText(row.unit).width;
2370
2439
  const startX = cx - (labelW + unitW) / 2;
2371
2440
  ctx.textAlign = "left";
2372
- ctx.font = getFont(size, font, customFont, "bold");
2441
+ ctx.font = getFont(size, options, "bold");
2373
2442
  ctx.fillText(row.label, startX, cy);
2374
- ctx.font = getFont(size * UNIT_RATIO, font, customFont, "bold");
2443
+ ctx.font = getFont(size * UNIT_RATIO, options, "bold");
2375
2444
  ctx.fillText(row.unit, startX + labelW, cy);
2376
2445
  ctx.textAlign = "center";
2377
2446
  }
@@ -2384,6 +2453,571 @@ function formatValue2(v, decimals = 1, groupThousands = true) {
2384
2453
  return frac ? `${grouped}.${frac}` : grouped;
2385
2454
  }
2386
2455
 
2456
+ // src/core/canvas/subscript.ts
2457
+ var SUB_SCALE = 0.65;
2458
+ var SUB_DROP = 0.16;
2459
+ var SUB_CHARS = /[0-9A-Za-z]/;
2460
+ var HAS_SUB = /_[0-9A-Za-z]/;
2461
+ function parseRich(text) {
2462
+ if (!HAS_SUB.test(text)) return [{ text, sub: false }];
2463
+ const runs = [];
2464
+ let plain = "";
2465
+ for (let i = 0; i < text.length; i++) {
2466
+ if (text[i] === "_" && i + 1 < text.length && SUB_CHARS.test(text[i + 1])) {
2467
+ let j = i + 1;
2468
+ while (j < text.length && SUB_CHARS.test(text[j])) j++;
2469
+ if (plain) {
2470
+ runs.push({ text: plain, sub: false });
2471
+ plain = "";
2472
+ }
2473
+ runs.push({ text: text.slice(i + 1, j), sub: true });
2474
+ i = j - 1;
2475
+ continue;
2476
+ }
2477
+ plain += text[i];
2478
+ }
2479
+ if (plain) runs.push({ text: plain, sub: false });
2480
+ return runs;
2481
+ }
2482
+ function isPlain(runs) {
2483
+ return runs.length === 1 && !runs[0].sub;
2484
+ }
2485
+ function richWidth(ctx, text, size, makeFont) {
2486
+ const runs = parseRich(text);
2487
+ const base = makeFont(size);
2488
+ if (isPlain(runs)) {
2489
+ ctx.font = base;
2490
+ return ctx.measureText(text).width;
2491
+ }
2492
+ const sub = makeFont(size * SUB_SCALE);
2493
+ let w = 0;
2494
+ for (const r of runs) {
2495
+ ctx.font = r.sub ? sub : base;
2496
+ w += ctx.measureText(r.text).width;
2497
+ }
2498
+ ctx.font = base;
2499
+ return w;
2500
+ }
2501
+ function richExtent(ctx, text, size, makeFont) {
2502
+ const runs = parseRich(text);
2503
+ const base = makeFont(size);
2504
+ if (isPlain(runs)) {
2505
+ ctx.font = base;
2506
+ return textExtent(ctx, text);
2507
+ }
2508
+ const subFont = makeFont(size * SUB_SCALE);
2509
+ const drop = size * SUB_DROP;
2510
+ let w = 0;
2511
+ let up = 0;
2512
+ let down = 0;
2513
+ for (const r of runs) {
2514
+ ctx.font = r.sub ? subFont : base;
2515
+ const e = textExtent(ctx, r.text);
2516
+ w += e.left + e.right;
2517
+ up = Math.max(up, r.sub ? e.up - drop : e.up);
2518
+ down = Math.max(down, r.sub ? e.down + drop : e.down);
2519
+ }
2520
+ ctx.font = base;
2521
+ const left = ctx.textAlign === "right" || ctx.textAlign === "end" ? w : ctx.textAlign === "center" ? w / 2 : 0;
2522
+ return { left, right: w - left, up, down };
2523
+ }
2524
+ function fillRich(ctx, text, x, y, size, makeFont) {
2525
+ const runs = parseRich(text);
2526
+ const base = makeFont(size);
2527
+ if (isPlain(runs)) {
2528
+ ctx.font = base;
2529
+ ctx.fillText(text, x, y);
2530
+ return;
2531
+ }
2532
+ const subFont = makeFont(size * SUB_SCALE);
2533
+ const drop = size * SUB_DROP;
2534
+ let total = 0;
2535
+ for (const r of runs) {
2536
+ ctx.font = r.sub ? subFont : base;
2537
+ total += ctx.measureText(r.text).width;
2538
+ }
2539
+ const align = ctx.textAlign;
2540
+ let cx = align === "right" || align === "end" ? x - total : align === "center" ? x - total / 2 : x;
2541
+ ctx.textAlign = "left";
2542
+ for (const r of runs) {
2543
+ ctx.font = r.sub ? subFont : base;
2544
+ ctx.fillText(r.text, cx, r.sub ? y + drop : y);
2545
+ cx += ctx.measureText(r.text).width;
2546
+ }
2547
+ ctx.textAlign = align;
2548
+ ctx.font = base;
2549
+ }
2550
+ function shrinkRichToWidth(ctx, texts, size, available, makeFont, minScale = MIN_SCALE) {
2551
+ if (available <= 0 || texts.length === 0) return size;
2552
+ const saved = ctx.font;
2553
+ const w = Math.max(...texts.map((t) => richWidth(ctx, t, size, makeFont)));
2554
+ ctx.font = saved;
2555
+ if (w <= available) return size;
2556
+ return Math.max(size * minScale, size * (available / w));
2557
+ }
2558
+ function nudgeRichInside(ctx, text, x, y, canvasW, canvasH, size, makeFont, margin = EDGE) {
2559
+ const e = richExtent(ctx, text, size, makeFont);
2560
+ let nx = x;
2561
+ let ny = y;
2562
+ if (x - e.left < margin) nx = margin + e.left;
2563
+ else if (x + e.right > canvasW - margin) nx = canvasW - margin - e.right;
2564
+ if (y - e.up < margin) ny = margin + e.up;
2565
+ else if (y + e.down > canvasH - margin) ny = canvasH - margin - e.down;
2566
+ return { x: nx, y: ny };
2567
+ }
2568
+ function drawFloatingRich(ctx, text, x, y, canvasW, canvasH, fontSize, makeFont) {
2569
+ if (!text) return;
2570
+ if (!HAS_SUB.test(text)) {
2571
+ drawFloatingLabel(ctx, text, x, y, canvasW, canvasH, fontSize, makeFont);
2572
+ return;
2573
+ }
2574
+ const size = shrinkRichToWidth(ctx, [text], fontSize, canvasW - EDGE * 2, makeFont);
2575
+ const at = nudgeRichInside(ctx, text, x, y, canvasW, canvasH, size, makeFont);
2576
+ fillRich(ctx, text, at.x, at.y, size, makeFont);
2577
+ }
2578
+
2579
+ // src/core/graphs/EconPlane.ts
2580
+ var ARROW_EXT = 26;
2581
+ var AXIS_W = 2.5;
2582
+ var LINE_W = 3;
2583
+ var GUIDE_W = 1.5;
2584
+ var ARROW_W = 2.5;
2585
+ var DOT_R = 6;
2586
+ var HEAD_LEN = 14;
2587
+ var HEAD_HALF = 6;
2588
+ var LABEL_GAP = 13;
2589
+ var NAME_LINE_H = 1.15;
2590
+ var DASH_PATTERN = {
2591
+ dashed: [7, 5],
2592
+ dotted: [1.5, 4]
2593
+ };
2594
+ var THICK_DASH = [12, 8];
2595
+ var clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
2596
+ function formatTick3(v) {
2597
+ const s = Number.isInteger(v) ? String(v) : v.toFixed(1);
2598
+ return s.startsWith("-") ? "\u2212" + s.slice(1) : s;
2599
+ }
2600
+ function tickText(axis, i) {
2601
+ const given = Array.isArray(axis.tickLabels) ? axis.tickLabels[i] : void 0;
2602
+ return typeof given === "string" && given !== "" ? given : formatTick3(axis.ticks[i]);
2603
+ }
2604
+ function splitLines(text) {
2605
+ return text ? text.split(/\\n|\n/) : [""];
2606
+ }
2607
+ function span(axis) {
2608
+ const d = axis.max - axis.min;
2609
+ return Number.isFinite(d) && Math.abs(d) > 1e-9 ? d : 1;
2610
+ }
2611
+ function seriesOf(data) {
2612
+ return Array.isArray(data.series) ? data.series.filter((s) => s && Array.isArray(s.points)) : [];
2613
+ }
2614
+ function drawOrder(series) {
2615
+ return series.length > 1 ? [...series].reverse() : series;
2616
+ }
2617
+ function drawSeriesMarker(ctx, s, cx, cy) {
2618
+ ctx.beginPath();
2619
+ if (s.marker === "square") ctx.rect(cx - DOT_R, cy - DOT_R, DOT_R * 2, DOT_R * 2);
2620
+ else ctx.arc(cx, cy, DOT_R, 0, Math.PI * 2);
2621
+ ctx.fillStyle = s.hollow ? "#fff" : "#000";
2622
+ ctx.fill();
2623
+ if (s.hollow) {
2624
+ ctx.strokeStyle = "#000";
2625
+ ctx.lineWidth = 2;
2626
+ ctx.setLineDash([]);
2627
+ ctx.stroke();
2628
+ }
2629
+ }
2630
+ function anchorOf(pos, gap) {
2631
+ const d = gap * 0.75;
2632
+ switch (pos) {
2633
+ case "left":
2634
+ return { dx: -gap, dy: 0, align: "right", baseline: "middle" };
2635
+ case "top":
2636
+ return { dx: 0, dy: -gap, align: "center", baseline: "bottom" };
2637
+ case "bottom":
2638
+ return { dx: 0, dy: gap, align: "center", baseline: "top" };
2639
+ case "top-left":
2640
+ return { dx: -d, dy: -d, align: "right", baseline: "bottom" };
2641
+ case "top-right":
2642
+ return { dx: d, dy: -d, align: "left", baseline: "bottom" };
2643
+ case "bottom-left":
2644
+ return { dx: -d, dy: d, align: "right", baseline: "top" };
2645
+ case "bottom-right":
2646
+ return { dx: d, dy: d, align: "left", baseline: "top" };
2647
+ default:
2648
+ return { dx: gap, dy: 0, align: "left", baseline: "middle" };
2649
+ }
2650
+ }
2651
+ function drawArrowHead(ctx, x1, y1, x2, y2) {
2652
+ const dx = x2 - x1;
2653
+ const dy = y2 - y1;
2654
+ const len = Math.hypot(dx, dy);
2655
+ if (!(len > 0)) return;
2656
+ const ux = dx / len;
2657
+ const uy = dy / len;
2658
+ ctx.beginPath();
2659
+ ctx.moveTo(x2, y2);
2660
+ ctx.lineTo(x2 - ux * HEAD_LEN - uy * HEAD_HALF, y2 - uy * HEAD_LEN + ux * HEAD_HALF);
2661
+ ctx.lineTo(x2 - ux * HEAD_LEN + uy * HEAD_HALF, y2 - uy * HEAD_LEN - ux * HEAD_HALF);
2662
+ ctx.closePath();
2663
+ ctx.fill();
2664
+ }
2665
+ function drawBreakMark(ctx, x, y, vertical) {
2666
+ const half = 8;
2667
+ const gap = 7;
2668
+ ctx.save();
2669
+ ctx.translate(x, y);
2670
+ if (vertical) ctx.rotate(-Math.PI / 2);
2671
+ ctx.fillStyle = "#fff";
2672
+ ctx.fillRect(-half - 2, -gap, (half + 2) * 2, gap * 2);
2673
+ ctx.strokeStyle = "#000";
2674
+ ctx.lineWidth = 1.6;
2675
+ for (const dy of [-gap * 0.5, gap * 0.5]) {
2676
+ ctx.beginPath();
2677
+ ctx.moveTo(-half, dy + 2.5);
2678
+ ctx.quadraticCurveTo(-half / 2, dy - 4.5, 0, dy);
2679
+ ctx.quadraticCurveTo(half / 2, dy + 4.5, half, dy - 2.5);
2680
+ ctx.stroke();
2681
+ }
2682
+ ctx.restore();
2683
+ }
2684
+ function renderEconPlane(ctx, w, h, data, options) {
2685
+ clearCanvas(ctx, w, h);
2686
+ const fs = options.fontSize;
2687
+ const four = data.quadrants === "all";
2688
+ const nameFont = (size) => getFont(size, options);
2689
+ const tickFont = getFont(fs.tick, options);
2690
+ const series = seriesOf(data);
2691
+ ctx.font = tickFont;
2692
+ const yTickW = data.yAxis.ticks.length > 0 ? Math.max(...data.yAxis.ticks.map((_, i) => richWidth(ctx, tickText(data.yAxis, i), fs.tick, nameFont))) : 0;
2693
+ ctx.font = nameFont(fs.axisLabel);
2694
+ const yNameLines = splitLines(data.yAxis.label);
2695
+ const xNameW = data.xAxis.label ? richWidth(ctx, data.xAxis.label, fs.axisLabel, nameFont) : 0;
2696
+ const yNameW = data.yAxis.label ? Math.max(...yNameLines.map((l) => l ? richWidth(ctx, l, fs.axisLabel, nameFont) : 0)) : 0;
2697
+ const lineLabelW = data.lines.length > 0 ? Math.max(...data.lines.map((l) => l.label ? richWidth(ctx, l.label, fs.axisLabel, nameFont) : 0)) : 0;
2698
+ const notes = options.footnotes.filter((f) => f.trim()).length;
2699
+ const inlineSource = !!options.sourceInline && !!options.source && !options.sourceLeft && notes > 0;
2700
+ const hasSourceLine = (!!options.source || !!options.sourceLeft) && !inlineSource;
2701
+ const bottomText = (hasSourceLine ? fs.dataLabel + 6 : 0) + notes * (fs.dataLabel * 0.9 + 6) + 12;
2702
+ const yNameExtra = (yNameLines.length - 1) * fs.axisLabel * NAME_LINE_H;
2703
+ const padding = {
2704
+ top: (options.title ? fs.title + 40 : 18) + fs.axisLabel + yNameExtra + 10 + ARROW_EXT,
2705
+ // 세로축 이름은 축 **왼쪽 위**에 오른쪽 맞춤으로 선다(네 사분면이면 가운데).
2706
+ // 자리가 모자라면 글꼴을 줄이고 밀어 넣지만, 흔한 길이는 여기서 비워 둔다.
2707
+ // 34% 를 넘겨 비우지는 않는다 — 이름 하나 때문에 그림이 사라지면 안 된다.
2708
+ left: four ? Math.max(46, ARROW_EXT + 20) : Math.max(46, yTickW + 14, Math.min(yNameW + 8, w * 0.34)),
2709
+ right: Math.min(
2710
+ w * 0.45,
2711
+ Math.max(56, lineLabelW + 16, ARROW_EXT + 12 + xNameW + 6)
2712
+ ),
2713
+ bottom: fs.tick + 24 + bottomText
2714
+ };
2715
+ const plotX = padding.left;
2716
+ const plotY = padding.top;
2717
+ const plotW = Math.max(40, w - padding.left - padding.right);
2718
+ const plotH = Math.max(40, h - padding.top - padding.bottom);
2719
+ const toX = (v) => plotX + (v - data.xAxis.min) / span(data.xAxis) * plotW;
2720
+ const toY = (v) => plotY + plotH - (v - data.yAxis.min) / span(data.yAxis) * plotH;
2721
+ const axX = four ? clamp(toX(0), plotX, plotX + plotW) : plotX;
2722
+ const axY = four ? clamp(toY(0), plotY, plotY + plotH) : plotY + plotH;
2723
+ const dash = DASH_PATTERN[data.dash] ?? DASH_PATTERN.dashed;
2724
+ if (data.grid) {
2725
+ const xEnd = data.xAxis.ticks.length > 0 ? toX(Math.max(...data.xAxis.ticks)) : plotX + plotW;
2726
+ const yEnd = data.yAxis.ticks.length > 0 ? toY(Math.max(...data.yAxis.ticks)) : plotY;
2727
+ ctx.save();
2728
+ ctx.strokeStyle = "#000";
2729
+ ctx.lineWidth = GUIDE_W;
2730
+ ctx.setLineDash(dash);
2731
+ for (const v of data.xAxis.ticks) {
2732
+ if (v === 0) continue;
2733
+ const x = toX(v);
2734
+ ctx.beginPath();
2735
+ ctx.moveTo(x, axY);
2736
+ ctx.lineTo(x, yEnd);
2737
+ ctx.stroke();
2738
+ }
2739
+ for (const v of data.yAxis.ticks) {
2740
+ if (v === 0) continue;
2741
+ const y = toY(v);
2742
+ ctx.beginPath();
2743
+ ctx.moveTo(axX, y);
2744
+ ctx.lineTo(xEnd, y);
2745
+ ctx.stroke();
2746
+ }
2747
+ ctx.restore();
2748
+ }
2749
+ ctx.save();
2750
+ ctx.strokeStyle = "#000";
2751
+ ctx.lineWidth = GUIDE_W;
2752
+ ctx.setLineDash(dash);
2753
+ for (const p of data.points) {
2754
+ if (p.guide === "none") continue;
2755
+ const px = toX(p.x);
2756
+ const py = toY(p.y);
2757
+ const cross = p.guide === "cross";
2758
+ if (cross || p.guide === "to-x" || p.guide === "both") {
2759
+ ctx.beginPath();
2760
+ ctx.moveTo(px, cross ? plotY : py);
2761
+ ctx.lineTo(px, cross ? plotY + plotH : axY);
2762
+ ctx.stroke();
2763
+ }
2764
+ if (cross || p.guide === "to-y" || p.guide === "both") {
2765
+ ctx.beginPath();
2766
+ ctx.moveTo(cross ? plotX : px, py);
2767
+ ctx.lineTo(cross ? plotX + plotW : axX, py);
2768
+ ctx.stroke();
2769
+ }
2770
+ }
2771
+ if (data.seriesGuides && series.length > 0) {
2772
+ const highest = /* @__PURE__ */ new Map();
2773
+ for (const s of series) {
2774
+ for (const p of s.points) {
2775
+ const prev = highest.get(p.x);
2776
+ if (prev == null || p.y > prev) highest.set(p.x, p.y);
2777
+ }
2778
+ }
2779
+ for (const [x, y] of highest) {
2780
+ ctx.beginPath();
2781
+ ctx.moveTo(toX(x), toY(y));
2782
+ ctx.lineTo(toX(x), axY);
2783
+ ctx.stroke();
2784
+ }
2785
+ }
2786
+ ctx.restore();
2787
+ ctx.save();
2788
+ ctx.strokeStyle = "#000";
2789
+ ctx.fillStyle = "#000";
2790
+ ctx.lineWidth = AXIS_W;
2791
+ ctx.lineCap = "butt";
2792
+ const xLeft = four ? plotX - ARROW_EXT : axX;
2793
+ const xRight = plotX + plotW + ARROW_EXT;
2794
+ ctx.beginPath();
2795
+ ctx.moveTo(xLeft, axY);
2796
+ ctx.lineTo(xRight, axY);
2797
+ ctx.stroke();
2798
+ drawArrowHead(ctx, xLeft, axY, xRight, axY);
2799
+ if (four) drawArrowHead(ctx, xRight, axY, xLeft, axY);
2800
+ const yBottom = four ? plotY + plotH + ARROW_EXT : axY;
2801
+ const yTop = plotY - ARROW_EXT;
2802
+ ctx.beginPath();
2803
+ ctx.moveTo(axX, yBottom);
2804
+ ctx.lineTo(axX, yTop);
2805
+ ctx.stroke();
2806
+ drawArrowHead(ctx, axX, yBottom, axX, yTop);
2807
+ if (four) drawArrowHead(ctx, axX, yTop, axX, yBottom);
2808
+ if (data.xAxis.broken) {
2809
+ const first = data.xAxis.ticks.find((v) => v !== 0);
2810
+ drawBreakMark(ctx, first == null ? axX + plotW * 0.12 : (axX + toX(first)) / 2, axY, false);
2811
+ }
2812
+ if (data.yAxis.broken) {
2813
+ const first = data.yAxis.ticks.find((v) => v !== 0);
2814
+ drawBreakMark(ctx, axX, first == null ? axY - plotH * 0.12 : (axY + toY(first)) / 2, true);
2815
+ }
2816
+ ctx.restore();
2817
+ ctx.save();
2818
+ ctx.strokeStyle = "#000";
2819
+ ctx.lineWidth = LINE_W;
2820
+ for (const line2 of data.lines) {
2821
+ ctx.lineCap = line2.dashed ? "butt" : "round";
2822
+ ctx.setLineDash(line2.dashed ? THICK_DASH : []);
2823
+ ctx.beginPath();
2824
+ ctx.moveTo(toX(line2.from.x), toY(line2.from.y));
2825
+ ctx.lineTo(toX(line2.to.x), toY(line2.to.y));
2826
+ ctx.stroke();
2827
+ }
2828
+ ctx.restore();
2829
+ ctx.save();
2830
+ ctx.strokeStyle = "#000";
2831
+ ctx.lineWidth = LINE_W;
2832
+ for (const s of drawOrder(series)) {
2833
+ if (s.points.length < 2) continue;
2834
+ ctx.lineCap = s.dashed ? "butt" : "round";
2835
+ ctx.setLineDash(s.dashed ? THICK_DASH : []);
2836
+ ctx.beginPath();
2837
+ s.points.forEach((p, i) => {
2838
+ if (i === 0) ctx.moveTo(toX(p.x), toY(p.y));
2839
+ else ctx.lineTo(toX(p.x), toY(p.y));
2840
+ });
2841
+ ctx.stroke();
2842
+ }
2843
+ ctx.restore();
2844
+ ctx.save();
2845
+ ctx.fillStyle = "#000";
2846
+ ctx.textAlign = "left";
2847
+ ctx.textBaseline = "middle";
2848
+ for (const line2 of data.lines) {
2849
+ if (!line2.label) continue;
2850
+ const end = line2.labelAt === "from" ? line2.from : line2.to;
2851
+ const lx = toX(end.x) + 12 + (line2.labelDx ?? 0);
2852
+ const raw = toY(end.y) + (line2.labelDy ?? 0);
2853
+ const ly = Math.min(raw, axY - 6 - fs.axisLabel * 0.5);
2854
+ drawFloatingRich(ctx, line2.label, lx, ly, w, h, fs.axisLabel, nameFont);
2855
+ }
2856
+ ctx.restore();
2857
+ ctx.save();
2858
+ ctx.strokeStyle = "#000";
2859
+ ctx.fillStyle = "#000";
2860
+ ctx.lineWidth = ARROW_W;
2861
+ for (const a of data.arrows) {
2862
+ const x1 = toX(a.from.x);
2863
+ const y1 = toY(a.from.y);
2864
+ const x2 = toX(a.to.x);
2865
+ const y2 = toY(a.to.y);
2866
+ const len = Math.hypot(x2 - x1, y2 - y1);
2867
+ if (!(len > 0)) continue;
2868
+ const ux = (x2 - x1) / len;
2869
+ const uy = (y2 - y1) / len;
2870
+ const ox = -uy * a.offset;
2871
+ const oy = ux * a.offset;
2872
+ const cut = Math.min(a.shorten, len * 0.4);
2873
+ const sx = x1 + ux * cut + ox;
2874
+ const sy = y1 + uy * cut + oy;
2875
+ const ex = x2 - ux * cut + ox;
2876
+ const ey = y2 - uy * cut + oy;
2877
+ ctx.beginPath();
2878
+ ctx.moveTo(sx, sy);
2879
+ ctx.lineTo(ex, ey);
2880
+ ctx.stroke();
2881
+ drawArrowHead(ctx, sx, sy, ex, ey);
2882
+ if (a.label) {
2883
+ const at = anchorOf(a.labelPos, LABEL_GAP + 4);
2884
+ ctx.textAlign = at.align;
2885
+ ctx.textBaseline = at.baseline;
2886
+ drawFloatingRich(ctx, a.label, (sx + ex) / 2 + at.dx, (sy + ey) / 2 + at.dy, w, h, fs.axisLabel, nameFont);
2887
+ }
2888
+ }
2889
+ ctx.restore();
2890
+ ctx.save();
2891
+ ctx.fillStyle = "#000";
2892
+ for (const p of data.points) {
2893
+ const px = toX(p.x);
2894
+ const py = toY(p.y);
2895
+ if (p.dot) {
2896
+ ctx.beginPath();
2897
+ ctx.arc(px, py, DOT_R, 0, Math.PI * 2);
2898
+ ctx.fill();
2899
+ }
2900
+ if (!p.label) continue;
2901
+ const at = anchorOf(p.labelPos, DOT_R + LABEL_GAP);
2902
+ ctx.textAlign = at.align;
2903
+ ctx.textBaseline = at.baseline;
2904
+ drawFloatingRich(ctx, p.label, px + at.dx, py + at.dy, w, h, fs.axisLabel, nameFont);
2905
+ }
2906
+ ctx.restore();
2907
+ ctx.save();
2908
+ for (const s of drawOrder(series)) {
2909
+ for (const p of s.points) drawSeriesMarker(ctx, s, toX(p.x), toY(p.y));
2910
+ }
2911
+ ctx.restore();
2912
+ const tickInk = (text, x, y) => {
2913
+ if (four) {
2914
+ const tw = richWidth(ctx, text, fs.tick, nameFont);
2915
+ const left = ctx.textAlign === "right" ? tw : ctx.textAlign === "center" ? tw / 2 : 0;
2916
+ const up = ctx.textBaseline === "top" ? 0 : fs.tick * 0.5;
2917
+ const rx = clamp(x - left - 3, 0, w);
2918
+ const ry = clamp(y - up - 2, 0, h);
2919
+ ctx.fillStyle = "#fff";
2920
+ ctx.fillRect(rx, ry, Math.min(tw + 6, w - rx), Math.min(fs.tick + 4, h - ry));
2921
+ ctx.fillStyle = "#000";
2922
+ }
2923
+ fillRich(ctx, text, x, y, fs.tick, nameFont);
2924
+ };
2925
+ ctx.save();
2926
+ ctx.fillStyle = "#000";
2927
+ ctx.font = tickFont;
2928
+ ctx.textAlign = "center";
2929
+ ctx.textBaseline = "top";
2930
+ data.xAxis.ticks.forEach((v, i) => {
2931
+ if (v === 0) return;
2932
+ const text = tickText(data.xAxis, i);
2933
+ const at = nudgeRichInside(ctx, text, toX(v), axY + 10, w, h, fs.tick, nameFont);
2934
+ tickInk(text, at.x, at.y);
2935
+ });
2936
+ ctx.textAlign = four ? "left" : "right";
2937
+ ctx.textBaseline = "middle";
2938
+ data.yAxis.ticks.forEach((v, i) => {
2939
+ if (v === 0) return;
2940
+ const text = tickText(data.yAxis, i);
2941
+ const at = nudgeRichInside(ctx, text, axX + (four ? 10 : -10), toY(v), w, h, fs.tick, nameFont);
2942
+ tickInk(text, at.x, at.y);
2943
+ });
2944
+ ctx.textAlign = four ? "left" : "center";
2945
+ ctx.textBaseline = "top";
2946
+ const zero = nudgeInside(ctx, "0", axX + (four ? 8 : 0), axY + 10, w, h);
2947
+ tickInk("0", zero.x, zero.y);
2948
+ ctx.restore();
2949
+ ctx.save();
2950
+ ctx.fillStyle = "#000";
2951
+ if (data.xAxis.label) {
2952
+ ctx.textAlign = "left";
2953
+ ctx.textBaseline = "middle";
2954
+ drawFloatingRich(ctx, data.xAxis.label, xRight + 10, axY + fs.tick * 0.6, w, h, fs.axisLabel, nameFont);
2955
+ }
2956
+ if (data.yAxis.label) {
2957
+ ctx.textAlign = four ? "center" : "right";
2958
+ ctx.textBaseline = "bottom";
2959
+ const baseY = yTop - (four ? 8 : -4);
2960
+ if (yNameLines.length === 1) {
2961
+ drawFloatingRich(ctx, data.yAxis.label, axX + (four ? 0 : -8), baseY, w, h, fs.axisLabel, nameFont);
2962
+ } else {
2963
+ const lh = fs.axisLabel * NAME_LINE_H;
2964
+ const cx = four ? axX : axX - 8 - yNameW / 2;
2965
+ ctx.textAlign = "center";
2966
+ yNameLines.forEach((lineText, i) => {
2967
+ const ly = baseY - (yNameLines.length - 1 - i) * lh;
2968
+ drawFloatingRich(ctx, lineText, cx, ly, w, h, fs.axisLabel, nameFont);
2969
+ });
2970
+ }
2971
+ }
2972
+ ctx.restore();
2973
+ if (data.legend && series.length > 0) {
2974
+ const items = series.map((s) => ({
2975
+ type: "line",
2976
+ fillStyle: "#000",
2977
+ strokeStyle: "#000",
2978
+ label: s.label,
2979
+ dash: s.dashed ? THICK_DASH : [],
2980
+ lineWidth: 2.5,
2981
+ marker: s.marker,
2982
+ hollow: s.hollow
2983
+ }));
2984
+ drawInsideLegend({
2985
+ ctx,
2986
+ items,
2987
+ corner: data.legend,
2988
+ plotX,
2989
+ plotY,
2990
+ plotW,
2991
+ plotH,
2992
+ canvasW: w,
2993
+ canvasH: h,
2994
+ fontSize: fs.axisLabel * 0.8,
2995
+ font: nameFont(fs.axisLabel * 0.8),
2996
+ // 기호가 상자에 덮이지 않게 꼭짓점 둘레를 피할 자리로 넘긴다
2997
+ avoid: series.flatMap((s) => s.points.map((p) => ({
2998
+ x0: toX(p.x) - DOT_R,
2999
+ y0: toY(p.y) - DOT_R,
3000
+ x1: toX(p.x) + DOT_R,
3001
+ y1: toY(p.y) + DOT_R
3002
+ })))
3003
+ });
3004
+ }
3005
+ drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: fs.title, fonts: options, canvasWidth: w });
3006
+ drawSourceAndFootnote({
3007
+ ctx,
3008
+ plotX,
3009
+ plotW,
3010
+ height: h,
3011
+ source: options.source,
3012
+ sourceLeft: options.sourceLeft,
3013
+ sourceInline: options.sourceInline,
3014
+ footnotes: options.footnotes,
3015
+ fontSize: fs.dataLabel,
3016
+ fonts: options,
3017
+ canvasWidth: w
3018
+ });
3019
+ }
3020
+
2387
3021
  // src/core/graphs/TreemapGraph.ts
2388
3022
  var PAD_RATIO3 = 0.4;
2389
3023
  var MIN_LABEL_W = 34;
@@ -2491,29 +3125,30 @@ function renderTreemapGraph(ctx, w, h, data, options) {
2491
3125
  ctx.fillStyle = "#000";
2492
3126
  ctx.textAlign = "center";
2493
3127
  ctx.textBaseline = "middle";
2494
- ctx.font = getFont(labelSize, options.fontFamily, options.customFont);
3128
+ ctx.font = getFont(labelSize, options);
2495
3129
  rects.forEach((r, i) => {
2496
3130
  if (shouldOmitLabel(r, minLabelW, labelSize)) return;
2497
3131
  const avail = r.w - pad * 2;
2498
- ctx.font = getFont(labelSize, options.fontFamily, options.customFont);
3132
+ ctx.font = getFont(labelSize, options);
2499
3133
  const layout = pickLabelLayout(ctx, data.cells[i].label, avail, r.h, labelSize);
2500
3134
  if (shouldOmitLayout(layout)) return;
2501
3135
  const drawSize = resolveDrawSize(labelSize, layout);
2502
3136
  if (drawSize !== labelSize) {
2503
- ctx.font = getFont(drawSize, options.fontFamily, options.customFont);
3137
+ ctx.font = getFont(drawSize, options);
2504
3138
  }
2505
3139
  const startY = r.y + r.h / 2 - (layout.lines.length - 1) * drawSize * 1.2 / 2;
2506
3140
  layout.lines.forEach((line2, k) => {
2507
3141
  ctx.fillText(line2, r.x + r.w / 2, startY + k * drawSize * 1.2, avail);
2508
3142
  });
2509
3143
  if (drawSize !== labelSize) {
2510
- ctx.font = getFont(labelSize, options.fontFamily, options.customFont);
3144
+ ctx.font = getFont(labelSize, options);
2511
3145
  }
2512
3146
  });
2513
3147
  ctx.lineWidth = 2;
2514
3148
  ctx.strokeRect(frame.x, frame.y, frame.w, frame.h);
2515
3149
  drawTitle({
2516
3150
  ctx,
3151
+ fonts: options,
2517
3152
  plotX: frame.x,
2518
3153
  plotW: frame.w,
2519
3154
  title: options.title,
@@ -2522,6 +3157,7 @@ function renderTreemapGraph(ctx, w, h, data, options) {
2522
3157
  });
2523
3158
  drawSourceAndFootnote({
2524
3159
  ctx,
3160
+ fonts: options,
2525
3161
  plotX: frame.x,
2526
3162
  plotW: frame.w,
2527
3163
  height: h,
@@ -2547,12 +3183,13 @@ function renderClimateGraph(ctx, w, h, data, options) {
2547
3183
  options.legendLabel1 || data.precipLabel,
2548
3184
  options.legendLabel2 || data.tempLabel
2549
3185
  ];
2550
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
3186
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
2551
3187
  const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(
2552
3188
  ctx,
2553
3189
  legendLabels,
2554
3190
  options.fontSize.dataLabel * 0.85 + 5,
2555
3191
  w - 130 - (130 + legendW),
3192
+ options,
2556
3193
  ["rect", data.monthInterval === 12 ? "line" : "circle"]
2557
3194
  ) : 0;
2558
3195
  const padding = {
@@ -2585,8 +3222,6 @@ function renderClimateGraph(ctx, w, h, data, options) {
2585
3222
  step: data.precipRange.step || Math.max(1, Math.round((data.precipRange.max - data.precipRange.min) / 6))
2586
3223
  };
2587
3224
  if (precipAxis.min < 0) precipAxis.min = 0;
2588
- const font = options.fontFamily;
2589
- const customFont = options.customFont;
2590
3225
  const indices = INTERVAL_INDICES[data.monthInterval] ?? INTERVAL_INDICES[12];
2591
3226
  drawYAxis({
2592
3227
  ctx,
@@ -2598,8 +3233,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2598
3233
  step: tempAxis.step,
2599
3234
  label: data.tempLabel,
2600
3235
  side: "left",
2601
- fontFamily: font,
2602
- customFont,
3236
+ fonts: options,
2603
3237
  tickFontSize: options.fontSize.tick,
2604
3238
  labelFontSize: options.fontSize.axisLabel,
2605
3239
  drawGrid: true
@@ -2614,8 +3248,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2614
3248
  step: precipAxis.step,
2615
3249
  label: data.precipLabel,
2616
3250
  side: "right",
2617
- fontFamily: font,
2618
- customFont,
3251
+ fonts: options,
2619
3252
  tickFontSize: options.fontSize.tick,
2620
3253
  labelFontSize: options.fontSize.axisLabel
2621
3254
  });
@@ -2627,8 +3260,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2627
3260
  height: h,
2628
3261
  labels: MONTH_LABELS,
2629
3262
  indices,
2630
- fontFamily: font,
2631
- customFont,
3263
+ fonts: options,
2632
3264
  tickFontSize: options.fontSize.tick,
2633
3265
  labelFontSize: options.fontSize.axisLabel
2634
3266
  });
@@ -2640,8 +3272,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2640
3272
  width: w,
2641
3273
  height: h,
2642
3274
  labels: filteredLabels,
2643
- fontFamily: font,
2644
- customFont,
3275
+ fonts: options,
2645
3276
  tickFontSize: options.fontSize.tick,
2646
3277
  labelFontSize: options.fontSize.axisLabel
2647
3278
  });
@@ -2666,7 +3297,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2666
3297
  }
2667
3298
  if (options.showDataLabels) {
2668
3299
  ctx.fillStyle = "#000";
2669
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
3300
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
2670
3301
  ctx.textAlign = "center";
2671
3302
  ctx.textBaseline = "bottom";
2672
3303
  for (let s = 0; s < indices.length; s++) {
@@ -2701,21 +3332,22 @@ function renderClimateGraph(ctx, w, h, data, options) {
2701
3332
  ctx.fill();
2702
3333
  if (options.showDataLabels) {
2703
3334
  ctx.fillStyle = "#000";
2704
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
3335
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
2705
3336
  ctx.textAlign = "center";
2706
3337
  ctx.textBaseline = "bottom";
2707
3338
  ctx.fillText(String(data.months[i].temp), cx, y - 8);
2708
3339
  }
2709
3340
  }
2710
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
3341
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
2711
3342
  ctx.fillStyle = "#000";
2712
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
3343
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
2713
3344
  ctx.textAlign = "center";
2714
3345
  ctx.textBaseline = "top";
2715
3346
  ctx.fillText("(\uC6D4)", plotX + plotW + 30, plotY + plotH + 12);
2716
3347
  if (showLegend) {
2717
3348
  drawLegend({
2718
3349
  ctx,
3350
+ fonts: options,
2719
3351
  items: [
2720
3352
  { type: "rect", fillStyle: "#AAA", strokeStyle: "#666", label: legendLabels[0] },
2721
3353
  { type: data.monthInterval === 12 ? "line" : "circle", fillStyle: "#000", label: legendLabels[1] }
@@ -2731,7 +3363,7 @@ function renderClimateGraph(ctx, w, h, data, options) {
2731
3363
  rightGap: 80
2732
3364
  });
2733
3365
  }
2734
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel });
3366
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel });
2735
3367
  }
2736
3368
 
2737
3369
  // src/core/graphs/CubeGraph.ts
@@ -2783,8 +3415,6 @@ var EDGES = [
2783
3415
  ];
2784
3416
  function renderCubeGraph(ctx, w, h, data, options) {
2785
3417
  clearCanvas(ctx, w, h);
2786
- const font = options.fontFamily;
2787
- const cf = options.customFont;
2788
3418
  const fs = options.fontSize;
2789
3419
  const topPad = options.title ? 100 : 60;
2790
3420
  let bottomPad = 40;
@@ -2801,8 +3431,7 @@ function renderCubeGraph(ctx, w, h, data, options) {
2801
3431
  availH,
2802
3432
  Math.min(availW * 0.5, availH * 0.55),
2803
3433
  fs,
2804
- font,
2805
- cf
3434
+ options
2806
3435
  );
2807
3436
  const scale = fit.scale;
2808
3437
  const cubeCenter = project(0.5, 0.5, 0.5, 0, 0, scale);
@@ -2830,7 +3459,7 @@ function renderCubeGraph(ctx, w, h, data, options) {
2830
3459
  ctx.lineTo(v2[b][0], v2[b][1]);
2831
3460
  ctx.stroke();
2832
3461
  }
2833
- drawAxes(ctx, data, cx, cy, scale, w, h, font, cf, fs, fit.names, fit.nameSize);
3462
+ drawAxes(ctx, data, cx, cy, scale, w, h, options, fs, fit.names, fit.nameSize);
2834
3463
  const cubeCenter2D = project(0.5, 0.5, 0.5, cx, cy, scale);
2835
3464
  for (const pt of data.points) {
2836
3465
  const [px, py] = project(pt.z, pt.y, pt.x, cx, cy, scale);
@@ -2845,7 +3474,7 @@ function renderCubeGraph(ctx, w, h, data, options) {
2845
3474
  autoDy = autoDy / len * 40;
2846
3475
  const dx = autoDx + pt.labelDx;
2847
3476
  const dy = autoDy + pt.labelDy;
2848
- ctx.font = getFont(fs.dataLabel + 10, font, cf, "bold");
3477
+ ctx.font = getFont(fs.dataLabel + 10, options, "bold");
2849
3478
  ctx.textAlign = dx >= 0 ? "left" : "right";
2850
3479
  ctx.textBaseline = "middle";
2851
3480
  const anchor = nudgeInside(ctx, pt.label, px + dx + (dx >= 0 ? 4 : -4), py + dy, w, h);
@@ -2866,9 +3495,10 @@ function renderCubeGraph(ctx, w, h, data, options) {
2866
3495
  const plotX = cubeLeft;
2867
3496
  const plotW = cubeRight - cubeLeft;
2868
3497
  if (options.title) {
3498
+ const titleFont = sansFont(options);
2869
3499
  const yAxisTop = project(0, 1.25, 0, cx, cy, scale);
2870
3500
  ctx.fillStyle = "#000";
2871
- ctx.font = `bold ${fs.title}px 'Noto Sans KR', sans-serif`;
3501
+ ctx.font = `bold ${fs.title}px ${titleFont}`;
2872
3502
  ctx.textAlign = "center";
2873
3503
  ctx.textBaseline = "bottom";
2874
3504
  drawFloatingLabel(
@@ -2879,13 +3509,13 @@ function renderCubeGraph(ctx, w, h, data, options) {
2879
3509
  w,
2880
3510
  h,
2881
3511
  fs.title,
2882
- (size) => `bold ${size}px 'Noto Sans KR', sans-serif`
3512
+ (size) => `bold ${size}px ${titleFont}`
2883
3513
  );
2884
3514
  }
2885
3515
  const zAxisEnd = project(1.25, 0, 0, cx, cy, scale);
2886
3516
  const zHighRef = project(1, 0, 0, cx, cy, scale);
2887
3517
  const sourceY = Math.max(zAxisEnd[1] + 20 + fs.axisLabel, zHighRef[1] + 10 + fs.axisLabel * 0.9) + 47;
2888
- drawSourceAndFootnote({ ctx, plotX, plotW, height: sourceY, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel });
3518
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: sourceY, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel });
2889
3519
  }
2890
3520
  function drawArrow(ctx, x1, y1, x2, y2) {
2891
3521
  const headLen = 10;
@@ -2932,11 +3562,11 @@ function centerAt(w, topPad, availH, s) {
2932
3562
  const c = project(0.5, 0.5, 0.5, 0, 0, s);
2933
3563
  return [w / 2 - c[0], topPad + availH / 2 - c[1]];
2934
3564
  }
2935
- function fitCubeScale(ctx, data, w, h, topPad, availH, maxScale, fs, font, cf) {
3565
+ function fitCubeScale(ctx, data, w, h, topPad, availH, maxScale, fs, options) {
2936
3566
  ctx.save();
2937
3567
  let nameSize = fs.axisLabel;
2938
- const makeNameFont = (size) => getFont(size, font, cf, "bold");
2939
- const dirFont = getFont(fs.axisLabel * 0.9, font, cf, "normal");
3568
+ const makeNameFont = (size) => getFont(size, options, "bold");
3569
+ const dirFont = getFont(fs.axisLabel * 0.9, options, "normal");
2940
3570
  let names = { x: [data.xAxis.name], y: [data.yAxis.name], z: [data.zAxis.name] };
2941
3571
  const fits = (s) => {
2942
3572
  const [cx, cy] = centerAt(w, topPad, availH, s);
@@ -2976,7 +3606,7 @@ function fitCubeScale(ctx, data, w, h, topPad, availH, maxScale, fs, font, cf) {
2976
3606
  ctx.restore();
2977
3607
  return { scale, names, nameSize };
2978
3608
  }
2979
- function drawAxes(ctx, data, cx, cy, scale, w, h, font, cf, fs, names, nameSize) {
3609
+ function drawAxes(ctx, data, cx, cy, scale, w, h, options, fs, names, nameSize) {
2980
3610
  const ext = 1.25;
2981
3611
  ctx.strokeStyle = "#000";
2982
3612
  ctx.fillStyle = "#000";
@@ -2990,8 +3620,8 @@ function drawAxes(ctx, data, cx, cy, scale, w, h, font, cf, fs, names, nameSize)
2990
3620
  const zStart = project(0, 0, 1, cx, cy, scale);
2991
3621
  const zEnd = project(0, 0, ext, cx, cy, scale);
2992
3622
  drawArrow(ctx, zStart[0], zStart[1], zEnd[0], zEnd[1]);
2993
- const nameFont = getFont(nameSize, font, cf, "bold");
2994
- const dirFont = getFont(fs.axisLabel * 0.9, font, cf, "normal");
3623
+ const nameFont = getFont(nameSize, options, "bold");
3624
+ const dirFont = getFont(fs.axisLabel * 0.9, options, "normal");
2995
3625
  ctx.fillStyle = "#000";
2996
3626
  for (const t of axisTexts(data, cx, cy, scale, names)) {
2997
3627
  if (t.lines.every((l) => !l)) continue;
@@ -3019,11 +3649,11 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3019
3649
  options.legendLabel1 || data.precipLabel,
3020
3650
  options.legendLabel2 || data.tempLabel
3021
3651
  ];
3022
- const legendW = showLegend && legendPos === "right" && !data.insideLegend ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
3652
+ const legendW = showLegend && legendPos === "right" && !data.insideLegend ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
3023
3653
  let nameW = 0;
3024
3654
  if (data.tempAxisName || data.precipAxisName) {
3025
3655
  ctx.save();
3026
- ctx.font = getFont(options.fontSize.axisLabel, options.fontFamily, options.customFont, "bold");
3656
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
3027
3657
  nameW = ctx.measureText("\uAC00").width + 12;
3028
3658
  ctx.restore();
3029
3659
  }
@@ -3034,6 +3664,7 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3034
3664
  legendLabels,
3035
3665
  options.fontSize.dataLabel * 0.85 + 5,
3036
3666
  w - padLeft - padRight,
3667
+ options,
3037
3668
  ["rect", data.monthInterval === 12 ? "line" : "circle"]
3038
3669
  ) : 0;
3039
3670
  const padding = {
@@ -3053,8 +3684,6 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3053
3684
  const plotY = padding.top;
3054
3685
  const plotW = w - padding.left - padding.right;
3055
3686
  const plotH = h - padding.top - padding.bottom;
3056
- const font = options.fontFamily;
3057
- const customFont = options.customFont;
3058
3687
  const indices = INTERVAL_INDICES2[data.monthInterval] ?? INTERVAL_INDICES2[12];
3059
3688
  const tempDevs = data.months.map((m, i) => m.temp - data.baseMonths[i].temp);
3060
3689
  const precipDevs = data.months.map((m, i) => m.precip - data.baseMonths[i].precip);
@@ -3077,8 +3706,8 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3077
3706
  if (tempAxis.step === 0) {
3078
3707
  tempAxis.step = Math.max(0.5, Math.round((tempAxis.max - tempAxis.min) / 6 * 10) / 10);
3079
3708
  }
3080
- const tempTickW = drawDeviationYAxis(ctx, padding, w, h, tempAxis, "left", data.tempLabel, font, customFont, options.fontSize);
3081
- const precipTickW = drawDeviationYAxis(ctx, padding, w, h, precipAxis, "right", data.precipLabel, font, customFont, options.fontSize);
3709
+ const tempTickW = drawDeviationYAxis(ctx, padding, w, h, tempAxis, "left", data.tempLabel, options, options.fontSize);
3710
+ const precipTickW = drawDeviationYAxis(ctx, padding, w, h, precipAxis, "right", data.precipLabel, options, options.fontSize);
3082
3711
  const totalSlots = indices.length;
3083
3712
  const slotW = plotW / totalSlots;
3084
3713
  ctx.strokeStyle = "#000";
@@ -3094,7 +3723,7 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3094
3723
  ctx.stroke();
3095
3724
  }
3096
3725
  ctx.fillStyle = "#000";
3097
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
3726
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3098
3727
  ctx.textAlign = "center";
3099
3728
  ctx.textBaseline = "top";
3100
3729
  for (let s = 0; s < totalSlots; s++) {
@@ -3164,7 +3793,7 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3164
3793
  inkRects.push({ x0: cx - 6, y0: y - 6, x1: cx + 6, y1: y + 6 });
3165
3794
  }
3166
3795
  ctx.fillStyle = "#000";
3167
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
3796
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3168
3797
  ctx.textAlign = "center";
3169
3798
  ctx.textBaseline = "top";
3170
3799
  ctx.fillText("(\uC6D4)", plotX + plotW + 30, plotY + plotH + 12);
@@ -3193,12 +3822,13 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3193
3822
  canvasW: w,
3194
3823
  canvasH: h,
3195
3824
  fontSize: options.fontSize.dataLabel * 0.9,
3196
- font: getFont(options.fontSize.dataLabel * 0.9, font, customFont, "bold"),
3825
+ font: getFont(options.fontSize.dataLabel * 0.9, options, "bold"),
3197
3826
  avoid: inkRects
3198
3827
  });
3199
3828
  } else if (showLegend) {
3200
3829
  drawLegend({
3201
3830
  ctx,
3831
+ fonts: options,
3202
3832
  items: [
3203
3833
  { type: "rect", fillStyle: "#888", strokeStyle: "#444", label: legendLabels[0] },
3204
3834
  { type: data.monthInterval === 12 ? "line" : "circle", fillStyle: "#000", label: legendLabels[1] }
@@ -3223,8 +3853,7 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3223
3853
  plotH,
3224
3854
  h,
3225
3855
  options.fontSize.axisLabel,
3226
- font,
3227
- customFont
3856
+ options
3228
3857
  );
3229
3858
  }
3230
3859
  if (data.precipAxisName) {
@@ -3236,14 +3865,13 @@ function renderDeviationAGraph(ctx, w, h, data, options) {
3236
3865
  plotH,
3237
3866
  h,
3238
3867
  options.fontSize.axisLabel,
3239
- font,
3240
- customFont
3868
+ options
3241
3869
  );
3242
3870
  }
3243
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
3244
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
3871
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
3872
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
3245
3873
  }
3246
- function drawDeviationYAxis(ctx, padding, width, height, axis, side, label, fontFamily, customFont, fontSize) {
3874
+ function drawDeviationYAxis(ctx, padding, width, height, axis, side, label, fonts, fontSize) {
3247
3875
  const plotX = padding.left;
3248
3876
  const plotY = padding.top;
3249
3877
  const plotW = width - padding.left - padding.right;
@@ -3256,7 +3884,7 @@ function drawDeviationYAxis(ctx, padding, width, height, axis, side, label, font
3256
3884
  ctx.lineTo(x, plotY + plotH);
3257
3885
  ctx.stroke();
3258
3886
  ctx.fillStyle = "#000";
3259
- ctx.font = getFont(fontSize.tick, fontFamily, customFont, "bold");
3887
+ ctx.font = getFont(fontSize.tick, fonts, "bold");
3260
3888
  ctx.textBaseline = "middle";
3261
3889
  ctx.textAlign = side === "left" ? "right" : "left";
3262
3890
  let maxTickW = 0;
@@ -3281,7 +3909,7 @@ function drawDeviationYAxis(ctx, padding, width, height, axis, side, label, font
3281
3909
  }
3282
3910
  if (label) {
3283
3911
  ctx.save();
3284
- const makeFont = (size) => getFont(size, fontFamily, customFont, "bold");
3912
+ const makeFont = (size) => getFont(size, fonts, "bold");
3285
3913
  ctx.fillStyle = "#000";
3286
3914
  ctx.textBaseline = "bottom";
3287
3915
  ctx.textAlign = side === "left" ? "right" : "left";
@@ -3293,7 +3921,7 @@ function drawDeviationYAxis(ctx, padding, width, height, axis, side, label, font
3293
3921
  }
3294
3922
  return maxTickW;
3295
3923
  }
3296
- function drawVerticalAxisName(ctx, name, cx, plotY, plotH, canvasH, fontSize, fontFamily, customFont) {
3924
+ function drawVerticalAxisName(ctx, name, cx, plotY, plotH, canvasH, fontSize, fonts) {
3297
3925
  const chars = [...name].filter((c) => c.trim() !== "");
3298
3926
  if (chars.length === 0) return;
3299
3927
  const stack = (size2) => (chars.length - 1) * size2 * 1.15 + size2;
@@ -3303,7 +3931,7 @@ function drawVerticalAxisName(ctx, name, cx, plotY, plotH, canvasH, fontSize, fo
3303
3931
  }
3304
3932
  if (stack(size) > canvasH - EDGE * 2) size = (canvasH - EDGE * 2) / stack(1);
3305
3933
  ctx.save();
3306
- ctx.font = getFont(size, fontFamily, customFont, "bold");
3934
+ ctx.font = getFont(size, fonts, "bold");
3307
3935
  ctx.fillStyle = "#000";
3308
3936
  ctx.textAlign = "center";
3309
3937
  ctx.textBaseline = "middle";
@@ -3331,12 +3959,13 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3331
3959
  options.legendLabel1 || data.precipDiffLabel,
3332
3960
  options.legendLabel2 || data.tempDiffLabel
3333
3961
  ];
3334
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
3962
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
3335
3963
  const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(
3336
3964
  ctx,
3337
3965
  legendLabels,
3338
3966
  options.fontSize.dataLabel * 0.85 + 5,
3339
3967
  w - 130 - (130 + legendW),
3968
+ options,
3340
3969
  ["rect", "circle"]
3341
3970
  ) : 0;
3342
3971
  const padding = {
@@ -3356,8 +3985,6 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3356
3985
  const plotY = padding.top;
3357
3986
  const plotW = w - padding.left - padding.right;
3358
3987
  const plotH = h - padding.top - padding.bottom;
3359
- const font = options.fontFamily;
3360
- const customFont = options.customFont;
3361
3988
  const regions = data.regions;
3362
3989
  const n = regions.length;
3363
3990
  const precipDiffs = regions.map((r) => r.precip - data.basePrecip);
@@ -3378,8 +4005,8 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3378
4005
  tempAxis.min = -tempAbs;
3379
4006
  tempAxis.max = tempAbs;
3380
4007
  }
3381
- drawDevBYAxis(ctx, padding, w, h, tempAxis, "left", data.tempUnit, font, customFont, options.fontSize);
3382
- drawDevBYAxis(ctx, padding, w, h, precipAxis, "right", data.precipUnit, font, customFont, options.fontSize);
4008
+ drawDevBYAxis(ctx, padding, w, h, tempAxis, "left", data.tempUnit, options, options.fontSize);
4009
+ drawDevBYAxis(ctx, padding, w, h, precipAxis, "right", data.precipUnit, options, options.fontSize);
3383
4010
  ctx.strokeStyle = "#000";
3384
4011
  ctx.lineWidth = 2;
3385
4012
  ctx.beginPath();
@@ -3396,10 +4023,10 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3396
4023
  const slotW = plotW / n;
3397
4024
  const barWidth = Math.min(slotW * 0.5, 80);
3398
4025
  ctx.fillStyle = "#000";
3399
- ctx.font = getFont(options.fontSize.tick * 1.2, font, customFont, "bold");
4026
+ ctx.font = getFont(options.fontSize.tick * 1.2, options, "bold");
3400
4027
  ctx.textAlign = "center";
3401
4028
  ctx.textBaseline = "top";
3402
- const regionFont = (size) => getFont(size, font, customFont, "bold");
4029
+ const regionFont = (size) => getFont(size, options, "bold");
3403
4030
  for (let i = 0; i < n; i++) {
3404
4031
  const cx = plotX + slotW * i + slotW / 2;
3405
4032
  drawFloatingLabel(
@@ -3435,7 +4062,7 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3435
4062
  ctx.fill();
3436
4063
  }
3437
4064
  if (options.showDataLabels) {
3438
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
4065
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
3439
4066
  ctx.textAlign = "center";
3440
4067
  for (let i = 0; i < n; i++) {
3441
4068
  const cx = plotX + slotW * i + slotW / 2;
@@ -3453,6 +4080,7 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3453
4080
  if (showLegend) {
3454
4081
  drawLegend({
3455
4082
  ctx,
4083
+ fonts: options,
3456
4084
  items: [
3457
4085
  { type: "rect", fillStyle: "#888", strokeStyle: "#444", label: legendLabels[0] },
3458
4086
  { type: "circle", fillStyle: "#000", label: legendLabels[1] }
@@ -3468,10 +4096,10 @@ function renderDeviationBGraph(ctx, w, h, data, options) {
3468
4096
  rightGap: 80
3469
4097
  });
3470
4098
  }
3471
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
3472
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel });
4099
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
4100
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel });
3473
4101
  }
3474
- function drawDevBYAxis(ctx, padding, width, height, axis, side, label, fontFamily, customFont, fontSize) {
4102
+ function drawDevBYAxis(ctx, padding, width, height, axis, side, label, fonts, fontSize) {
3475
4103
  const plotX = padding.left;
3476
4104
  const plotY = padding.top;
3477
4105
  const plotW = width - padding.left - padding.right;
@@ -3484,7 +4112,7 @@ function drawDevBYAxis(ctx, padding, width, height, axis, side, label, fontFamil
3484
4112
  ctx.lineTo(x, plotY + plotH);
3485
4113
  ctx.stroke();
3486
4114
  ctx.fillStyle = "#000";
3487
- ctx.font = getFont(fontSize.tick, fontFamily, customFont, "bold");
4115
+ ctx.font = getFont(fontSize.tick, fonts, "bold");
3488
4116
  ctx.textBaseline = "middle";
3489
4117
  ctx.textAlign = side === "left" ? "right" : "left";
3490
4118
  const tickCount = Math.round((axis.max - axis.min) / axis.step);
@@ -3506,7 +4134,7 @@ function drawDevBYAxis(ctx, padding, width, height, axis, side, label, fontFamil
3506
4134
  ctx.fillText(valStr, tx, y);
3507
4135
  }
3508
4136
  ctx.save();
3509
- ctx.font = getFont(fontSize.axisLabel, fontFamily, customFont, "bold");
4137
+ ctx.font = getFont(fontSize.axisLabel, fonts, "bold");
3510
4138
  ctx.fillStyle = "#000";
3511
4139
  ctx.textBaseline = "bottom";
3512
4140
  const labelX = side === "left" ? x - 12 : x + 12;
@@ -3572,19 +4200,18 @@ function drawMarkerLegendIcon(ctx, type, cx, cy) {
3572
4200
  }
3573
4201
  function renderHythergraph(ctx, w, h, data, options) {
3574
4202
  clearCanvas(ctx, w, h);
3575
- const font = options.fontFamily;
3576
- const cf = options.customFont;
3577
4203
  const fs = options.fontSize;
3578
4204
  const showLegend = options.showLegend && data.series.length > 0;
3579
4205
  const legendPos = options.legendPosition;
3580
4206
  const legendLabels = data.series.map((s) => s.label);
3581
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, fs.dataLabel * 0.85 + 5) : 0;
4207
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, fs.dataLabel * 0.85 + 5, options) : 0;
3582
4208
  const legendReserve = showLegend && legendPos === "bottom" ? 65 + layoutBottomLegend(
3583
4209
  ctx,
3584
4210
  legendLabels,
3585
4211
  data.series.map(() => 36),
3586
4212
  fs.dataLabel * 0.85 + 5,
3587
4213
  w - 80 - (80 + legendW),
4214
+ options,
3588
4215
  { iconGap: 8 }
3589
4216
  ).boxH + 2 : 0;
3590
4217
  const padding = {
@@ -3643,7 +4270,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3643
4270
  ctx.lineTo(plotX + plotW, plotY + plotH);
3644
4271
  ctx.stroke();
3645
4272
  ctx.fillStyle = "#000";
3646
- ctx.font = getFont(fs.tick, font, cf, "bold");
4273
+ ctx.font = getFont(fs.tick, options, "bold");
3647
4274
  ctx.textAlign = "center";
3648
4275
  ctx.textBaseline = "top";
3649
4276
  for (let v = xMin; v <= xMax + xStep * 0.01; v += xStep) {
@@ -3655,7 +4282,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3655
4282
  ctx.stroke();
3656
4283
  const isLast = v + xStep > xMax + xStep * 0.01;
3657
4284
  if (!(isLast && data.xUnit)) {
3658
- ctx.fillText(formatTick3(v), x, plotY + plotH + 10);
4285
+ ctx.fillText(formatTick4(v), x, plotY + plotH + 10);
3659
4286
  }
3660
4287
  }
3661
4288
  ctx.textAlign = "right";
@@ -3669,12 +4296,12 @@ function renderHythergraph(ctx, w, h, data, options) {
3669
4296
  ctx.stroke();
3670
4297
  const isLast = v + yStep > yMax + yStep * 0.01;
3671
4298
  if (!(isLast && data.yUnit)) {
3672
- ctx.fillText(formatTick3(v), plotX - 10, y);
4299
+ ctx.fillText(formatTick4(v), plotX - 10, y);
3673
4300
  }
3674
4301
  }
3675
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
4302
+ ctx.font = getFont(fs.axisLabel, options, "bold");
3676
4303
  ctx.fillStyle = "#000";
3677
- const unitFont = (size) => getFont(size, font, cf, "bold");
4304
+ const unitFont = (size) => getFont(size, options, "bold");
3678
4305
  if (data.xUnit) {
3679
4306
  ctx.textAlign = "right";
3680
4307
  ctx.textBaseline = "top";
@@ -3716,7 +4343,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3716
4343
  drawMarker3(ctx, marker, cx, cy, 5);
3717
4344
  {
3718
4345
  ctx.fillStyle = "#000";
3719
- ctx.font = getFont(fs.dataLabel, font, cf, "bold");
4346
+ ctx.font = getFont(fs.dataLabel, options, "bold");
3720
4347
  ctx.textAlign = "left";
3721
4348
  ctx.textBaseline = "bottom";
3722
4349
  ctx.fillText(mLabels[i], cx + 9, cy - 5);
@@ -3726,9 +4353,9 @@ function renderHythergraph(ctx, w, h, data, options) {
3726
4353
  }
3727
4354
  if (showLegend) {
3728
4355
  const lfSize = fs.dataLabel * 0.85 + 5;
3729
- const LEGEND_FONT2 = "'Noto Sans KR', sans-serif";
4356
+ const LEGEND_FONT = sansFont(options);
3730
4357
  ctx.save();
3731
- ctx.font = `bold ${lfSize}px ${LEGEND_FONT2}`;
4358
+ ctx.font = `bold ${lfSize}px ${LEGEND_FONT}`;
3732
4359
  const iconW = 36;
3733
4360
  const iconGap = 8;
3734
4361
  const pad = 12;
@@ -3744,6 +4371,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3744
4371
  data.series.map(() => iconW),
3745
4372
  lfSize,
3746
4373
  boxW,
4374
+ options,
3747
4375
  { iconGap, padding: pad, spacing }
3748
4376
  );
3749
4377
  const boxH = layout.boxH;
@@ -3772,7 +4400,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3772
4400
  ctx.stroke();
3773
4401
  ctx.setLineDash([]);
3774
4402
  drawMarkerLegendIcon(ctx, MARKERS[i % MARKERS.length], cx + iconW / 2, cy);
3775
- ctx.font = `bold ${layout.fontSize}px ${LEGEND_FONT2}`;
4403
+ ctx.font = `bold ${layout.fontSize}px ${LEGEND_FONT}`;
3776
4404
  ctx.fillStyle = "#000";
3777
4405
  ctx.textAlign = "left";
3778
4406
  ctx.textBaseline = "middle";
@@ -3807,7 +4435,7 @@ function renderHythergraph(ctx, w, h, data, options) {
3807
4435
  ctx.stroke();
3808
4436
  ctx.setLineDash([]);
3809
4437
  drawMarkerLegendIcon(ctx, MARKERS[i % MARKERS.length], ix + iconW / 2, cy);
3810
- ctx.font = `bold ${lfSize}px ${LEGEND_FONT2}`;
4438
+ ctx.font = `bold ${lfSize}px ${LEGEND_FONT}`;
3811
4439
  ctx.fillStyle = "#000";
3812
4440
  ctx.textAlign = "left";
3813
4441
  ctx.textBaseline = "middle";
@@ -3817,10 +4445,10 @@ function renderHythergraph(ctx, w, h, data, options) {
3817
4445
  }
3818
4446
  ctx.restore();
3819
4447
  }
3820
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
3821
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
4448
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
4449
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
3822
4450
  }
3823
- function formatTick3(v) {
4451
+ function formatTick4(v) {
3824
4452
  return Math.abs(v) < 1e-4 ? "0" : Number(v.toFixed(2)).toString();
3825
4453
  }
3826
4454
 
@@ -3851,12 +4479,13 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3851
4479
  options.legendLabel1 || data.maleLabel,
3852
4480
  options.legendLabel2 || data.femaleLabel
3853
4481
  ];
3854
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
4482
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
3855
4483
  const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(
3856
4484
  ctx,
3857
4485
  legendLabels,
3858
4486
  options.fontSize.dataLabel * 0.85 + 5,
3859
- w - 60 - (80 + legendW)
4487
+ w - 60 - (80 + legendW),
4488
+ options
3860
4489
  ) : 0;
3861
4490
  const padding = {
3862
4491
  top: options.title ? 100 : 50,
@@ -3875,8 +4504,6 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3875
4504
  const plotY = padding.top;
3876
4505
  const plotW = w - padding.left - padding.right;
3877
4506
  const plotH = h - padding.top - padding.bottom;
3878
- const font = options.fontFamily;
3879
- const customFont = options.customFont;
3880
4507
  const displayAges = data.ages;
3881
4508
  const n = data.numericAgeAxis && displayAges.length > 0 ? displayAges.length : AGE_GROUPS.length;
3882
4509
  const isPercent = data.unit === "percent";
@@ -3921,7 +4548,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3921
4548
  ctx.lineTo(plotX + plotW, plotY + plotH);
3922
4549
  ctx.stroke();
3923
4550
  ctx.save();
3924
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
4551
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3925
4552
  const tickStep = pickTickStep(maxVal, halfW, ctx.measureText(fmtTick(maxVal)).width);
3926
4553
  ctx.restore();
3927
4554
  for (let v = tickStep; v <= maxVal; v += tickStep) {
@@ -3958,7 +4585,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3958
4585
  ctx.fillStyle = "#000";
3959
4586
  ctx.textBaseline = "middle";
3960
4587
  if (data.numericAgeAxis) {
3961
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
4588
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3962
4589
  ctx.textAlign = "right";
3963
4590
  const ageStep = 20;
3964
4591
  const topAge = n * 5;
@@ -3982,13 +4609,13 @@ function renderPyramidGraph(ctx, w, h, data, options) {
3982
4609
  w,
3983
4610
  h,
3984
4611
  options.fontSize.tick,
3985
- (size) => getFont(size, font, customFont, "bold")
4612
+ (size) => getFont(size, options, "bold")
3986
4613
  );
3987
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
4614
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
3988
4615
  ctx.textBaseline = "middle";
3989
4616
  }
3990
4617
  } else {
3991
- ctx.font = getFont(options.fontSize.tick * 0.75, font, customFont, "bold");
4618
+ ctx.font = getFont(options.fontSize.tick * 0.75, options, "bold");
3992
4619
  for (let i = 0; i < n; i++) {
3993
4620
  const y = plotY + plotH - (i + 1) * barH + barH / 2;
3994
4621
  if (side === "center") {
@@ -4003,7 +4630,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
4003
4630
  }
4004
4631
  }
4005
4632
  }
4006
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
4633
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
4007
4634
  ctx.textBaseline = "top";
4008
4635
  ctx.fillStyle = "#000";
4009
4636
  const xTickValues = [];
@@ -4045,7 +4672,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
4045
4672
  ctx.fillText(fmtTick(rv), rx, plotY + plotH + 10);
4046
4673
  }
4047
4674
  }
4048
- ctx.font = getFont(options.fontSize.axisLabel, font, customFont, "bold");
4675
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
4049
4676
  ctx.textBaseline = "bottom";
4050
4677
  ctx.fillStyle = "#000";
4051
4678
  ctx.textAlign = "center";
@@ -4053,7 +4680,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
4053
4680
  ctx.fillText(data.femaleLabel, centerX + halfW / 2, plotY - 16);
4054
4681
  ctx.save();
4055
4682
  if (data.axisLabelInline) {
4056
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
4683
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
4057
4684
  const lastTickHalfW = ctx.measureText(fmtTick(maxVal)).width / 2;
4058
4685
  ctx.textAlign = "left";
4059
4686
  ctx.textBaseline = "top";
@@ -4065,10 +4692,10 @@ function renderPyramidGraph(ctx, w, h, data, options) {
4065
4692
  w,
4066
4693
  h,
4067
4694
  options.fontSize.tick,
4068
- (size) => getFont(size, font, customFont, "bold")
4695
+ (size) => getFont(size, options, "bold")
4069
4696
  );
4070
4697
  } else {
4071
- ctx.font = getFont(options.fontSize.axisLabel * 0.85, font, customFont, "bold");
4698
+ ctx.font = getFont(options.fontSize.axisLabel * 0.85, options, "bold");
4072
4699
  const unitY = plotY + plotH + 10 + options.fontSize.tick + 22;
4073
4700
  ctx.textAlign = "left";
4074
4701
  drawFloatingLabel(
@@ -4079,12 +4706,12 @@ function renderPyramidGraph(ctx, w, h, data, options) {
4079
4706
  w,
4080
4707
  h,
4081
4708
  options.fontSize.axisLabel * 0.85,
4082
- (size) => getFont(size, font, customFont, "bold")
4709
+ (size) => getFont(size, options, "bold")
4083
4710
  );
4084
4711
  }
4085
4712
  ctx.restore();
4086
4713
  if (options.showDataLabels) {
4087
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
4714
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
4088
4715
  ctx.fillStyle = "#000";
4089
4716
  for (let i = 0; i < n; i++) {
4090
4717
  const y = plotY + plotH - (i + 1) * barH + barH / 2;
@@ -4102,10 +4729,11 @@ function renderPyramidGraph(ctx, w, h, data, options) {
4102
4729
  }
4103
4730
  }
4104
4731
  }
4105
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
4732
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
4106
4733
  if (showLegend) {
4107
4734
  drawLegend({
4108
4735
  ctx,
4736
+ fonts: options,
4109
4737
  items: [
4110
4738
  { type: "rect", fillStyle: "#666", strokeStyle: "#444", label: legendLabels[0] },
4111
4739
  { type: "rect", fillStyle: "#BBB", strokeStyle: "#888", label: legendLabels[1] }
@@ -4120,7 +4748,7 @@ function renderPyramidGraph(ctx, w, h, data, options) {
4120
4748
  fontSize: options.fontSize.dataLabel * 0.85 + 5
4121
4749
  });
4122
4750
  }
4123
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
4751
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
4124
4752
  }
4125
4753
 
4126
4754
  // src/core/graphs/RadarChart.ts
@@ -4134,15 +4762,13 @@ var LINE_STYLES2 = [
4134
4762
  var GRAY_SHADES = ["#000", "#444", "#777", "#AAA", "#CCC"];
4135
4763
  function renderRadarChart(ctx, w, h, data, options) {
4136
4764
  clearCanvas(ctx, w, h);
4137
- const font = options.fontFamily;
4138
- const cf = options.customFont;
4139
4765
  const fs = options.fontSize;
4140
4766
  const n = data.axisLabels.length;
4141
4767
  if (n < 3) return;
4142
4768
  const showLegend = options.showLegend && data.series.length > 1;
4143
4769
  const legendPos = options.legendPosition;
4144
4770
  const legendLabels = data.series.map((s) => s.label);
4145
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, fs.dataLabel * 0.85 + 5, "line") : 0;
4771
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, legendLabels, fs.dataLabel * 0.85 + 5, options, "line") : 0;
4146
4772
  const topPad = options.title ? 60 : 10;
4147
4773
  const rightPad = 60 + legendW;
4148
4774
  const leftPad = 60;
@@ -4154,6 +4780,7 @@ function renderRadarChart(ctx, w, h, data, options) {
4154
4780
  legendLabels,
4155
4781
  fs.dataLabel * 0.85 + 5,
4156
4782
  w - leftPad - rightPad,
4783
+ options,
4157
4784
  "line",
4158
4785
  30
4159
4786
  ));
@@ -4166,7 +4793,7 @@ function renderRadarChart(ctx, w, h, data, options) {
4166
4793
  const cy = topPad + availH / 2;
4167
4794
  const angles = Array.from({ length: n }, (_, i) => -Math.PI / 2 + 2 * Math.PI * i / n);
4168
4795
  const labelFontSize = fs.axisLabel * 0.85;
4169
- const makeLabelFont = (size) => getFont(size, font, cf, "bold");
4796
+ const makeLabelFont = (size) => getFont(size, options, "bold");
4170
4797
  const { radius, labelLines, labelSize } = fitAxisLabels(
4171
4798
  ctx,
4172
4799
  data.axisLabels,
@@ -4210,13 +4837,13 @@ function renderRadarChart(ctx, w, h, data, options) {
4210
4837
  ctx.stroke();
4211
4838
  }
4212
4839
  ctx.fillStyle = "#888";
4213
- ctx.font = getFont(fs.tick * 0.8, font, cf, "normal");
4840
+ ctx.font = getFont(fs.tick * 0.8, options, "normal");
4214
4841
  ctx.textAlign = "left";
4215
4842
  ctx.textBaseline = "middle";
4216
4843
  for (let step = 1; step <= data.gridSteps; step++) {
4217
4844
  const val = maxVal / data.gridSteps * step;
4218
4845
  const r = step / data.gridSteps * radius;
4219
- ctx.fillText(formatTick4(val), cx + 4, cy - r - 2);
4846
+ ctx.fillText(formatTick5(val), cx + 4, cy - r - 2);
4220
4847
  }
4221
4848
  ctx.fillStyle = "#000";
4222
4849
  ctx.font = makeLabelFont(labelSize);
@@ -4295,6 +4922,7 @@ function renderRadarChart(ctx, w, h, data, options) {
4295
4922
  });
4296
4923
  drawLegend({
4297
4924
  ctx,
4925
+ fonts: options,
4298
4926
  items,
4299
4927
  position: legendPos,
4300
4928
  plotX: plotX2,
@@ -4309,10 +4937,10 @@ function renderRadarChart(ctx, w, h, data, options) {
4309
4937
  }
4310
4938
  const plotX = leftPad;
4311
4939
  const plotW = availW;
4312
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
4313
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
4940
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
4941
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
4314
4942
  }
4315
- function formatTick4(v) {
4943
+ function formatTick5(v) {
4316
4944
  if (Number.isInteger(v)) return v.toString();
4317
4945
  return v.toFixed(1);
4318
4946
  }
@@ -4378,17 +5006,15 @@ function renderScatterGraph(ctx, w, h, data, options) {
4378
5006
  }
4379
5007
  }
4380
5008
  function renderNormal(ctx, w, h, data, options) {
4381
- const font = options.fontFamily;
4382
- const cf = options.customFont;
4383
5009
  const fs = options.fontSize;
4384
5010
  const outsideLegend = data.bubbleLegendPosition === "outside-right" && data.showBubble && data.points.length > 0;
4385
- const bubbleM = outsideLegend ? bubbleLegendMetrics(ctx, data, fs, font, cf) : null;
4386
- const fillM = outsideLegend ? measureFillLegend(ctx, data, fs, font, cf) : { boxW: 0, boxH: 0 };
5011
+ const bubbleM = outsideLegend ? bubbleLegendMetrics(ctx, data, fs, options) : null;
5012
+ const fillM = outsideLegend ? measureFillLegend(ctx, data, fs, options) : { boxW: 0, boxH: 0 };
4387
5013
  const legendW = Math.max(bubbleM?.boxW ?? 0, fillM.boxW);
4388
5014
  const examUnitW = (() => {
4389
5015
  if (data.examFrame !== true || !data.xUnit) return 0;
4390
5016
  ctx.save();
4391
- ctx.font = getFont(fs.tick, font, cf, "bold");
5017
+ ctx.font = getFont(fs.tick, options, "bold");
4392
5018
  const width = ctx.measureText(data.xUnit).width;
4393
5019
  ctx.restore();
4394
5020
  return width;
@@ -4403,7 +5029,7 @@ function renderNormal(ctx, w, h, data, options) {
4403
5029
  const yMax = data.yRange.auto ? yAuto.max : data.yRange.max;
4404
5030
  const xStep = data.xRange.step ?? (data.xRange.auto ? xAuto.step : (xMax - xMin) / 5);
4405
5031
  const yStep = data.yRange.step ?? (data.yRange.auto ? yAuto.step : (yMax - yMin) / 5);
4406
- const yName = measureYAxisName(ctx, data, w, yMin, yMax, yStep, fs, font, cf);
5032
+ const yName = measureYAxisName(ctx, data, w, yMin, yMax, yStep, fs, options);
4407
5033
  const padding = {
4408
5034
  top: options.title ? 100 : 50,
4409
5035
  right: legendW > 0 ? legendW + 40 : Math.max(60, examUnitW + 34),
@@ -4457,14 +5083,14 @@ function renderNormal(ctx, w, h, data, options) {
4457
5083
  ctx.stroke();
4458
5084
  }
4459
5085
  ctx.fillStyle = "#000";
4460
- ctx.font = getFont(fs.tick, font, cf, "bold");
5086
+ ctx.font = getFont(fs.tick, options, "bold");
4461
5087
  ctx.textAlign = "center";
4462
5088
  ctx.textBaseline = "top";
4463
5089
  const xTicks = [];
4464
5090
  for (let v = xMin; v <= xMax + xStep * 0.01; v += xStep) xTicks.push(v);
4465
5091
  const xStride = labelStride(
4466
5092
  plotW / Math.max(1, xTicks.length - 1),
4467
- widestLabel(ctx, xTicks.map(formatTick5))
5093
+ widestLabel(ctx, xTicks.map(formatTick6))
4468
5094
  );
4469
5095
  let lastXLabelRight = plotX + plotW;
4470
5096
  xTicks.forEach((v, i) => {
@@ -4475,7 +5101,7 @@ function renderNormal(ctx, w, h, data, options) {
4475
5101
  ctx.lineTo(x, plotY + plotH + 6);
4476
5102
  ctx.stroke();
4477
5103
  if (i % xStride === 0) {
4478
- const text = formatTick5(v);
5104
+ const text = formatTick6(v);
4479
5105
  ctx.fillText(text, x, plotY + plotH + 10);
4480
5106
  lastXLabelRight = Math.max(lastXLabelRight, x + ctx.measureText(text).width / 2);
4481
5107
  }
@@ -4493,9 +5119,9 @@ function renderNormal(ctx, w, h, data, options) {
4493
5119
  ctx.moveTo(plotX - 6, y);
4494
5120
  ctx.lineTo(plotX, y);
4495
5121
  ctx.stroke();
4496
- if (i % yStride === 0) ctx.fillText(formatTick5(v), plotX - 10, y);
5122
+ if (i % yStride === 0) ctx.fillText(formatTick6(v), plotX - 10, y);
4497
5123
  });
4498
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
5124
+ ctx.font = getFont(fs.axisLabel, options, "bold");
4499
5125
  ctx.textAlign = "center";
4500
5126
  ctx.textBaseline = "top";
4501
5127
  drawFloatingLabel(
@@ -4506,16 +5132,16 @@ function renderNormal(ctx, w, h, data, options) {
4506
5132
  w,
4507
5133
  h,
4508
5134
  fs.axisLabel,
4509
- (size) => getFont(size, font, cf, "bold")
5135
+ (size) => getFont(size, options, "bold")
4510
5136
  );
4511
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
5137
+ ctx.font = getFont(fs.axisLabel, options, "bold");
4512
5138
  if (data.xUnit) {
4513
5139
  if (exam) {
4514
- ctx.font = getFont(fs.tick, font, cf, "bold");
5140
+ ctx.font = getFont(fs.tick, options, "bold");
4515
5141
  ctx.textAlign = "left";
4516
5142
  ctx.textBaseline = "top";
4517
5143
  ctx.fillText(data.xUnit, lastXLabelRight + 6, plotY + plotH + 10);
4518
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
5144
+ ctx.font = getFont(fs.axisLabel, options, "bold");
4519
5145
  } else {
4520
5146
  ctx.textAlign = "right";
4521
5147
  ctx.textBaseline = "top";
@@ -4533,10 +5159,10 @@ function renderNormal(ctx, w, h, data, options) {
4533
5159
  w,
4534
5160
  h,
4535
5161
  fs.axisLabel,
4536
- (size) => getFont(size, font, cf, "bold")
5162
+ (size) => getFont(size, options, "bold")
4537
5163
  );
4538
5164
  }
4539
- ctx.font = getFont(yName.size, font, cf, "bold");
5165
+ ctx.font = getFont(yName.size, options, "bold");
4540
5166
  ctx.textAlign = "right";
4541
5167
  ctx.textBaseline = "middle";
4542
5168
  const yNameLineH = yName.size * 1.3;
@@ -4547,15 +5173,14 @@ function renderNormal(ctx, w, h, data, options) {
4547
5173
  clampLinesMiddle(ctx, yName.lines, plotY + plotH / 2, yNameLineH, h),
4548
5174
  yNameLineH
4549
5175
  );
4550
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
5176
+ ctx.font = getFont(fs.axisLabel, options, "bold");
4551
5177
  drawPoints(
4552
5178
  ctx,
4553
5179
  data,
4554
5180
  toCanvasX,
4555
5181
  toCanvasY,
4556
5182
  fs,
4557
- font,
4558
- cf,
5183
+ options,
4559
5184
  options.showDataLabels,
4560
5185
  { left: plotX, right: plotX + plotW, top: plotY, bottom: plotY + plotH },
4561
5186
  w,
@@ -4565,22 +5190,20 @@ function renderNormal(ctx, w, h, data, options) {
4565
5190
  const x = plotX + plotW + 20;
4566
5191
  let y = plotY;
4567
5192
  if (bubbleM) {
4568
- drawBubbleLegendAt(ctx, bubbleM, x, y, font, cf);
5193
+ drawBubbleLegendAt(ctx, bubbleM, x, y, options);
4569
5194
  y += bubbleM.boxH + 8;
4570
5195
  }
4571
- if (fillM.boxH > 0) drawFillLegendAt(ctx, data, x, y, fillM, fs, font, cf);
5196
+ if (fillM.boxH > 0) drawFillLegendAt(ctx, data, x, y, fillM, fs, options);
4572
5197
  } else if (data.showBubble && data.points.length > 0) {
4573
5198
  const avoid = bubbleRects(data, toCanvasX, toCanvasY);
4574
- const fill = measureFillLegend(ctx, data, fs, font, cf);
4575
- const used = drawBubbleLegend(ctx, data, plotX, plotY, plotW, plotH, fs, font, cf, avoid, fill.boxH);
4576
- drawFillLegend(ctx, data, plotX, plotY, plotW, plotH, used.bottom, fs, font, cf, used.corner);
5199
+ const fill = measureFillLegend(ctx, data, fs, options);
5200
+ const used = drawBubbleLegend(ctx, data, plotX, plotY, plotW, plotH, fs, options, avoid, fill.boxH);
5201
+ drawFillLegend(ctx, data, plotX, plotY, plotW, plotH, used.bottom, fs, options, used.corner);
4577
5202
  }
4578
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
4579
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, sourceInline: options.sourceInline, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
5203
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
5204
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, sourceInline: options.sourceInline, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
4580
5205
  }
4581
5206
  function renderDeviation(ctx, w, h, data, options) {
4582
- const font = options.fontFamily;
4583
- const cf = options.customFont;
4584
5207
  const fs = options.fontSize;
4585
5208
  const padding = {
4586
5209
  // 상자 축이름은 그래프 위에 놓이고 그 아래에 y 단위가 들어가므로 자리를 더 준다
@@ -4657,7 +5280,7 @@ function renderDeviation(ctx, w, h, data, options) {
4657
5280
  ctx.lineTo(plotX + plotW, originY);
4658
5281
  ctx.stroke();
4659
5282
  ctx.fillStyle = "#000";
4660
- ctx.font = getFont(fs.tick, font, cf, "bold");
5283
+ ctx.font = getFont(fs.tick, options, "bold");
4661
5284
  ctx.textAlign = "right";
4662
5285
  ctx.textBaseline = "top";
4663
5286
  ctx.fillText("0", originX - 6, originY + 4);
@@ -4669,7 +5292,7 @@ function renderDeviation(ctx, w, h, data, options) {
4669
5292
  for (let v = -xLimit; v <= xLimit + xStep * 0.01; v += xStep) devXTicks.push(v);
4670
5293
  const devXStride = labelStride(
4671
5294
  plotW / Math.max(1, devXTicks.length - 1),
4672
- widestLabel(ctx, devXTicks.map(formatTick5))
5295
+ widestLabel(ctx, devXTicks.map(formatTick6))
4673
5296
  );
4674
5297
  devXTicks.forEach((v, i) => {
4675
5298
  if (Math.abs(v) < xStep * 0.01) return;
@@ -4679,14 +5302,14 @@ function renderDeviation(ctx, w, h, data, options) {
4679
5302
  ctx.moveTo(x, xTickBase);
4680
5303
  ctx.lineTo(x, xTickBase + 6);
4681
5304
  ctx.stroke();
4682
- if (i % devXStride === 0) ctx.fillText(formatTick5(v), x, xTickBase + 10);
5305
+ if (i % devXStride === 0) ctx.fillText(formatTick6(v), x, xTickBase + 10);
4683
5306
  });
4684
5307
  ctx.textAlign = "right";
4685
5308
  ctx.textBaseline = "middle";
4686
5309
  const devYTicks = [];
4687
5310
  for (let v = -yLimit; v <= yLimit + yStep * 0.01; v += yStep) devYTicks.push(v);
4688
5311
  const devYStride = labelStride(plotH / Math.max(1, devYTicks.length - 1), fs.tick * 1.1);
4689
- const yTickTextW = widestLabel(ctx, devYTicks.map(formatTick5));
5312
+ const yTickTextW = widestLabel(ctx, devYTicks.map(formatTick6));
4690
5313
  devYTicks.forEach((v, i) => {
4691
5314
  if (Math.abs(v) < yStep * 0.01) return;
4692
5315
  const y = toCanvasY(v);
@@ -4695,9 +5318,9 @@ function renderDeviation(ctx, w, h, data, options) {
4695
5318
  ctx.moveTo(yTickBase - 6, y);
4696
5319
  ctx.lineTo(yTickBase, y);
4697
5320
  ctx.stroke();
4698
- if (i % devYStride === 0) ctx.fillText(formatTick5(v), yTickBase - 10, y);
5321
+ if (i % devYStride === 0) ctx.fillText(formatTick6(v), yTickBase - 10, y);
4699
5322
  });
4700
- ctx.font = getFont(fs.axisLabel, font, cf, "bold");
5323
+ ctx.font = getFont(fs.axisLabel, options, "bold");
4701
5324
  if (!data.boxedAxisLabels) {
4702
5325
  ctx.textAlign = "center";
4703
5326
  ctx.textBaseline = "top";
@@ -4736,8 +5359,7 @@ function renderDeviation(ctx, w, h, data, options) {
4736
5359
  toCanvasX,
4737
5360
  toCanvasY,
4738
5361
  fs,
4739
- font,
4740
- cf,
5362
+ options,
4741
5363
  options.showDataLabels,
4742
5364
  { left: plotX, right: plotX + plotW, top: plotY, bottom: plotY + plotH },
4743
5365
  w,
@@ -4752,15 +5374,14 @@ function renderDeviation(ctx, w, h, data, options) {
4752
5374
  plotW,
4753
5375
  plotH,
4754
5376
  fs,
4755
- font,
4756
- cf,
5377
+ options,
4757
5378
  bubbleRects(data, toCanvasX, toCanvasY)
4758
5379
  );
4759
5380
  }
4760
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
4761
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, sourceInline: options.sourceInline, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
5381
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: fs.title, canvasWidth: w });
5382
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, sourceInline: options.sourceInline, footnotes: options.footnotes, fontSize: fs.dataLabel, canvasWidth: w });
4762
5383
  }
4763
- function drawPoints(ctx, data, toX, toY, fs, font, cf, showLabels, bounds, canvasW, canvasH) {
5384
+ function drawPoints(ctx, data, toX, toY, fs, options, showLabels, bounds, canvasW, canvasH) {
4764
5385
  const maxSize = data.points.length > 0 ? Math.max(...data.points.map((p) => p.size), 1) : 1;
4765
5386
  const placer = new LabelPlacer();
4766
5387
  if (bounds) {
@@ -4769,8 +5390,8 @@ function drawPoints(ctx, data, toX, toY, fs, font, cf, showLabels, bounds, canva
4769
5390
  placer.reserveCircle(toX(pt.x), toY(pt.y), r);
4770
5391
  }
4771
5392
  }
4772
- const drawOrder = [...data.points].sort((a, b) => b.size - a.size);
4773
- for (const pt of drawOrder) {
5393
+ const drawOrder2 = [...data.points].sort((a, b) => b.size - a.size);
5394
+ for (const pt of drawOrder2) {
4774
5395
  const cx = toX(pt.x);
4775
5396
  const cy = toY(pt.y);
4776
5397
  if (data.showBubble && pt.size > 0) {
@@ -4794,7 +5415,7 @@ function drawPoints(ctx, data, toX, toY, fs, font, cf, showLabels, bounds, canva
4794
5415
  }
4795
5416
  if (pt.label) {
4796
5417
  ctx.fillStyle = "#000";
4797
- ctx.font = getFont(fs.dataLabel, font, cf, "bold");
5418
+ ctx.font = getFont(fs.dataLabel, options, "bold");
4798
5419
  const offset = data.showBubble && pt.size > 0 ? pt.size / maxSize * data.bubbleScale + 4 : 8;
4799
5420
  if (bounds) {
4800
5421
  placer.place(ctx, pt.label, cx, cy, {
@@ -4810,7 +5431,7 @@ function drawPoints(ctx, data, toX, toY, fs, font, cf, showLabels, bounds, canva
4810
5431
  }
4811
5432
  if (showLabels) {
4812
5433
  ctx.fillStyle = "#555";
4813
- ctx.font = getFont(fs.dataLabel * 0.8, font, cf, "normal");
5434
+ ctx.font = getFont(fs.dataLabel * 0.8, options, "normal");
4814
5435
  ctx.textAlign = "left";
4815
5436
  ctx.textBaseline = "top";
4816
5437
  const offset = data.showBubble && pt.size > 0 ? pt.size / maxSize * data.bubbleScale + 4 : 8;
@@ -4836,16 +5457,16 @@ function symmetricRange(absMax) {
4836
5457
  const step = limit / 4;
4837
5458
  return { limit, step };
4838
5459
  }
4839
- function formatTick5(v) {
5460
+ function formatTick6(v) {
4840
5461
  return Math.abs(v) < 1e-4 ? "0" : Number(v.toFixed(2)).toString();
4841
5462
  }
4842
- function measureYAxisName(ctx, data, w, yMin, yMax, yStep, fs, font, cf) {
5463
+ function measureYAxisName(ctx, data, w, yMin, yMax, yStep, fs, options) {
4843
5464
  ctx.save();
4844
- ctx.font = getFont(fs.tick, font, cf, "bold");
5465
+ ctx.font = getFont(fs.tick, options, "bold");
4845
5466
  const ticks = [];
4846
5467
  for (let v = yMin; v <= yMax + yStep * 0.01; v += yStep) ticks.push(v);
4847
- const tickW = widestLabel(ctx, ticks.map(formatTick5));
4848
- const makeFont = (size2) => getFont(size2, font, cf, "bold");
5468
+ const tickW = widestLabel(ctx, ticks.map(formatTick6));
5469
+ const makeFont = (size2) => getFont(size2, options, "bold");
4849
5470
  ctx.font = makeFont(fs.axisLabel);
4850
5471
  const given = (data.yLabel || "").split("\\n");
4851
5472
  const budget = Math.max(40, w * 0.3 - 18 - tickW - 12);
@@ -4894,12 +5515,12 @@ function pickLegendCorner(preferred, boxW, boxH, plotX, plotY, plotW, plotH, avo
4894
5515
  }
4895
5516
  return { corner: best.corner, x: best.x, y: best.y };
4896
5517
  }
4897
- function measureFillLegend(ctx, data, fs, font, cf) {
5518
+ function measureFillLegend(ctx, data, fs, options) {
4898
5519
  const legend = data.fillLegend;
4899
5520
  if (!legend || legend.items.length === 0) return { boxW: 0, boxH: 0 };
4900
5521
  const fontSize = fs.dataLabel * 0.8;
4901
5522
  ctx.save();
4902
- ctx.font = getFont(fontSize, font, cf, "bold");
5523
+ ctx.font = getFont(fontSize, options, "bold");
4903
5524
  const rowH = fontSize * 1.5;
4904
5525
  const swatch = fontSize * 0.95;
4905
5526
  const pad = 8;
@@ -4911,7 +5532,7 @@ function measureFillLegend(ctx, data, fs, font, cf) {
4911
5532
  ctx.restore();
4912
5533
  return { boxW: textW + pad * 2, boxH: titleH + legend.items.length * rowH + pad * 2 };
4913
5534
  }
4914
- function bubbleLegendMetrics(ctx, data, fs, font, cf) {
5535
+ function bubbleLegendMetrics(ctx, data, fs, options) {
4915
5536
  const sizes = data.points.map((p) => p.size).filter((s) => s > 0);
4916
5537
  if (sizes.length === 0) return null;
4917
5538
  const maxSize = Math.max(...sizes);
@@ -4924,7 +5545,7 @@ function bubbleLegendMetrics(ctx, data, fs, font, cf) {
4924
5545
  const maxR = radiusOf(uniqueSteps[0]);
4925
5546
  const labelFontSize = fs.dataLabel * 0.85;
4926
5547
  ctx.save();
4927
- ctx.font = getFont(labelFontSize, font, cf, "bold");
5548
+ ctx.font = getFont(labelFontSize, options, "bold");
4928
5549
  const labelW = Math.max(50, ...uniqueSteps.map((v) => ctx.measureText(labelOf(v)).width));
4929
5550
  ctx.restore();
4930
5551
  const pad = 14;
@@ -4941,7 +5562,7 @@ function bubbleLegendMetrics(ctx, data, fs, font, cf) {
4941
5562
  const boxH = maxR * 2 + lowest + pad * 2 + 10;
4942
5563
  return { uniqueSteps, labelOf, radiusOf, maxR, boxW, boxH, labelOffsets, lowest, labelFontSize, pad };
4943
5564
  }
4944
- function drawBubbleLegendAt(ctx, m, boxX, boxY, font, cf) {
5565
+ function drawBubbleLegendAt(ctx, m, boxX, boxY, options) {
4945
5566
  ctx.fillStyle = "#fff";
4946
5567
  ctx.strokeStyle = "#666";
4947
5568
  ctx.lineWidth = 1.5;
@@ -4970,18 +5591,18 @@ function drawBubbleLegendAt(ctx, m, boxX, boxY, font, cf) {
4970
5591
  ctx.stroke();
4971
5592
  ctx.restore();
4972
5593
  ctx.fillStyle = "#000";
4973
- ctx.font = getFont(m.labelFontSize, font, cf, "bold");
5594
+ ctx.font = getFont(m.labelFontSize, options, "bold");
4974
5595
  ctx.textAlign = "left";
4975
5596
  ctx.textBaseline = "middle";
4976
5597
  ctx.fillText(m.labelOf(size), circleX + m.maxR + 16, bottomCircleY + m.labelOffsets[si]);
4977
5598
  });
4978
5599
  }
4979
- function drawBubbleLegend(ctx, data, plotX, plotY, plotW, plotH, fs, font, cf, avoid = [], extraH = 0) {
5600
+ function drawBubbleLegend(ctx, data, plotX, plotY, plotW, plotH, fs, options, avoid = [], extraH = 0) {
4980
5601
  const preferred = insideCorner(data.bubbleLegendPosition);
4981
- const m = bubbleLegendMetrics(ctx, data, fs, font, cf);
5602
+ const m = bubbleLegendMetrics(ctx, data, fs, options);
4982
5603
  if (!m) return { height: 0, corner: preferred, bottom: 0 };
4983
5604
  const spot = pickLegendCorner(preferred, m.boxW, m.boxH, plotX, plotY, plotW, plotH, avoid, extraH);
4984
- drawBubbleLegendAt(ctx, m, spot.x, spot.y, font, cf);
5605
+ drawBubbleLegendAt(ctx, m, spot.x, spot.y, options);
4985
5606
  return { height: m.boxH, corner: spot.corner, bottom: spot.y + m.boxH };
4986
5607
  }
4987
5608
  function drawBoxedLabel(ctx, text, x, y, fontSize, place) {
@@ -5009,12 +5630,12 @@ function drawBoxedLabel(ctx, text, x, y, fontSize, place) {
5009
5630
  }
5010
5631
  ctx.restore();
5011
5632
  }
5012
- function drawFillLegendAt(ctx, data, boxX, boxY, m, fs, font, cf) {
5633
+ function drawFillLegendAt(ctx, data, boxX, boxY, m, fs, options) {
5013
5634
  const legend = data.fillLegend;
5014
5635
  if (!legend || legend.items.length === 0) return;
5015
5636
  const fontSize = fs.dataLabel * 0.8;
5016
5637
  ctx.save();
5017
- ctx.font = getFont(fontSize, font, cf, "bold");
5638
+ ctx.font = getFont(fontSize, options, "bold");
5018
5639
  const rowH = fontSize * 1.5;
5019
5640
  const swatch = fontSize * 0.95;
5020
5641
  const pad = 8;
@@ -5046,13 +5667,13 @@ function drawFillLegendAt(ctx, data, boxX, boxY, m, fs, font, cf) {
5046
5667
  }
5047
5668
  ctx.restore();
5048
5669
  }
5049
- function drawFillLegend(ctx, data, plotX, plotY, plotW, plotH, stackBelow, fs, font, cf, corner = "bottom-right") {
5670
+ function drawFillLegend(ctx, data, plotX, plotY, plotW, plotH, stackBelow, fs, options, corner = "bottom-right") {
5050
5671
  const legend = data.fillLegend;
5051
5672
  if (!legend || legend.items.length === 0) return;
5052
- const m = measureFillLegend(ctx, data, fs, font, cf);
5673
+ const m = measureFillLegend(ctx, data, fs, options);
5053
5674
  const boxX = corner.endsWith("right") ? plotX + plotW - m.boxW - 10 : plotX + 10;
5054
5675
  const boxY = stackBelow > 0 ? stackBelow + 6 : corner.startsWith("top") ? plotY + 10 : plotY + plotH - m.boxH - 10;
5055
- drawFillLegendAt(ctx, data, boxX, boxY, m, fs, font, cf);
5676
+ drawFillLegendAt(ctx, data, boxX, boxY, m, fs, options);
5056
5677
  }
5057
5678
 
5058
5679
  // src/core/graphs/StackedBarPie.ts
@@ -5067,13 +5688,14 @@ function renderStackedGraph(ctx, w, h, data, options) {
5067
5688
  function renderStackedBar(ctx, w, h, data, options) {
5068
5689
  const showLegend = options.showLegend;
5069
5690
  const legendPos = options.legendPosition;
5070
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
5691
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
5071
5692
  const isVertical = data.barDirection === "vertical";
5072
5693
  const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(
5073
5694
  ctx,
5074
5695
  data.seriesLabels,
5075
5696
  options.fontSize.dataLabel * 0.85 + 5,
5076
- w - (isVertical ? 80 : 100) - (isVertical ? 60 + legendW : 160 + legendW)
5697
+ w - (isVertical ? 80 : 100) - (isVertical ? 60 + legendW : 160 + legendW),
5698
+ options
5077
5699
  ) : 0;
5078
5700
  const padding = {
5079
5701
  top: options.title ? 100 : 50,
@@ -5092,8 +5714,6 @@ function renderStackedBar(ctx, w, h, data, options) {
5092
5714
  const plotY = padding.top;
5093
5715
  const plotW = w - padding.left - padding.right;
5094
5716
  const plotH = h - padding.top - padding.bottom;
5095
- const font = options.fontFamily;
5096
- const customFont = options.customFont;
5097
5717
  const n = data.categories.length;
5098
5718
  const sCount = data.seriesLabels.length;
5099
5719
  ctx.strokeStyle = "#000";
@@ -5107,7 +5727,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5107
5727
  ctx.lineTo(plotX, plotY);
5108
5728
  ctx.stroke();
5109
5729
  const stepV = data.axisStep ?? 20;
5110
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
5730
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5111
5731
  ctx.fillStyle = "#000";
5112
5732
  ctx.textAlign = "right";
5113
5733
  ctx.textBaseline = "middle";
@@ -5131,7 +5751,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5131
5751
  ctx.restore();
5132
5752
  }
5133
5753
  }
5134
- ctx.font = getFont(options.fontSize.axisLabel, font, customFont, "bold");
5754
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
5135
5755
  ctx.textAlign = "right";
5136
5756
  ctx.textBaseline = "bottom";
5137
5757
  ctx.fillText(data.unit, plotX - 10, plotY - 16);
@@ -5152,10 +5772,10 @@ function renderStackedBar(ctx, w, h, data, options) {
5152
5772
  ctx.lineWidth = 0.8;
5153
5773
  ctx.strokeRect(cx - barW / 2, y, barW, barH);
5154
5774
  if (data.labelInSegment) {
5155
- drawSegmentLabel(ctx, data, options, s, cx, y + barH / 2, barW, barH, lightAt(data, s), font, customFont);
5775
+ drawSegmentLabel(ctx, data, options, s, cx, y + barH / 2, barW, barH, lightAt(data, s));
5156
5776
  } else if (options.showDataLabels && barH > options.fontSize.dataLabel) {
5157
5777
  ctx.fillStyle = lightAt(data, s) ? "#000" : "#fff";
5158
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
5778
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
5159
5779
  ctx.textAlign = "center";
5160
5780
  ctx.textBaseline = "middle";
5161
5781
  ctx.fillText(String(val), cx, y + barH / 2);
@@ -5163,7 +5783,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5163
5783
  cumY += barH;
5164
5784
  }
5165
5785
  ctx.fillStyle = "#000";
5166
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
5786
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5167
5787
  ctx.textAlign = "center";
5168
5788
  ctx.textBaseline = "top";
5169
5789
  ctx.fillText(data.categories[c].label, cx, plotY + plotH + 12);
@@ -5177,7 +5797,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5177
5797
  ctx.lineTo(plotX, plotY);
5178
5798
  ctx.stroke();
5179
5799
  const stepH = data.axisStep ?? 20;
5180
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
5800
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5181
5801
  ctx.fillStyle = "#000";
5182
5802
  ctx.textAlign = "center";
5183
5803
  ctx.textBaseline = "top";
@@ -5201,7 +5821,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5201
5821
  ctx.restore();
5202
5822
  }
5203
5823
  }
5204
- ctx.font = getFont(options.fontSize.axisLabel, font, customFont, "bold");
5824
+ ctx.font = getFont(options.fontSize.axisLabel, options, "bold");
5205
5825
  ctx.textAlign = "left";
5206
5826
  ctx.textBaseline = "top";
5207
5827
  ctx.fillText(data.unit, plotX + plotW + 30, plotY + plotH + 10);
@@ -5223,7 +5843,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5223
5843
  ctx.strokeRect(x, cy - barH / 2, bw, barH);
5224
5844
  if (options.showDataLabels && bw > options.fontSize.dataLabel * 2) {
5225
5845
  ctx.fillStyle = lightAt(data, s) ? "#000" : "#fff";
5226
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
5846
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
5227
5847
  ctx.textAlign = "center";
5228
5848
  ctx.textBaseline = "middle";
5229
5849
  ctx.fillText(String(val), x + bw / 2, cy);
@@ -5231,13 +5851,13 @@ function renderStackedBar(ctx, w, h, data, options) {
5231
5851
  cumX += bw;
5232
5852
  }
5233
5853
  ctx.fillStyle = "#000";
5234
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
5854
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5235
5855
  ctx.textAlign = "right";
5236
5856
  ctx.textBaseline = "middle";
5237
5857
  ctx.fillText(data.categories[c].label, plotX - 10, cy);
5238
5858
  }
5239
5859
  }
5240
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
5860
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
5241
5861
  if (showLegend) {
5242
5862
  const items = data.seriesLabels.map((label, i) => ({
5243
5863
  type: "rect",
@@ -5247,6 +5867,7 @@ function renderStackedBar(ctx, w, h, data, options) {
5247
5867
  }));
5248
5868
  drawLegend({
5249
5869
  ctx,
5870
+ fonts: options,
5250
5871
  items,
5251
5872
  position: legendPos,
5252
5873
  plotX,
@@ -5258,17 +5879,18 @@ function renderStackedBar(ctx, w, h, data, options) {
5258
5879
  fontSize: options.fontSize.dataLabel * 0.85 + 5
5259
5880
  });
5260
5881
  }
5261
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, sourceLeft: options.sourceLeft, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
5882
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, sourceLeft: options.sourceLeft, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
5262
5883
  }
5263
5884
  function renderPieChart(ctx, w, h, data, options) {
5264
5885
  const showLegend = options.showLegend;
5265
5886
  const legendPos = options.legendPosition;
5266
- const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5) : 0;
5887
+ const legendW = showLegend && legendPos === "right" ? measureLegendWidth(ctx, data.seriesLabels, options.fontSize.dataLabel * 0.85 + 5, options) : 0;
5267
5888
  const legendReserve = showLegend && legendPos === "bottom" ? measureBottomLegend(
5268
5889
  ctx,
5269
5890
  data.seriesLabels,
5270
5891
  options.fontSize.dataLabel * 0.85 + 5,
5271
5892
  w - 60 - (60 + legendW),
5893
+ options,
5272
5894
  "rect",
5273
5895
  16
5274
5896
  ) : 0;
@@ -5289,8 +5911,6 @@ function renderPieChart(ctx, w, h, data, options) {
5289
5911
  const plotY = padding.top;
5290
5912
  const plotW = w - padding.left - padding.right;
5291
5913
  const plotH = h - padding.top - padding.bottom;
5292
- const font = options.fontFamily;
5293
- const customFont = options.customFont;
5294
5914
  const n = data.categories.length;
5295
5915
  const sCount = data.seriesLabels.length;
5296
5916
  const pieScale = (data.pieScale ?? 100) / 100;
@@ -5330,7 +5950,7 @@ function renderPieChart(ctx, w, h, data, options) {
5330
5950
  const lx = cx + Math.cos(midAngle) * maxR * 0.65;
5331
5951
  const ly = cy + Math.sin(midAngle) * maxR * 0.65;
5332
5952
  ctx.fillStyle = lightAt(data, s) ? "#000" : "#fff";
5333
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont, "bold");
5953
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options, "bold");
5334
5954
  ctx.textAlign = "center";
5335
5955
  ctx.textBaseline = "middle";
5336
5956
  ctx.fillText(String(val), lx, ly);
@@ -5343,12 +5963,12 @@ function renderPieChart(ctx, w, h, data, options) {
5343
5963
  ctx.arc(cx, cy, maxR, 0, Math.PI * 2);
5344
5964
  ctx.stroke();
5345
5965
  ctx.fillStyle = "#000";
5346
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
5966
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5347
5967
  ctx.textAlign = "center";
5348
5968
  ctx.textBaseline = "top";
5349
5969
  ctx.fillText(data.categories[c].label, cx, cy + maxR + 12);
5350
5970
  }
5351
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
5971
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
5352
5972
  if (showLegend) {
5353
5973
  const items = data.seriesLabels.map((label, i) => ({
5354
5974
  type: "rect",
@@ -5366,6 +5986,7 @@ function renderPieChart(ctx, w, h, data, options) {
5366
5986
  const effectivePlotH = pieBottom - plotY;
5367
5987
  drawLegend({
5368
5988
  ctx,
5989
+ fonts: options,
5369
5990
  items,
5370
5991
  position: legendPos,
5371
5992
  plotX,
@@ -5383,6 +6004,7 @@ function renderPieChart(ctx, w, h, data, options) {
5383
6004
  const effectivePlotH = pieBottom - plotY;
5384
6005
  drawLegend({
5385
6006
  ctx,
6007
+ fonts: options,
5386
6008
  items,
5387
6009
  position: legendPos,
5388
6010
  plotX,
@@ -5396,7 +6018,7 @@ function renderPieChart(ctx, w, h, data, options) {
5396
6018
  });
5397
6019
  }
5398
6020
  }
5399
- drawSourceAndFootnote({ ctx, plotX, plotW, height: h, source: options.source, sourceLeft: options.sourceLeft, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
6021
+ drawSourceAndFootnote({ ctx, fonts: options, plotX, plotW, height: h, source: options.source, sourceLeft: options.sourceLeft, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
5400
6022
  }
5401
6023
  function stackOrder(cat, sCount) {
5402
6024
  const given = cat.segmentOrder;
@@ -5414,12 +6036,12 @@ function lightAt(data, i) {
5414
6036
  const v = data.seriesFills?.[i];
5415
6037
  return v === void 0 || v === null ? isLightFill(i) : isLightFillValue(v);
5416
6038
  }
5417
- function drawSegmentLabel(ctx, data, options, s, cx, cy, barW, barH, light, font, customFont) {
6039
+ function drawSegmentLabel(ctx, data, options, s, cx, cy, barW, barH, light) {
5418
6040
  const text = data.seriesLabels[s] ?? "";
5419
6041
  const size = options.fontSize.dataLabel * 0.9;
5420
6042
  if (!text || barH < size * 1.1) return;
5421
- const family = data.seriesIsSymbol && !data.seriesIsSymbol[s] ? "sans" : font;
5422
- ctx.font = getFont(size, family, customFont, "bold");
6043
+ const family = data.seriesIsSymbol && !data.seriesIsSymbol[s] ? "sans" : options.fontFamily;
6044
+ ctx.font = getFont(size, options, "bold", family);
5423
6045
  ctx.textAlign = "center";
5424
6046
  ctx.textBaseline = "middle";
5425
6047
  const maxW = Math.max(1, barW - 6);
@@ -5484,8 +6106,6 @@ function renderTernaryGraph(ctx, w, h, data, options) {
5484
6106
  const triH = triSize * Math.sqrt(3) / 2;
5485
6107
  const cx = plotX + plotW / 2;
5486
6108
  const cy = padding.top + 11 + triH * 2 / 3;
5487
- const font = options.fontFamily;
5488
- const customFont = options.customFont;
5489
6109
  const topPt = { x: cx, y: cy - triH * 2 / 3 };
5490
6110
  const leftPt = { x: cx - triSize / 2, y: cy + triH / 3 };
5491
6111
  const rightPt = { x: cx + triSize / 2, y: cy + triH / 3 };
@@ -5529,7 +6149,7 @@ function renderTernaryGraph(ctx, w, h, data, options) {
5529
6149
  const bTick = { x: Math.cos(Math.PI / 3), y: Math.sin(Math.PI / 3) };
5530
6150
  const cTick = { x: Math.cos(Math.PI / 3), y: -Math.sin(Math.PI / 3) };
5531
6151
  ctx.fillStyle = "#000";
5532
- ctx.font = getFont(options.fontSize.tick, font, customFont, "bold");
6152
+ ctx.font = getFont(options.fontSize.tick, options, "bold");
5533
6153
  ctx.strokeStyle = "#000";
5534
6154
  ctx.lineWidth = 1.5;
5535
6155
  for (let i = 0; i <= steps; i++) {
@@ -5561,7 +6181,7 @@ function renderTernaryGraph(ctx, w, h, data, options) {
5561
6181
  const cAt = nudgeInside(ctx, cLabel, cPos.x + cTick.x * tickLen + 4, cPos.y + cTick.y * tickLen, w, h);
5562
6182
  ctx.fillText(cLabel, cAt.x, cAt.y);
5563
6183
  }
5564
- ctx.font = getFont(nameFit.fontSize, font, customFont, "bold");
6184
+ ctx.font = getFont(nameFit.fontSize, options, "bold");
5565
6185
  ctx.fillStyle = "#000";
5566
6186
  const labelLineH = nameFit.fontSize * 1.3;
5567
6187
  const spots = namePlaces(w, plotX + plotW / 2, padding.top, triSize, nameFit.lines, labelLineH);
@@ -5579,21 +6199,21 @@ function renderTernaryGraph(ctx, w, h, data, options) {
5579
6199
  ctx.fill();
5580
6200
  if (p.label) {
5581
6201
  ctx.fillStyle = "#000";
5582
- ctx.font = getFont(options.fontSize.dataLabel, font, customFont, "bold");
6202
+ ctx.font = getFont(options.fontSize.dataLabel, options, "bold");
5583
6203
  ctx.textAlign = "left";
5584
6204
  ctx.textBaseline = "bottom";
5585
6205
  ctx.fillText(p.label, x + 10, y - 4);
5586
6206
  }
5587
6207
  if (options.showDataLabels) {
5588
6208
  ctx.fillStyle = "#555";
5589
- ctx.font = getFont(options.fontSize.dataLabel * 0.8, font, customFont);
6209
+ ctx.font = getFont(options.fontSize.dataLabel * 0.8, options);
5590
6210
  ctx.textAlign = "left";
5591
6211
  ctx.textBaseline = "top";
5592
6212
  ctx.fillText(`(${p.a}, ${p.b}, ${p.c})`, x + 10, y + 4);
5593
6213
  }
5594
6214
  }
5595
- drawTitle({ ctx, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
5596
- drawSourceAndFootnote({ ctx, plotX: leftPt.x, plotW: rightPt.x - leftPt.x, height: h, source: options.source, footnotes: options.footnotes, fontSize: options.fontSize.dataLabel, canvasWidth: w });
6215
+ drawTitle({ ctx, fonts: options, plotX, plotW, title: options.title, fontSize: options.fontSize.title, canvasWidth: w });
6216
+ 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 });
5597
6217
  }
5598
6218
  var SHRINK_LIMIT3 = 0.8;
5599
6219
  function namePlaces(_w, cx, padTop, triSize, lines, lineH) {
@@ -5613,7 +6233,7 @@ function namePlaces(_w, cx, padTop, triSize, lines, lineH) {
5613
6233
  }
5614
6234
  function fitAxisNames(ctx, labels, options, w, h, cx, padTop, maxSize) {
5615
6235
  ctx.save();
5616
- const makeFont = (size2) => getFont(size2, options.fontFamily, options.customFont, "bold");
6236
+ const makeFont = (size2) => getFont(size2, options, "bold");
5617
6237
  let fontSize = options.fontSize.axisLabel * 1.3;
5618
6238
  const given = labels.map((l) => (l || "").split("\\n"));
5619
6239
  let lines = given.map((g) => g.slice());
@@ -5667,6 +6287,7 @@ var REGISTRY = {
5667
6287
  // 위 climate 주석 참고 — 이 칸의 렌더러만 바꾼 오배선은 컴파일러가 잡지 못한다.
5668
6288
  "deviation-a": { render: renderDeviationAGraph, createDefaultData: createDefaultDeviationAData },
5669
6289
  "deviation-b": { render: renderDeviationBGraph, createDefaultData: createDefaultDeviationBData },
6290
+ "econ-plane": { render: renderEconPlane, createDefaultData: createDefaultEconPlaneData },
5670
6291
  hythergraph: { render: renderHythergraph, createDefaultData: createDefaultHythergraphData },
5671
6292
  line: { render: renderLineGraph, createDefaultData: createDefaultLineData },
5672
6293
  "matrix-table": { render: renderMatrixTable, createDefaultData: createDefaultMatrixTableData },
@@ -5927,6 +6548,7 @@ function mergeOptions(base, patch) {
5927
6548
  ...base,
5928
6549
  ...patch,
5929
6550
  fontSize: { ...base.fontSize, ...patch?.fontSize },
6551
+ fontStack: { ...base.fontStack, ...patch?.fontStack },
5930
6552
  footnotes: [...patch?.footnotes ?? base.footnotes]
5931
6553
  };
5932
6554
  }
@@ -5954,7 +6576,7 @@ var CsatChart = class {
5954
6576
  *
5955
6577
  * 필드가 아니라 **메서드**로 둔다. `static readonly ensureFonts = ensureFonts`
5956
6578
  * 로 적으면 ES2020 로 낮출 때 클래스 «뒤» 의 대입문이 되는데, 그것은 지울 수
5957
- * 없는 부수효과라 이 클래스와 레지스트리와 렌더러 16종이 모든 번들에 박힌다.
6579
+ * 없는 부수효과라 이 클래스와 레지스트리와 렌더러 17종이 모든 번들에 박힌다.
5958
6580
  * 메서드는 클래스 본문의 일부라 어느 목표에서도 그런 일이 없다 — 실제로
5959
6581
  * 겪은 문제이고, `test/bundle.test.ts` 가 이걸 회귀로 잡지는 않으니 여기
5960
6582
  * 적어 둔다.
@@ -6130,6 +6752,7 @@ var LINE_DASH2 = /* @__PURE__ */ Object.freeze({
6130
6752
  MONTH_LABELS_EN,
6131
6753
  MONTH_LABELS_NUM,
6132
6754
  autoRange,
6755
+ createAdAsData,
6133
6756
  createDefaultAbsBarData,
6134
6757
  createDefaultCategoryDotData,
6135
6758
  createDefaultClimateData,
@@ -6137,6 +6760,7 @@ var LINE_DASH2 = /* @__PURE__ */ Object.freeze({
6137
6760
  createDefaultDataTableData,
6138
6761
  createDefaultDeviationAData,
6139
6762
  createDefaultDeviationBData,
6763
+ createDefaultEconPlaneData,
6140
6764
  createDefaultGraphOptions,
6141
6765
  createDefaultHythergraphData,
6142
6766
  createDefaultLineData,
@@ -6147,6 +6771,8 @@ var LINE_DASH2 = /* @__PURE__ */ Object.freeze({
6147
6771
  createDefaultStackedData,
6148
6772
  createDefaultTernaryData,
6149
6773
  createDefaultTreemapData,
6774
+ createPointShiftData,
6775
+ createSupplyDemandData,
6150
6776
  ensureFonts,
6151
6777
  installRoundRectPolyfill,
6152
6778
  isCsatChartType,
@@ -6158,6 +6784,7 @@ var LINE_DASH2 = /* @__PURE__ */ Object.freeze({
6158
6784
  renderDataTable,
6159
6785
  renderDeviationAGraph,
6160
6786
  renderDeviationBGraph,
6787
+ renderEconPlane,
6161
6788
  renderHythergraph,
6162
6789
  renderLineGraph,
6163
6790
  renderMatrixTable,