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