@wil-works/evoke-charts 0.3.0 → 0.3.1
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/README.md +2 -0
- package/dist/evoke-charts.css +31 -31
- package/dist/index.mjs +390 -208
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Evoke Charts
|
|
2
2
|
|
|
3
|
+
> **⚠️ 本包正在开发迭代中,尚未发布正式版:API、图表类型与视觉细节可能随版本调整,请勿用于生产环境。**
|
|
4
|
+
|
|
3
5
|
`@wil-works/evoke-charts` — 零依赖 Canvas 自绘图表库(Vue 3)。Evoke 生态的独立图表包,不依赖任何第三方图表引擎,也不依赖 Evoke 其他组件库。
|
|
4
6
|
|
|
5
7
|
**在线文档**:[evoke-charts.wil-works.com](https://evoke-charts.wil-works.com) — 全部图表类型、完整配置项与在线示例。
|
package/dist/evoke-charts.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
.ev-chart[data-v-
|
|
2
|
+
.ev-chart[data-v-0c5d944e] {
|
|
3
3
|
position: relative;
|
|
4
4
|
/* 容器不带边框:是否加框、加多粗由宿主决定,图表只负责内容;
|
|
5
5
|
圆角裁剪下沉到 canvas 与浮层自身(border-radius: inherit) */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/* 关键:canvas 必须能接收鼠标事件 */
|
|
12
|
-
.ev-chart__canvas[data-v-
|
|
12
|
+
.ev-chart__canvas[data-v-0c5d944e] {
|
|
13
13
|
position: absolute;
|
|
14
14
|
top: 0;
|
|
15
15
|
left: 0;
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
|
|
24
24
|
/* HTML 覆盖层:只在内容层,不与 canvas 抢交互(子元素可自行 pointer-events:auto),
|
|
25
25
|
也不压过 tooltip(z-index 100) */
|
|
26
|
-
.ev-chart__overlay[data-v-
|
|
26
|
+
.ev-chart__overlay[data-v-0c5d944e] {
|
|
27
27
|
position: absolute;
|
|
28
28
|
inset: 0;
|
|
29
29
|
z-index: 1;
|
|
30
30
|
pointer-events: none;
|
|
31
31
|
border-radius: inherit;
|
|
32
32
|
}
|
|
33
|
-
.ev-chart__tooltip[data-v-
|
|
33
|
+
.ev-chart__tooltip[data-v-0c5d944e] {
|
|
34
34
|
position: absolute;
|
|
35
35
|
pointer-events: none;
|
|
36
36
|
z-index: 100;
|
|
@@ -51,47 +51,47 @@
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/* tooltip 淡入淡出(仅 opacity,位置由 left/top transition 负责) */
|
|
54
|
-
.ev-chart-tooltip-enter-active[data-v-
|
|
55
|
-
.ev-chart-tooltip-leave-active[data-v-
|
|
54
|
+
.ev-chart-tooltip-enter-active[data-v-0c5d944e],
|
|
55
|
+
.ev-chart-tooltip-leave-active[data-v-0c5d944e] {
|
|
56
56
|
transition: opacity 0.18s ease;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/* 进入期间禁用位置 transition,避免首次出现时从旧位置滑入 */
|
|
60
|
-
.ev-chart-tooltip-enter-active[data-v-
|
|
60
|
+
.ev-chart-tooltip-enter-active[data-v-0c5d944e] {
|
|
61
61
|
transition:
|
|
62
62
|
opacity 0.18s ease,
|
|
63
63
|
left 0s,
|
|
64
64
|
top 0s;
|
|
65
65
|
}
|
|
66
|
-
.ev-chart-tooltip-enter-from[data-v-
|
|
67
|
-
.ev-chart-tooltip-leave-to[data-v-
|
|
66
|
+
.ev-chart-tooltip-enter-from[data-v-0c5d944e],
|
|
67
|
+
.ev-chart-tooltip-leave-to[data-v-0c5d944e] {
|
|
68
68
|
opacity: 0;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
/* v-html 注入的内容不带 scoped 属性,需用 :deep() 穿透 */
|
|
72
|
-
[data-v-
|
|
72
|
+
[data-v-0c5d944e] .ev-chart__tooltip-title {
|
|
73
73
|
color: var(--ev-text-color-secondary, #6b7280);
|
|
74
74
|
font-size: 12px;
|
|
75
75
|
margin-bottom: 6px;
|
|
76
76
|
font-weight: 500;
|
|
77
77
|
}
|
|
78
|
-
[data-v-
|
|
78
|
+
[data-v-0c5d944e] .ev-chart__tooltip-item {
|
|
79
79
|
display: flex;
|
|
80
80
|
align-items: center;
|
|
81
81
|
gap: 8px;
|
|
82
82
|
padding: 2px 0;
|
|
83
83
|
}
|
|
84
|
-
[data-v-
|
|
84
|
+
[data-v-0c5d944e] .ev-chart__tooltip-dot {
|
|
85
85
|
width: 8px;
|
|
86
86
|
height: 8px;
|
|
87
87
|
border-radius: 50%;
|
|
88
88
|
flex-shrink: 0;
|
|
89
89
|
display: inline-block;
|
|
90
90
|
}
|
|
91
|
-
[data-v-
|
|
91
|
+
[data-v-0c5d944e] .ev-chart__tooltip-name {
|
|
92
92
|
color: var(--ev-text-color-primary, #111827);
|
|
93
93
|
}
|
|
94
|
-
[data-v-
|
|
94
|
+
[data-v-0c5d944e] .ev-chart__tooltip-value {
|
|
95
95
|
color: var(--ev-text-color-primary, #111827);
|
|
96
96
|
font-weight: 600;
|
|
97
97
|
margin-left: auto;
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/* ══════ 空数据占位 ══════ */
|
|
103
|
-
.ev-chart__empty[data-v-
|
|
103
|
+
.ev-chart__empty[data-v-0c5d944e] {
|
|
104
104
|
position: absolute;
|
|
105
105
|
inset: 0;
|
|
106
106
|
border-radius: inherit;
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
gap: 14px;
|
|
114
114
|
user-select: none;
|
|
115
115
|
}
|
|
116
|
-
.ev-chart__empty-icon-wrap[data-v-
|
|
116
|
+
.ev-chart__empty-icon-wrap[data-v-0c5d944e] {
|
|
117
117
|
display: flex;
|
|
118
118
|
align-items: center;
|
|
119
119
|
justify-content: center;
|
|
@@ -122,19 +122,19 @@
|
|
|
122
122
|
border-radius: 16px;
|
|
123
123
|
background: var(--ev-fill-color-light, #f5f7fa);
|
|
124
124
|
}
|
|
125
|
-
.ev-chart__empty-img[data-v-
|
|
125
|
+
.ev-chart__empty-img[data-v-0c5d944e] {
|
|
126
126
|
width: 32px;
|
|
127
127
|
height: 32px;
|
|
128
128
|
opacity: 0.4;
|
|
129
129
|
}
|
|
130
|
-
.ev-chart__empty-text[data-v-
|
|
130
|
+
.ev-chart__empty-text[data-v-0c5d944e] {
|
|
131
131
|
letter-spacing: 0.04em;
|
|
132
132
|
font-weight: 500;
|
|
133
133
|
font-size: 13px;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/* ══════ 加载中骨架 ══════ */
|
|
137
|
-
.ev-chart__loading[data-v-
|
|
137
|
+
.ev-chart__loading[data-v-0c5d944e] {
|
|
138
138
|
position: absolute;
|
|
139
139
|
inset: 0;
|
|
140
140
|
border-radius: inherit;
|
|
@@ -143,20 +143,20 @@
|
|
|
143
143
|
justify-content: center;
|
|
144
144
|
background: var(--ev-bg-color, #ffffff);
|
|
145
145
|
}
|
|
146
|
-
.ev-chart__loading-bars[data-v-
|
|
146
|
+
.ev-chart__loading-bars[data-v-0c5d944e] {
|
|
147
147
|
display: flex;
|
|
148
148
|
align-items: flex-end;
|
|
149
149
|
gap: 6px;
|
|
150
150
|
height: 60px;
|
|
151
151
|
}
|
|
152
|
-
.ev-chart__loading-bar[data-v-
|
|
152
|
+
.ev-chart__loading-bar[data-v-0c5d944e] {
|
|
153
153
|
width: 10px;
|
|
154
154
|
height: 40px;
|
|
155
155
|
border-radius: 4px;
|
|
156
156
|
background: var(--ev-border-color, #e2e8f0);
|
|
157
|
-
animation: ev-chart-shimmer-
|
|
157
|
+
animation: ev-chart-shimmer-0c5d944e 1.2s ease-in-out infinite;
|
|
158
158
|
}
|
|
159
|
-
@keyframes ev-chart-shimmer-
|
|
159
|
+
@keyframes ev-chart-shimmer-0c5d944e {
|
|
160
160
|
0%,
|
|
161
161
|
100% {
|
|
162
162
|
transform: scaleY(0.4);
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
/* ══════ 错误边界 ══════ */
|
|
172
|
-
.ev-chart__error[data-v-
|
|
172
|
+
.ev-chart__error[data-v-0c5d944e] {
|
|
173
173
|
position: absolute;
|
|
174
174
|
inset: 0;
|
|
175
175
|
border-radius: inherit;
|
|
@@ -183,13 +183,13 @@
|
|
|
183
183
|
padding: 20px;
|
|
184
184
|
text-align: center;
|
|
185
185
|
}
|
|
186
|
-
.ev-chart__error-icon[data-v-
|
|
186
|
+
.ev-chart__error-icon[data-v-0c5d944e] {
|
|
187
187
|
display: flex;
|
|
188
188
|
align-items: center;
|
|
189
189
|
justify-content: center;
|
|
190
190
|
font-size: 30px;
|
|
191
191
|
}
|
|
192
|
-
.ev-chart__error-text[data-v-
|
|
192
|
+
.ev-chart__error-text[data-v-0c5d944e] {
|
|
193
193
|
font-size: 12px;
|
|
194
194
|
line-height: 1.5;
|
|
195
195
|
max-width: 80%;
|
|
@@ -197,17 +197,17 @@
|
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/* ══════ 进入/离开淡入淡出 ══════ */
|
|
200
|
-
.ev-chart-fade-enter-active[data-v-
|
|
201
|
-
.ev-chart-fade-leave-active[data-v-
|
|
200
|
+
.ev-chart-fade-enter-active[data-v-0c5d944e],
|
|
201
|
+
.ev-chart-fade-leave-active[data-v-0c5d944e] {
|
|
202
202
|
transition: opacity 0.2s ease;
|
|
203
203
|
}
|
|
204
|
-
.ev-chart-fade-enter-from[data-v-
|
|
205
|
-
.ev-chart-fade-leave-to[data-v-
|
|
204
|
+
.ev-chart-fade-enter-from[data-v-0c5d944e],
|
|
205
|
+
.ev-chart-fade-leave-to[data-v-0c5d944e] {
|
|
206
206
|
opacity: 0;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
/* ══════ 无障碍:sr-only ══════ */
|
|
210
|
-
.ev-chart__sr-only[data-v-
|
|
210
|
+
.ev-chart__sr-only[data-v-0c5d944e] {
|
|
211
211
|
position: absolute;
|
|
212
212
|
width: 1px;
|
|
213
213
|
height: 1px;
|
package/dist/index.mjs
CHANGED
|
@@ -121,19 +121,62 @@ function estimateTextWidth(text, fontSize = 12) {
|
|
|
121
121
|
}
|
|
122
122
|
return width;
|
|
123
123
|
}
|
|
124
|
+
/** 解析 `#rgb` / `#rrggbb` / `rgb()` / `rgba()` 为通道数组;无法识别返回 null */
|
|
125
|
+
function parseColorChannels(color) {
|
|
126
|
+
if (typeof color !== "string") return null;
|
|
127
|
+
const text = color.trim();
|
|
128
|
+
const hex = text.replace(/^#/, "");
|
|
129
|
+
if (/^[0-9a-fA-F]{3}$/.test(hex)) return [
|
|
130
|
+
0,
|
|
131
|
+
1,
|
|
132
|
+
2
|
|
133
|
+
].map((i) => parseInt(hex[i] + hex[i], 16));
|
|
134
|
+
if (/^[0-9a-fA-F]{6}$/.test(hex)) return [
|
|
135
|
+
0,
|
|
136
|
+
2,
|
|
137
|
+
4
|
|
138
|
+
].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
|
139
|
+
const rgbMatch = text.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);
|
|
140
|
+
if (rgbMatch) return [
|
|
141
|
+
1,
|
|
142
|
+
2,
|
|
143
|
+
3
|
|
144
|
+
].map((i) => Math.round(parseFloat(rgbMatch[i])));
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
/** 浅色底判断(YIQ):底色可能来自令牌、混合出的 `rgb()` 或自定义主题 */
|
|
148
|
+
function isLightColor(color) {
|
|
149
|
+
const ch = parseColorChannels(color);
|
|
150
|
+
if (!ch) return true;
|
|
151
|
+
return (ch[0] * 299 + ch[1] * 587 + ch[2] * 114) / 1e3 >= 150;
|
|
152
|
+
}
|
|
153
|
+
function relativeLuminance(ch) {
|
|
154
|
+
const [r, g, b] = ch.map((v) => {
|
|
155
|
+
const s = v / 255;
|
|
156
|
+
return s <= .03928 ? s / 12.92 : Math.pow((s + .055) / 1.055, 2.4);
|
|
157
|
+
});
|
|
158
|
+
return .2126 * r + .7152 * g + .0722 * b;
|
|
159
|
+
}
|
|
160
|
+
/** 底色上的文字取反色:白字/深字谁对比度高用谁——中间调底色(如逐层提亮后的蓝)
|
|
161
|
+
* 用亮度阈值会误判成白字,导致浅底白字看不清 */
|
|
124
162
|
function getContrastText(bgColor) {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
163
|
+
const ch = parseColorChannels(bgColor);
|
|
164
|
+
if (!ch) return "#111827";
|
|
165
|
+
const luminance = relativeLuminance(ch);
|
|
166
|
+
const againstWhite = 1.05 / (luminance + .05);
|
|
167
|
+
return (luminance + .05) / (relativeLuminance([
|
|
168
|
+
17,
|
|
169
|
+
24,
|
|
170
|
+
39
|
|
171
|
+
]) + .05) >= againstWhite ? "#111827" : "#ffffff";
|
|
172
|
+
}
|
|
173
|
+
/** 颜色向 target 混合(ratio 0-1),返回 `#rrggbb`:同色系逐层提亮/压暗用 */
|
|
174
|
+
function mixColor(color, ratio, target) {
|
|
175
|
+
const from = parseColorChannels(color);
|
|
176
|
+
const to = parseColorChannels(target);
|
|
177
|
+
if (!from || !to) return color;
|
|
178
|
+
const r = Math.max(0, Math.min(1, ratio));
|
|
179
|
+
return `#${from.map((v, i) => Math.round(v + (to[i] - v) * r)).map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
137
180
|
}
|
|
138
181
|
function isMissingValue(v) {
|
|
139
182
|
return v === null || v === void 0 || Number.isNaN(v);
|
|
@@ -1493,6 +1536,12 @@ function collectLegendItems(options, theme, hiddenSeries) {
|
|
|
1493
1536
|
color: d.color || theme.colors[i % theme.colors.length],
|
|
1494
1537
|
hidden: hiddenSeries.has(d.label || "")
|
|
1495
1538
|
}));
|
|
1539
|
+
if (options.type === "scatter") return (options.scatterData || []).map((d, i) => ({
|
|
1540
|
+
name: d.label || `P${i + 1}`,
|
|
1541
|
+
label: fmt(d.label || `P${i + 1}`),
|
|
1542
|
+
color: d.color || theme.colors[i % theme.colors.length],
|
|
1543
|
+
hidden: hiddenSeries.has(d.label || `P${i + 1}`)
|
|
1544
|
+
}));
|
|
1496
1545
|
return (options.series || []).map((s, i) => ({
|
|
1497
1546
|
name: s.name,
|
|
1498
1547
|
label: fmt(s.name),
|
|
@@ -1671,12 +1720,69 @@ function renderToolbox(ctx, hoveredFeature) {
|
|
|
1671
1720
|
canvasCtx.restore();
|
|
1672
1721
|
});
|
|
1673
1722
|
}
|
|
1723
|
+
/**
|
|
1724
|
+
* items: [{ angle, r, text }](angle 扇区中角,r 引线锚点半径)
|
|
1725
|
+
* cfg: { canvasCtx, centerX, centerY, plotArea, theme } + 可选覆写
|
|
1726
|
+
*/
|
|
1727
|
+
function drawCalloutLabels(items, cfg) {
|
|
1728
|
+
const { canvasCtx, centerX, centerY, plotArea, theme, radialLen = 14, stubLen = 12, textGap = 5, minGap = 16, maxShift = 10, font = "12px Inter, sans-serif", textColor = theme.textColor, lineColor = theme.textColorSecondary } = cfg;
|
|
1729
|
+
if (!canvasCtx || items.length === 0) return;
|
|
1730
|
+
const topLimit = plotArea.y + 4;
|
|
1731
|
+
const bottomLimit = plotArea.y + plotArea.height - 4;
|
|
1732
|
+
const layoutSide = (side, sign) => {
|
|
1733
|
+
const sorted = side.map((it) => ({
|
|
1734
|
+
...it,
|
|
1735
|
+
y: centerY + Math.sin(it.angle) * (it.r + radialLen),
|
|
1736
|
+
visible: true
|
|
1737
|
+
})).sort((a, b) => a.y - b.y);
|
|
1738
|
+
let lastY = -Infinity;
|
|
1739
|
+
sorted.forEach((it) => {
|
|
1740
|
+
let y = it.y;
|
|
1741
|
+
if (y < lastY + minGap) {
|
|
1742
|
+
const shifted = lastY + minGap;
|
|
1743
|
+
if (shifted - it.y <= maxShift && shifted <= bottomLimit) y = shifted;
|
|
1744
|
+
else {
|
|
1745
|
+
it.visible = false;
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
if (y > bottomLimit || y < topLimit) {
|
|
1750
|
+
it.visible = false;
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
it.y = y;
|
|
1754
|
+
lastY = y;
|
|
1755
|
+
});
|
|
1756
|
+
sorted.forEach((it) => {
|
|
1757
|
+
if (!it.visible) return;
|
|
1758
|
+
const dirX = Math.cos(it.angle);
|
|
1759
|
+
const dirY = Math.sin(it.angle);
|
|
1760
|
+
const p1x = centerX + dirX * (it.r + radialLen);
|
|
1761
|
+
const p1y = centerY + dirY * (it.r + radialLen);
|
|
1762
|
+
const p2x = p1x + sign * stubLen;
|
|
1763
|
+
canvasCtx.save();
|
|
1764
|
+
canvasCtx.strokeStyle = lineColor;
|
|
1765
|
+
canvasCtx.lineWidth = 1;
|
|
1766
|
+
canvasCtx.beginPath();
|
|
1767
|
+
canvasCtx.moveTo(centerX + dirX * it.r, centerY + dirY * it.r);
|
|
1768
|
+
canvasCtx.lineTo(p1x, p1y);
|
|
1769
|
+
canvasCtx.lineTo(p2x, it.y);
|
|
1770
|
+
canvasCtx.stroke();
|
|
1771
|
+
canvasCtx.restore();
|
|
1772
|
+
canvasCtx.save();
|
|
1773
|
+
canvasCtx.fillStyle = textColor;
|
|
1774
|
+
canvasCtx.font = font;
|
|
1775
|
+
canvasCtx.textAlign = sign === 1 ? "left" : "right";
|
|
1776
|
+
canvasCtx.textBaseline = "middle";
|
|
1777
|
+
canvasCtx.fillText(it.text, p2x + sign * textGap, it.y);
|
|
1778
|
+
canvasCtx.restore();
|
|
1779
|
+
});
|
|
1780
|
+
};
|
|
1781
|
+
layoutSide(items.filter((it) => Math.cos(it.angle) >= 0), 1);
|
|
1782
|
+
layoutSide(items.filter((it) => Math.cos(it.angle) < 0), -1);
|
|
1783
|
+
}
|
|
1674
1784
|
//#endregion
|
|
1675
1785
|
//#region src/renderer/charts-basic.js
|
|
1676
|
-
var PIE_RADIAL_LEN = 14;
|
|
1677
|
-
var PIE_STUB_LEN = 12;
|
|
1678
|
-
var PIE_TEXT_GAP = 5;
|
|
1679
|
-
var PIE_MAX_SHIFT = 10;
|
|
1680
1786
|
function computePieMaxRadius(pieData, options, plotArea) {
|
|
1681
1787
|
const total = pieData.reduce((sum, d) => sum + d.value, 0);
|
|
1682
1788
|
if (total <= 0) return 30;
|
|
@@ -2295,70 +2401,21 @@ function renderPieChart(ctx, isDoughnut, isRose = false) {
|
|
|
2295
2401
|
if (progress > .9) {
|
|
2296
2402
|
const percentage = (data.value / total * 100).toFixed(1);
|
|
2297
2403
|
const text = percentMode ? `${percentage}%` : `${data.label || ""} (${percentage}%)`;
|
|
2298
|
-
const r = isRose ? roseRadius : maxRadius;
|
|
2299
2404
|
labelItems.push({
|
|
2300
2405
|
angle: midAngle,
|
|
2301
|
-
r,
|
|
2302
|
-
|
|
2303
|
-
text,
|
|
2304
|
-
y: centerY + Math.sin(midAngle) * (r + PIE_RADIAL_LEN),
|
|
2305
|
-
visible: true
|
|
2406
|
+
r: isRose ? roseRadius : maxRadius,
|
|
2407
|
+
text
|
|
2306
2408
|
});
|
|
2307
2409
|
}
|
|
2308
2410
|
startAngle = endAngle;
|
|
2309
2411
|
});
|
|
2310
|
-
if (labelItems.length > 0 && progress > .9) {
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
sorted.forEach((it) => {
|
|
2318
|
-
let y = it.y;
|
|
2319
|
-
if (y < lastY + minGap) {
|
|
2320
|
-
const shifted = lastY + minGap;
|
|
2321
|
-
if (shifted - it.y <= PIE_MAX_SHIFT && shifted <= bottomLimit) y = shifted;
|
|
2322
|
-
else {
|
|
2323
|
-
it.visible = false;
|
|
2324
|
-
return;
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
if (y > bottomLimit || y < topLimit) {
|
|
2328
|
-
it.visible = false;
|
|
2329
|
-
return;
|
|
2330
|
-
}
|
|
2331
|
-
it.y = y;
|
|
2332
|
-
lastY = y;
|
|
2333
|
-
});
|
|
2334
|
-
sorted.forEach((it) => {
|
|
2335
|
-
if (!it.visible) return;
|
|
2336
|
-
const dirX = Math.cos(it.angle);
|
|
2337
|
-
const dirY = Math.sin(it.angle);
|
|
2338
|
-
const p1x = centerX + dirX * (it.r + PIE_RADIAL_LEN);
|
|
2339
|
-
const p1y = centerY + dirY * (it.r + PIE_RADIAL_LEN);
|
|
2340
|
-
const p2x = p1x + sign * PIE_STUB_LEN;
|
|
2341
|
-
canvasCtx.save();
|
|
2342
|
-
canvasCtx.strokeStyle = theme.textColorSecondary;
|
|
2343
|
-
canvasCtx.lineWidth = 1;
|
|
2344
|
-
canvasCtx.beginPath();
|
|
2345
|
-
canvasCtx.moveTo(centerX + dirX * it.r, centerY + dirY * it.r);
|
|
2346
|
-
canvasCtx.lineTo(p1x, p1y);
|
|
2347
|
-
canvasCtx.lineTo(p2x, it.y);
|
|
2348
|
-
canvasCtx.stroke();
|
|
2349
|
-
canvasCtx.restore();
|
|
2350
|
-
canvasCtx.save();
|
|
2351
|
-
canvasCtx.fillStyle = theme.textColor;
|
|
2352
|
-
canvasCtx.font = "12px Inter, sans-serif";
|
|
2353
|
-
canvasCtx.textAlign = sign === 1 ? "left" : "right";
|
|
2354
|
-
canvasCtx.textBaseline = "middle";
|
|
2355
|
-
canvasCtx.fillText(it.text, p2x + sign * PIE_TEXT_GAP, it.y);
|
|
2356
|
-
canvasCtx.restore();
|
|
2357
|
-
});
|
|
2358
|
-
};
|
|
2359
|
-
layoutSide(labelItems.filter((it) => Math.cos(it.angle) >= 0), 1);
|
|
2360
|
-
layoutSide(labelItems.filter((it) => Math.cos(it.angle) < 0), -1);
|
|
2361
|
-
}
|
|
2412
|
+
if (labelItems.length > 0 && progress > .9) drawCalloutLabels(labelItems, {
|
|
2413
|
+
canvasCtx,
|
|
2414
|
+
centerX,
|
|
2415
|
+
centerY,
|
|
2416
|
+
plotArea,
|
|
2417
|
+
theme
|
|
2418
|
+
});
|
|
2362
2419
|
if (isDoughnut && options.gauge?.value !== void 0) {
|
|
2363
2420
|
canvasCtx.save();
|
|
2364
2421
|
canvasCtx.fillStyle = theme.textColor;
|
|
@@ -2401,6 +2458,10 @@ function renderScatterChart(ctx, yRange) {
|
|
|
2401
2458
|
const points = [];
|
|
2402
2459
|
const yFactor = Math.min(ctx.progress * 1.2, 1);
|
|
2403
2460
|
scatterData.forEach((point, i) => {
|
|
2461
|
+
if (ctx.hiddenSeries.has(point.label || `P${i + 1}`)) {
|
|
2462
|
+
points.push([NaN, NaN]);
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2404
2465
|
const x = scale.toX(point.x);
|
|
2405
2466
|
const finalY = scale.toY(point.y);
|
|
2406
2467
|
const animatedY = plotArea.y + plotArea.height - (plotArea.y + plotArea.height - finalY) * yFactor;
|
|
@@ -2553,6 +2614,20 @@ function renderRadarChart(ctx) {
|
|
|
2553
2614
|
canvasCtx.stroke();
|
|
2554
2615
|
}
|
|
2555
2616
|
canvasCtx.restore();
|
|
2617
|
+
const sharedMax = indicators[0].max;
|
|
2618
|
+
const sharedMin = indicators[0].min || 0;
|
|
2619
|
+
if (sharedMax != null && indicators.every((ind) => (ind.max ?? sharedMax) === sharedMax && (ind.min || 0) === sharedMin)) {
|
|
2620
|
+
canvasCtx.save();
|
|
2621
|
+
canvasCtx.fillStyle = theme.textColorSecondary;
|
|
2622
|
+
canvasCtx.font = "10px Inter, sans-serif";
|
|
2623
|
+
canvasCtx.textAlign = "left";
|
|
2624
|
+
canvasCtx.textBaseline = "middle";
|
|
2625
|
+
for (let level = 1; level <= 5; level++) {
|
|
2626
|
+
const v = sharedMin + (sharedMax - sharedMin) * level / 5;
|
|
2627
|
+
canvasCtx.fillText(Number.isInteger(sharedMax) ? String(Math.round(v)) : v.toFixed(1), centerX + 5, centerY - radius * level / 5);
|
|
2628
|
+
}
|
|
2629
|
+
canvasCtx.restore();
|
|
2630
|
+
}
|
|
2556
2631
|
canvasCtx.save();
|
|
2557
2632
|
canvasCtx.fillStyle = theme.textColorSecondary;
|
|
2558
2633
|
canvasCtx.font = "12px Inter, sans-serif";
|
|
@@ -2606,7 +2681,15 @@ function renderRadarChart(ctx) {
|
|
|
2606
2681
|
else canvasCtx.lineTo(p[0], p[1]);
|
|
2607
2682
|
});
|
|
2608
2683
|
canvasCtx.closePath();
|
|
2609
|
-
|
|
2684
|
+
if (/^#[0-9a-fA-F]{6}$/.test(color)) {
|
|
2685
|
+
const grad = canvasCtx.createLinearGradient(0, centerY - radius, 0, centerY + radius);
|
|
2686
|
+
grad.addColorStop(0, color + (hoverIndex >= 0 ? "59" : "40"));
|
|
2687
|
+
grad.addColorStop(1, color + "08");
|
|
2688
|
+
canvasCtx.fillStyle = grad;
|
|
2689
|
+
} else {
|
|
2690
|
+
canvasCtx.fillStyle = color;
|
|
2691
|
+
canvasCtx.globalAlpha = alpha * .18;
|
|
2692
|
+
}
|
|
2610
2693
|
canvasCtx.fill();
|
|
2611
2694
|
canvasCtx.restore();
|
|
2612
2695
|
}
|
|
@@ -2619,15 +2702,17 @@ function renderRadarChart(ctx) {
|
|
|
2619
2702
|
});
|
|
2620
2703
|
canvasCtx.closePath();
|
|
2621
2704
|
canvasCtx.strokeStyle = color;
|
|
2622
|
-
canvasCtx.lineWidth = hoverIndex >= 0 ? 2.5 :
|
|
2705
|
+
canvasCtx.lineWidth = hoverIndex >= 0 ? 2.5 : 1.5;
|
|
2623
2706
|
canvasCtx.stroke();
|
|
2624
2707
|
canvasCtx.restore();
|
|
2625
2708
|
canvasCtx.save();
|
|
2626
2709
|
canvasCtx.globalAlpha = alpha;
|
|
2710
|
+
const showSymbol = s.showSymbol === true;
|
|
2627
2711
|
points.forEach((p, i) => {
|
|
2628
2712
|
const isHover = i === hoverIndex;
|
|
2713
|
+
if (!showSymbol && !isHover) return;
|
|
2629
2714
|
canvasCtx.beginPath();
|
|
2630
|
-
canvasCtx.arc(p[0], p[1], isHover ? 6 :
|
|
2715
|
+
canvasCtx.arc(p[0], p[1], isHover ? 6 : 3, 0, Math.PI * 2);
|
|
2631
2716
|
canvasCtx.fillStyle = isHover ? theme.backgroundColor : color;
|
|
2632
2717
|
canvasCtx.fill();
|
|
2633
2718
|
canvasCtx.strokeStyle = color;
|
|
@@ -2662,11 +2747,14 @@ function renderFunnelChart(ctx) {
|
|
|
2662
2747
|
const stepHeight = (plotArea.y + plotArea.height - 8 - topY) / funnelData.length;
|
|
2663
2748
|
const maxValue = Math.max(...funnelData.map((d) => d.value));
|
|
2664
2749
|
const drawData = options.pyramid === true ? [...funnelData].reverse() : funnelData;
|
|
2750
|
+
const labelAlpha = Math.min(1, Math.max(0, (progress - .72) / .28));
|
|
2751
|
+
const outsideLabels = [];
|
|
2665
2752
|
drawData.forEach((data, i) => {
|
|
2666
2753
|
const color = data.color || theme.colors[funnelData.indexOf(data) % theme.colors.length];
|
|
2667
2754
|
const isHover = i === hoverIndex;
|
|
2668
|
-
const
|
|
2669
|
-
const
|
|
2755
|
+
const easedWidth = 1 - Math.pow(1 - progress, 3);
|
|
2756
|
+
const currentWidth = data.value / maxValue * maxWidth * easedWidth;
|
|
2757
|
+
const nextWidth = (drawData[i + 1]?.value ?? data.value * .5) / maxValue * maxWidth * easedWidth;
|
|
2670
2758
|
const y = topY + i * stepHeight;
|
|
2671
2759
|
const yOffset = isHover ? -3 : 0;
|
|
2672
2760
|
canvasCtx.save();
|
|
@@ -2681,48 +2769,67 @@ function renderFunnelChart(ctx) {
|
|
|
2681
2769
|
canvasCtx.strokeStyle = theme.backgroundColor;
|
|
2682
2770
|
canvasCtx.lineWidth = 2;
|
|
2683
2771
|
canvasCtx.stroke();
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2772
|
+
canvasCtx.restore();
|
|
2773
|
+
if (labelAlpha <= 0) return;
|
|
2774
|
+
const labelY = y + stepHeight / 2 + yOffset;
|
|
2775
|
+
const percentage = (data.value / drawData[0].value * 100).toFixed(1);
|
|
2776
|
+
const label = `${data.label}`;
|
|
2777
|
+
const valueText = `${data.value} (${percentage}%)`;
|
|
2778
|
+
canvasCtx.save();
|
|
2779
|
+
canvasCtx.font = "bold 13px Inter, sans-serif";
|
|
2780
|
+
const labelWidth = canvasCtx.measureText(label).width;
|
|
2781
|
+
canvasCtx.font = "11px Inter, sans-serif";
|
|
2782
|
+
const valueWidth = canvasCtx.measureText(valueText).width;
|
|
2783
|
+
const minTextWidth = Math.max(labelWidth, valueWidth) + 28;
|
|
2784
|
+
if ((currentWidth + nextWidth) / 2 >= minTextWidth && stepHeight >= 36) {
|
|
2785
|
+
canvasCtx.globalAlpha = labelAlpha;
|
|
2786
|
+
canvasCtx.fillStyle = theme.backgroundColor;
|
|
2689
2787
|
canvasCtx.font = "bold 13px Inter, sans-serif";
|
|
2690
|
-
|
|
2788
|
+
canvasCtx.textAlign = "center";
|
|
2789
|
+
canvasCtx.textBaseline = "middle";
|
|
2790
|
+
canvasCtx.fillText(label, centerX, labelY - 9);
|
|
2691
2791
|
canvasCtx.font = "11px Inter, sans-serif";
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2792
|
+
canvasCtx.fillText(valueText, centerX, labelY + 9);
|
|
2793
|
+
} else outsideLabels.push({
|
|
2794
|
+
labelY,
|
|
2795
|
+
lineEndX: centerX + currentWidth / 2,
|
|
2796
|
+
label,
|
|
2797
|
+
valueText,
|
|
2798
|
+
labelTextWidth: Math.max(labelWidth, valueWidth)
|
|
2799
|
+
});
|
|
2800
|
+
canvasCtx.restore();
|
|
2801
|
+
});
|
|
2802
|
+
outsideLabels.sort((a, b) => a.labelY - b.labelY);
|
|
2803
|
+
const textX = plotArea.x + plotArea.width - 4;
|
|
2804
|
+
let lastBottom = -Infinity;
|
|
2805
|
+
outsideLabels.map((o) => {
|
|
2806
|
+
const top = Math.max(o.labelY - 13, lastBottom + 2);
|
|
2807
|
+
lastBottom = top + 36;
|
|
2808
|
+
return {
|
|
2809
|
+
...o,
|
|
2810
|
+
top
|
|
2811
|
+
};
|
|
2812
|
+
}).forEach((o) => {
|
|
2813
|
+
canvasCtx.save();
|
|
2814
|
+
canvasCtx.globalAlpha = labelAlpha;
|
|
2815
|
+
canvasCtx.strokeStyle = theme.textColorSecondary;
|
|
2816
|
+
canvasCtx.lineWidth = 1;
|
|
2817
|
+
canvasCtx.setLineDash([3, 3]);
|
|
2818
|
+
canvasCtx.beginPath();
|
|
2819
|
+
canvasCtx.moveTo(o.lineEndX, o.labelY);
|
|
2820
|
+
canvasCtx.lineTo(textX - o.labelTextWidth - 10, o.top + 12);
|
|
2821
|
+
canvasCtx.stroke();
|
|
2822
|
+
canvasCtx.setLineDash([]);
|
|
2823
|
+
const nameY = o.top + 12;
|
|
2824
|
+
canvasCtx.fillStyle = theme.textColor;
|
|
2825
|
+
canvasCtx.font = "bold 12px Inter, sans-serif";
|
|
2826
|
+
canvasCtx.textAlign = "right";
|
|
2827
|
+
canvasCtx.textBaseline = "bottom";
|
|
2828
|
+
canvasCtx.fillText(o.label, textX, nameY);
|
|
2829
|
+
canvasCtx.font = "11px Inter, sans-serif";
|
|
2830
|
+
canvasCtx.fillStyle = theme.textColorSecondary;
|
|
2831
|
+
canvasCtx.textBaseline = "top";
|
|
2832
|
+
canvasCtx.fillText(o.valueText, textX, nameY + 2);
|
|
2726
2833
|
canvasCtx.restore();
|
|
2727
2834
|
});
|
|
2728
2835
|
}
|
|
@@ -3206,42 +3313,40 @@ function squarifyTreemap(nodes, x, y, w, h, depth, result) {
|
|
|
3206
3313
|
});
|
|
3207
3314
|
return result;
|
|
3208
3315
|
}
|
|
3316
|
+
function isRectWithinFocus(rect, focus) {
|
|
3317
|
+
return rect.depth >= focus.depth && rect.x >= focus.x - .5 && rect.y >= focus.y - .5 && rect.x + rect.w <= focus.x + focus.w + .5 && rect.y + rect.h <= focus.y + focus.h + .5;
|
|
3318
|
+
}
|
|
3209
3319
|
function renderTreemapChart(ctx) {
|
|
3210
|
-
const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, valueFormatter } = ctx;
|
|
3320
|
+
const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, valueFormatter, hoverAnimProgress = 1 } = ctx;
|
|
3211
3321
|
const rootData = options.treemapData || [];
|
|
3212
3322
|
if (rootData.length === 0) return;
|
|
3213
3323
|
const rects = [];
|
|
3214
3324
|
squarifyTreemap(rootData, plotArea.x, plotArea.y, plotArea.width, plotArea.height, 0, rects);
|
|
3215
3325
|
const totalValue = rootData.reduce((s, n) => s + treemapValueOf(n), 0);
|
|
3216
3326
|
const scale = progress;
|
|
3217
|
-
|
|
3327
|
+
const focus = hoverIndex >= 0 && hoverIndex < rects.length ? rects[hoverIndex] : null;
|
|
3328
|
+
const dimAlpha = 1 - .75 * hoverAnimProgress;
|
|
3329
|
+
function drawRect(rect, isFocusRoot) {
|
|
3218
3330
|
const cx = rect.x + rect.w / 2;
|
|
3219
3331
|
const cy = rect.y + rect.h / 2;
|
|
3220
3332
|
const aw = rect.w * scale;
|
|
3221
3333
|
const ah = rect.h * scale;
|
|
3222
3334
|
const ax = cx - aw / 2;
|
|
3223
3335
|
const ay = cy - ah / 2;
|
|
3224
|
-
const
|
|
3336
|
+
const base = rect.node.color || theme.colors[(rect.colorSeed + rect.depth) % theme.colors.length];
|
|
3337
|
+
const color = isFocusRoot ? mixColor(base, .08, "#000000") : base;
|
|
3225
3338
|
canvasCtx.save();
|
|
3226
|
-
canvasCtx.
|
|
3339
|
+
canvasCtx.globalAlpha = !focus || isRectWithinFocus(rect, focus) ? 1 : dimAlpha;
|
|
3340
|
+
canvasCtx.fillStyle = isFocusRoot ? color : color + (rect.depth === 0 ? "" : "cc");
|
|
3227
3341
|
roundRect(canvasCtx, ax, ay, Math.max(0, aw), Math.max(0, ah), 3);
|
|
3228
3342
|
canvasCtx.fill();
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
canvasCtx.stroke();
|
|
3233
|
-
} else {
|
|
3234
|
-
canvasCtx.shadowColor = color;
|
|
3235
|
-
canvasCtx.shadowBlur = 10;
|
|
3236
|
-
canvasCtx.strokeStyle = theme.textColor;
|
|
3237
|
-
canvasCtx.lineWidth = 2.5;
|
|
3238
|
-
canvasCtx.stroke();
|
|
3239
|
-
canvasCtx.shadowBlur = 0;
|
|
3240
|
-
canvasCtx.stroke();
|
|
3241
|
-
}
|
|
3343
|
+
canvasCtx.strokeStyle = theme.backgroundColor;
|
|
3344
|
+
canvasCtx.lineWidth = 2;
|
|
3345
|
+
canvasCtx.stroke();
|
|
3242
3346
|
canvasCtx.restore();
|
|
3243
3347
|
if (progress > .8 && aw > 50 && ah > 24) {
|
|
3244
3348
|
canvasCtx.save();
|
|
3349
|
+
canvasCtx.globalAlpha = !focus || isRectWithinFocus(rect, focus) ? 1 : dimAlpha;
|
|
3245
3350
|
canvasCtx.fillStyle = getContrastText(color);
|
|
3246
3351
|
if (rect.node.children && rect.node.children.length > 0) {
|
|
3247
3352
|
canvasCtx.font = "bold 11px Inter, sans-serif";
|
|
@@ -3262,10 +3367,7 @@ function renderTreemapChart(ctx) {
|
|
|
3262
3367
|
canvasCtx.restore();
|
|
3263
3368
|
}
|
|
3264
3369
|
}
|
|
3265
|
-
rects.forEach((rect
|
|
3266
|
-
if (i !== hoverIndex) drawRect(rect, false);
|
|
3267
|
-
});
|
|
3268
|
-
if (hoverIndex >= 0 && hoverIndex < rects.length) drawRect(rects[hoverIndex], true);
|
|
3370
|
+
rects.forEach((rect) => drawRect(rect, focus === rect));
|
|
3269
3371
|
}
|
|
3270
3372
|
function renderSparklineChart(ctx) {
|
|
3271
3373
|
const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex } = ctx;
|
|
@@ -3545,12 +3647,26 @@ function computeSunburstDepth(nodes) {
|
|
|
3545
3647
|
});
|
|
3546
3648
|
return max;
|
|
3547
3649
|
}
|
|
3548
|
-
|
|
3549
|
-
|
|
3650
|
+
var SUNBURST_HOLE_RATIO = .22;
|
|
3651
|
+
var SUNBURST_LIGHT_STEP = .26;
|
|
3652
|
+
var SUNBURST_LIGHT_MAX = .6;
|
|
3653
|
+
var SUNBURST_DARK_STEP = .14;
|
|
3654
|
+
var SUNBURST_DARK_MAX = .34;
|
|
3655
|
+
/** 父节点可省略 value(由子孙汇总);直接读 n.value 会得到 NaN,
|
|
3656
|
+
* 让后续兄弟分支的角度全部失效、只剩第一个分支可见 */
|
|
3657
|
+
function sunburstValue(node) {
|
|
3658
|
+
if (typeof node.value === "number" && Number.isFinite(node.value)) return node.value;
|
|
3659
|
+
if (node.children && node.children.length > 0) return node.children.reduce((s, c) => s + sunburstValue(c), 0);
|
|
3660
|
+
return 0;
|
|
3661
|
+
}
|
|
3662
|
+
function layoutSunburst(nodes, r0, ringWidth, depth, startAngle, colorOffset, result, sweep = Math.PI * 2, parentIndex = -1) {
|
|
3663
|
+
const total = nodes.reduce((s, n) => s + sunburstValue(n), 0);
|
|
3550
3664
|
if (total <= 0) return result;
|
|
3551
3665
|
let a = startAngle;
|
|
3552
3666
|
nodes.forEach((n, i) => {
|
|
3553
|
-
const
|
|
3667
|
+
const slot = depth === 0 ? (colorOffset + i) % 8 : colorOffset % 8;
|
|
3668
|
+
const span = sweep * sunburstValue(n) / total;
|
|
3669
|
+
const selfIndex = result.length;
|
|
3554
3670
|
result.push({
|
|
3555
3671
|
startAngle: a,
|
|
3556
3672
|
endAngle: a + span,
|
|
@@ -3558,25 +3674,72 @@ function layoutSunburst(nodes, r0, ringWidth, depth, startAngle, colorOffset, re
|
|
|
3558
3674
|
r0,
|
|
3559
3675
|
r1: r0 + ringWidth,
|
|
3560
3676
|
node: n,
|
|
3677
|
+
value: sunburstValue(n),
|
|
3561
3678
|
depth,
|
|
3562
|
-
colorIndex:
|
|
3679
|
+
colorIndex: slot,
|
|
3680
|
+
parentIndex
|
|
3563
3681
|
});
|
|
3564
|
-
if (n.children && n.children.length > 0) layoutSunburst(n.children, r0 + ringWidth, ringWidth, depth + 1, a,
|
|
3682
|
+
if (n.children && n.children.length > 0) layoutSunburst(n.children, r0 + ringWidth, ringWidth, depth + 1, a, slot, result, span, selfIndex);
|
|
3565
3683
|
a += span;
|
|
3566
3684
|
});
|
|
3567
3685
|
return result;
|
|
3568
3686
|
}
|
|
3569
|
-
function
|
|
3570
|
-
|
|
3687
|
+
function isSunburstDescendant(segments, index, ancestorIndex) {
|
|
3688
|
+
let cursor = index;
|
|
3689
|
+
while (cursor >= 0) {
|
|
3690
|
+
if (cursor === ancestorIndex) return true;
|
|
3691
|
+
cursor = segments[cursor].parentIndex;
|
|
3692
|
+
}
|
|
3693
|
+
return false;
|
|
3694
|
+
}
|
|
3695
|
+
/** 环带颜色:同色系按深度向背景方向混合,外圈大面积不刺眼 */
|
|
3696
|
+
function sunburstDepthColor(base, depth, theme) {
|
|
3697
|
+
if (depth <= 0) return base;
|
|
3698
|
+
const darker = !isLightColor(theme.backgroundColor);
|
|
3699
|
+
return mixColor(base, Math.min(darker ? SUNBURST_DARK_MAX : SUNBURST_LIGHT_MAX, depth * (darker ? SUNBURST_DARK_STEP : SUNBURST_LIGHT_STEP)), darker ? "#000000" : "#ffffff");
|
|
3700
|
+
}
|
|
3701
|
+
function sunburstNodeColor(seg, theme) {
|
|
3702
|
+
const base = seg.node.color || theme.colors[seg.colorIndex % theme.colors.length];
|
|
3703
|
+
if (seg.node.color && seg.depth > 0) return seg.node.color;
|
|
3704
|
+
return sunburstDepthColor(base, seg.depth, theme);
|
|
3705
|
+
}
|
|
3706
|
+
/** 旭日图几何:渲染与悬浮命中共用一份口径(环厚、留白、半径预算) */
|
|
3707
|
+
function computeSunburstGeometry(plotArea, options, theme, valueFormatter) {
|
|
3571
3708
|
const data = options.sunburstData || [];
|
|
3572
|
-
|
|
3709
|
+
const depthCount = computeSunburstDepth(data);
|
|
3710
|
+
if (data.length === 0 || depthCount === 0) return null;
|
|
3573
3711
|
const centerX = plotArea.x + plotArea.width / 2;
|
|
3574
3712
|
const centerY = plotArea.y + plotArea.height / 2;
|
|
3575
|
-
const maxR = Math.max(40, Math.min(plotArea.width, plotArea.height) / 2 -
|
|
3576
|
-
const innerHole = Math.max(0, maxR *
|
|
3577
|
-
const
|
|
3578
|
-
const segments = layoutSunburst(data, innerHole,
|
|
3713
|
+
const maxR = Math.max(40, Math.min(plotArea.width, plotArea.height) / 2 - 20);
|
|
3714
|
+
const innerHole = Math.max(0, maxR * SUNBURST_HOLE_RATIO);
|
|
3715
|
+
const ringWidth = (maxR - innerHole) / depthCount;
|
|
3716
|
+
const segments = layoutSunburst(data, innerHole, ringWidth, 0, -Math.PI / 2, 0, []);
|
|
3717
|
+
segments.forEach((seg) => {
|
|
3718
|
+
seg.color = sunburstNodeColor(seg, theme);
|
|
3719
|
+
});
|
|
3720
|
+
return {
|
|
3721
|
+
centerX,
|
|
3722
|
+
centerY,
|
|
3723
|
+
maxR,
|
|
3724
|
+
innerHole,
|
|
3725
|
+
ringWidth,
|
|
3726
|
+
depthCount,
|
|
3727
|
+
segments,
|
|
3728
|
+
showValues: options.showValues === true,
|
|
3729
|
+
valueFormatter
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
function renderSunburstChart(ctx) {
|
|
3733
|
+
const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, valueFormatter, hoverAnimProgress = 1 } = ctx;
|
|
3734
|
+
const geo = computeSunburstGeometry(plotArea, options, theme, valueFormatter);
|
|
3735
|
+
if (!geo) return;
|
|
3736
|
+
const { centerX, centerY, maxR, ringWidth, depthCount, segments, showValues } = geo;
|
|
3579
3737
|
const sweep = -Math.PI / 2 + Math.PI * 2 * progress;
|
|
3738
|
+
const grow = .94 + .06 * progress;
|
|
3739
|
+
const focusIndex = hoverIndex >= 0 && hoverIndex < segments.length ? hoverIndex : -1;
|
|
3740
|
+
const dimAlpha = 1 - .75 * hoverAnimProgress;
|
|
3741
|
+
const alphaAt = (i) => focusIndex < 0 || isSunburstDescendant(segments, i, focusIndex) ? 1 : dimAlpha;
|
|
3742
|
+
const labels = [];
|
|
3580
3743
|
canvasCtx.save();
|
|
3581
3744
|
canvasCtx.beginPath();
|
|
3582
3745
|
canvasCtx.moveTo(centerX, centerY);
|
|
@@ -3584,45 +3747,58 @@ function renderSunburstChart(ctx) {
|
|
|
3584
3747
|
canvasCtx.closePath();
|
|
3585
3748
|
canvasCtx.clip();
|
|
3586
3749
|
segments.forEach((seg, i) => {
|
|
3587
|
-
const color = seg.
|
|
3588
|
-
const isHover = i === hoverIndex;
|
|
3750
|
+
const color = i === focusIndex ? mixColor(seg.color, .08, "#000000") : seg.color;
|
|
3589
3751
|
canvasCtx.save();
|
|
3752
|
+
canvasCtx.globalAlpha = alphaAt(i);
|
|
3590
3753
|
canvasCtx.beginPath();
|
|
3591
|
-
canvasCtx.arc(centerX, centerY, seg.r1, seg.startAngle, seg.endAngle);
|
|
3592
|
-
canvasCtx.arc(centerX, centerY, seg.r0, seg.endAngle, seg.startAngle, true);
|
|
3754
|
+
canvasCtx.arc(centerX, centerY, seg.r1 * grow, seg.startAngle, seg.endAngle);
|
|
3755
|
+
canvasCtx.arc(centerX, centerY, seg.r0 * grow, seg.endAngle, seg.startAngle, true);
|
|
3593
3756
|
canvasCtx.closePath();
|
|
3594
|
-
canvasCtx.fillStyle =
|
|
3757
|
+
canvasCtx.fillStyle = color;
|
|
3595
3758
|
canvasCtx.fill();
|
|
3596
3759
|
canvasCtx.strokeStyle = theme.backgroundColor;
|
|
3597
3760
|
canvasCtx.lineWidth = 2;
|
|
3598
3761
|
canvasCtx.stroke();
|
|
3599
|
-
if (isHover) {
|
|
3600
|
-
canvasCtx.strokeStyle = theme.textColor;
|
|
3601
|
-
canvasCtx.lineWidth = 2;
|
|
3602
|
-
canvasCtx.stroke();
|
|
3603
|
-
}
|
|
3604
3762
|
canvasCtx.restore();
|
|
3763
|
+
if (progress <= .9) return;
|
|
3764
|
+
const name = seg.node.name || "";
|
|
3765
|
+
if (!name) return;
|
|
3605
3766
|
const span = seg.endAngle - seg.startAngle;
|
|
3606
3767
|
const midR = (seg.r0 + seg.r1) / 2;
|
|
3607
3768
|
const chord = midR * span;
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
}
|
|
3622
|
-
canvasCtx.restore();
|
|
3623
|
-
}
|
|
3769
|
+
if (ringWidth < 12 || chord < 11) return;
|
|
3770
|
+
const value = showValues && ringWidth >= 26 ? valueFormatter(seg.value) : null;
|
|
3771
|
+
if (Math.max(estimateTextWidth(name, 11), value ? estimateTextWidth(value, 10) : 0) > ringWidth + 20) return;
|
|
3772
|
+
labels.push({
|
|
3773
|
+
x: centerX + Math.cos(seg.midAngle) * midR,
|
|
3774
|
+
y: centerY + Math.sin(seg.midAngle) * midR,
|
|
3775
|
+
midAngle: seg.midAngle,
|
|
3776
|
+
name,
|
|
3777
|
+
value,
|
|
3778
|
+
bold: seg.depth === 0,
|
|
3779
|
+
fill: getContrastText(color),
|
|
3780
|
+
alpha: alphaAt(i)
|
|
3781
|
+
});
|
|
3624
3782
|
});
|
|
3625
3783
|
canvasCtx.restore();
|
|
3784
|
+
labels.forEach((l) => {
|
|
3785
|
+
canvasCtx.save();
|
|
3786
|
+
canvasCtx.globalAlpha = l.alpha;
|
|
3787
|
+
canvasCtx.fillStyle = l.fill;
|
|
3788
|
+
canvasCtx.font = l.bold ? "bold 11px Inter, sans-serif" : "11px Inter, sans-serif";
|
|
3789
|
+
canvasCtx.textAlign = "center";
|
|
3790
|
+
canvasCtx.textBaseline = "middle";
|
|
3791
|
+
canvasCtx.translate(l.x, l.y);
|
|
3792
|
+
let rot = l.midAngle;
|
|
3793
|
+
if (rot > Math.PI / 2 || rot < -Math.PI / 2) rot += Math.PI;
|
|
3794
|
+
canvasCtx.rotate(rot);
|
|
3795
|
+
canvasCtx.fillText(l.name, 0, l.value ? -7 : 0);
|
|
3796
|
+
if (l.value) {
|
|
3797
|
+
canvasCtx.font = "10px Inter, sans-serif";
|
|
3798
|
+
canvasCtx.fillText(l.value, 0, 7);
|
|
3799
|
+
}
|
|
3800
|
+
canvasCtx.restore();
|
|
3801
|
+
});
|
|
3626
3802
|
}
|
|
3627
3803
|
function renderMixedChart(ctx, leftRange, rightRange) {
|
|
3628
3804
|
const { ctx: canvasCtx, theme, plotArea, options, progress, hoverIndex, hiddenSeries } = ctx;
|
|
@@ -4480,6 +4656,10 @@ var chartOptionsSchema = {
|
|
|
4480
4656
|
enum: ["left", "right"]
|
|
4481
4657
|
},
|
|
4482
4658
|
type: { type: "string" },
|
|
4659
|
+
chartType: {
|
|
4660
|
+
type: "string",
|
|
4661
|
+
enum: ["bar", "line"]
|
|
4662
|
+
},
|
|
4483
4663
|
area: { type: "boolean" }
|
|
4484
4664
|
}
|
|
4485
4665
|
}
|
|
@@ -5334,9 +5514,24 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5334
5514
|
hoverAnimFrameId = null;
|
|
5335
5515
|
}
|
|
5336
5516
|
}
|
|
5337
|
-
function
|
|
5517
|
+
function usesHoverAnimation() {
|
|
5518
|
+
const t = props.options.type;
|
|
5519
|
+
return t === "pie" || t === "doughnut" || t === "rose" || t === "sunburst" || t === "treemap";
|
|
5520
|
+
}
|
|
5521
|
+
function clearHover() {
|
|
5522
|
+
if (hoverIndex === -1) return false;
|
|
5523
|
+
hoverIndex = -1;
|
|
5338
5524
|
const t = props.options.type;
|
|
5339
|
-
if (t
|
|
5525
|
+
if (t === "pie" || t === "doughnut" || t === "rose") startHoverAnimation(-1);
|
|
5526
|
+
else {
|
|
5527
|
+
stopHoverAnimation();
|
|
5528
|
+
hoverAnimProgress = 0;
|
|
5529
|
+
redraw();
|
|
5530
|
+
}
|
|
5531
|
+
return true;
|
|
5532
|
+
}
|
|
5533
|
+
function startHoverAnimation(dir) {
|
|
5534
|
+
if (!usesHoverAnimation()) return;
|
|
5340
5535
|
if (props.options.animation?.enabled === false) {
|
|
5341
5536
|
hoverAnimProgress = dir > 0 ? 1 : 0;
|
|
5342
5537
|
redraw();
|
|
@@ -5377,7 +5572,8 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5377
5572
|
"doughnut",
|
|
5378
5573
|
"rose",
|
|
5379
5574
|
"radar",
|
|
5380
|
-
"funnel"
|
|
5575
|
+
"funnel",
|
|
5576
|
+
"scatter"
|
|
5381
5577
|
];
|
|
5382
5578
|
function checkLegendHit(x, y) {
|
|
5383
5579
|
const canvas = canvasRef.value;
|
|
@@ -5570,20 +5766,14 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5570
5766
|
};
|
|
5571
5767
|
}
|
|
5572
5768
|
if (options.type === "sunburst") {
|
|
5573
|
-
const
|
|
5574
|
-
if (
|
|
5575
|
-
const centerX
|
|
5576
|
-
const centerY = plotArea.y + plotArea.height / 2;
|
|
5577
|
-
const maxR = Math.max(40, Math.min(plotArea.width, plotArea.height) / 2 - 10);
|
|
5578
|
-
const innerHole = Math.max(0, maxR * .18);
|
|
5579
|
-
const depthCount = computeSunburstDepth(data);
|
|
5580
|
-
const ringWidth = (maxR - innerHole) / depthCount;
|
|
5769
|
+
const geo = computeSunburstGeometry(plotArea, options, theme, formatTooltipValue);
|
|
5770
|
+
if (!geo) return null;
|
|
5771
|
+
const { centerX, centerY, segments } = geo;
|
|
5581
5772
|
const dx = canvasX - centerX;
|
|
5582
5773
|
const dy = canvasY - centerY;
|
|
5583
5774
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
5584
5775
|
let hoverAngle = Math.atan2(dy, dx);
|
|
5585
5776
|
if (hoverAngle < -Math.PI / 2) hoverAngle += Math.PI * 2;
|
|
5586
|
-
const segments = layoutSunburst(data, innerHole, ringWidth, 0, -Math.PI / 2, 0, []);
|
|
5587
5777
|
for (let i = 0; i < segments.length; i++) {
|
|
5588
5778
|
const seg = segments[i];
|
|
5589
5779
|
if (distance >= seg.r0 && distance <= seg.r1 && hoverAngle >= seg.startAngle && hoverAngle < seg.endAngle) return {
|
|
@@ -5591,8 +5781,8 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
5591
5781
|
params: {
|
|
5592
5782
|
seriesName: seg.node.name,
|
|
5593
5783
|
name: seg.node.name,
|
|
5594
|
-
value: seg.node
|
|
5595
|
-
color: seg.
|
|
5784
|
+
value: sunburstValue(seg.node),
|
|
5785
|
+
color: seg.color,
|
|
5596
5786
|
dataIndex: i,
|
|
5597
5787
|
seriesIndex: 0
|
|
5598
5788
|
}
|
|
@@ -6211,12 +6401,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
6211
6401
|
}
|
|
6212
6402
|
}
|
|
6213
6403
|
if (legendName) {
|
|
6214
|
-
|
|
6215
|
-
hoverIndex = -1;
|
|
6216
|
-
const t = props.options.type;
|
|
6217
|
-
if (t === "pie" || t === "doughnut" || t === "rose") startHoverAnimation(-1);
|
|
6218
|
-
else redraw();
|
|
6219
|
-
}
|
|
6404
|
+
clearHover();
|
|
6220
6405
|
tooltipVisible.value = false;
|
|
6221
6406
|
return;
|
|
6222
6407
|
}
|
|
@@ -6235,6 +6420,10 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
6235
6420
|
hoverAnimProgress = .35;
|
|
6236
6421
|
startHoverAnimation(1);
|
|
6237
6422
|
}
|
|
6423
|
+
} else if (usesHoverAnimation()) {
|
|
6424
|
+
stopHoverAnimation();
|
|
6425
|
+
hoverAnimProgress = 1;
|
|
6426
|
+
redraw();
|
|
6238
6427
|
} else redraw();
|
|
6239
6428
|
if (prevIndex === -1) {
|
|
6240
6429
|
const emitParams = Array.isArray(result.params) ? result.params[0] : result.params;
|
|
@@ -6259,10 +6448,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
6259
6448
|
});
|
|
6260
6449
|
tooltipVisible.value = true;
|
|
6261
6450
|
} else {
|
|
6262
|
-
if (
|
|
6263
|
-
hoverIndex = -1;
|
|
6264
|
-
if (isPieLike) startHoverAnimation(-1);
|
|
6265
|
-
else redraw();
|
|
6451
|
+
if (clearHover()) {
|
|
6266
6452
|
emit("unhover");
|
|
6267
6453
|
ariaLiveText.value = "";
|
|
6268
6454
|
}
|
|
@@ -6344,11 +6530,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
6344
6530
|
redraw();
|
|
6345
6531
|
}
|
|
6346
6532
|
dragMode = "none";
|
|
6347
|
-
if (
|
|
6348
|
-
hoverIndex = -1;
|
|
6349
|
-
const t = props.options.type;
|
|
6350
|
-
if (t === "pie" || t === "doughnut" || t === "rose") startHoverAnimation(-1);
|
|
6351
|
-
else redraw();
|
|
6533
|
+
if (clearHover()) {
|
|
6352
6534
|
emit("unhover");
|
|
6353
6535
|
ariaLiveText.value = "";
|
|
6354
6536
|
}
|
|
@@ -6821,7 +7003,7 @@ var chart_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__
|
|
|
6821
7003
|
], 12, _hoisted_1);
|
|
6822
7004
|
};
|
|
6823
7005
|
}
|
|
6824
|
-
}), [["__scopeId", "data-v-
|
|
7006
|
+
}), [["__scopeId", "data-v-0c5d944e"]]);
|
|
6825
7007
|
//#endregion
|
|
6826
7008
|
//#region src/useChart.js
|
|
6827
7009
|
var USE_CHART_KEY = /* @__PURE__ */ Symbol("ev-chart-context");
|