@katlux/block-charts 0.1.1-alpha.87 → 0.1.1-alpha.89
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/dist/module.d.mts +3 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/KAreaChart/KAreaChart.vue +233 -60
- package/dist/runtime/components/KBarChart/KBarChart.vue +237 -114
- package/dist/runtime/components/KHeatMap/KHeatMap.vue +28 -15
- package/dist/runtime/components/KLineChart/KLineChart.vue +149 -41
- package/dist/runtime/components/KPieChart/KPieChart.vue +25 -26
- package/dist/runtime/components/KScatterChart/KScatterChart.vue +84 -32
- package/dist/runtime/composables/chartUtils.d.ts +8 -0
- package/dist/runtime/composables/chartUtils.js +58 -0
- package/dist/runtime/composables/useChartAxes.d.ts +3 -1
- package/dist/runtime/composables/useChartAxes.js +26 -9
- package/dist/runtime/composables/useChartData.d.ts +4 -3
- package/dist/runtime/composables/useChartData.js +19 -18
- package/dist/runtime/composables/useChartExport.d.ts +1 -1
- package/dist/runtime/composables/useChartExport.js +18 -2
- package/dist/runtime/composables/useChartHitTest.d.ts +1 -1
- package/dist/runtime/composables/useChartHitTest.js +4 -3
- package/package.json +1 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<template lang="pug">
|
|
2
2
|
.k-chart-wrapper(
|
|
3
3
|
:class="{ 'k-chart--grabbing': isDragging, 'k-chart--pannable': canPan }"
|
|
4
|
-
:style="{ cursor: isDragging ? 'grabbing' : canPan ? 'grab' : 'default' }"
|
|
5
4
|
@wheel.prevent="onWheel"
|
|
6
5
|
@mousedown="onMouseDown"
|
|
7
6
|
@mousemove="onMouseMove"
|
|
@@ -25,7 +24,7 @@
|
|
|
25
24
|
:height="plotHeight.value"
|
|
26
25
|
)
|
|
27
26
|
g.k-chart-hits
|
|
28
|
-
// Hit detection is
|
|
27
|
+
// Hit detection is handled by calculation in onMouseMove
|
|
29
28
|
|
|
30
29
|
// Dynamic Interaction Layer
|
|
31
30
|
g.k-chart-interaction(v-if="hoveredIndex !== -1 && hoveredBar" clip-path="url(#plot-clip)")
|
|
@@ -35,7 +34,6 @@
|
|
|
35
34
|
:width="hoveredBar.w"
|
|
36
35
|
:height="hoveredBar.h"
|
|
37
36
|
fill="transparent"
|
|
38
|
-
style="cursor: pointer"
|
|
39
37
|
@click="emit('click-point', hoveredBar.item, hoveredIndex)"
|
|
40
38
|
)
|
|
41
39
|
rect(
|
|
@@ -49,7 +47,7 @@
|
|
|
49
47
|
rx="2"
|
|
50
48
|
pointer-events="none"
|
|
51
49
|
)
|
|
52
|
-
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="
|
|
50
|
+
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="tooltipStyle")
|
|
53
51
|
slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
|
|
54
52
|
span {{ tooltipState.content }}
|
|
55
53
|
</template>
|
|
@@ -62,6 +60,7 @@ import { useChartData } from "../../composables/useChartData";
|
|
|
62
60
|
import { useChartViewport } from "../../composables/useChartViewport";
|
|
63
61
|
import { useChartAnimation, easeOut } from "../../composables/useChartAnimation";
|
|
64
62
|
import { useChartExport } from "../../composables/useChartExport";
|
|
63
|
+
import { cleanNumber, safeMax } from "../../composables/chartUtils";
|
|
65
64
|
const props = defineProps({
|
|
66
65
|
dataProvider: { type: null, required: false },
|
|
67
66
|
categoryField: { type: String, required: false, default: "category" },
|
|
@@ -82,7 +81,7 @@ const emit = defineEmits(["click-point", "hover-point", "zoom-change", "pan-chan
|
|
|
82
81
|
const containerRef = ref(null);
|
|
83
82
|
const canvasRef = ref(null);
|
|
84
83
|
const svgRef = ref(null);
|
|
85
|
-
const { ctx, width, height, clear, setupCanvas } = useChartCanvas();
|
|
84
|
+
const { ctx, width, height, resizeTick, clear, setupCanvas } = useChartCanvas();
|
|
86
85
|
const { hoveredIndex, tooltipState, showTooltip, hideTooltip, setHovered } = useChartSvg();
|
|
87
86
|
const dataRef = computed(() => props.dataProvider);
|
|
88
87
|
const { items } = useChartData(dataRef);
|
|
@@ -91,6 +90,10 @@ const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
|
91
90
|
const isLoading = computed(() => {
|
|
92
91
|
return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
|
|
93
92
|
});
|
|
93
|
+
const tooltipStyle = computed(() => ({
|
|
94
|
+
left: `${tooltipState.value.x}px`,
|
|
95
|
+
top: `${tooltipState.value.y}px`
|
|
96
|
+
}));
|
|
94
97
|
const maxZoomRef = computed(() => props.maxZoom);
|
|
95
98
|
const plotWidthRef = computed(() => Math.max(0, width.value - 75));
|
|
96
99
|
const plotLeftRef = computed(() => 55);
|
|
@@ -119,12 +122,12 @@ const groups = computed(() => [...new Set(items.value.map((r) => String(r[props.
|
|
|
119
122
|
const maxValue = computed(() => {
|
|
120
123
|
if (props.mode === "stacked") {
|
|
121
124
|
const sums = categories.value.map(
|
|
122
|
-
(cat) => items.value.filter((r) => String(r[props.categoryField]) === cat).reduce((acc, r) => acc + Math.max(0,
|
|
125
|
+
(cat) => items.value.filter((r) => String(r[props.categoryField]) === cat).reduce((acc, r) => acc + Math.max(0, cleanNumber(r[props.valueField])), 0)
|
|
123
126
|
);
|
|
124
|
-
return
|
|
127
|
+
return safeMax(sums, 10);
|
|
125
128
|
}
|
|
126
|
-
const vals = items.value.map((r) =>
|
|
127
|
-
return
|
|
129
|
+
const vals = items.value.map((r) => cleanNumber(r[props.valueField]));
|
|
130
|
+
return safeMax(vals, 10);
|
|
128
131
|
});
|
|
129
132
|
const visibleCatCount = computed(() => Math.max(1, Math.ceil(categories.value.length / scale.value)));
|
|
130
133
|
const panCatOffset = computed(() => {
|
|
@@ -144,117 +147,228 @@ const draw = () => {
|
|
|
144
147
|
if (!ctx.value) return;
|
|
145
148
|
clear(props.backgroundColor);
|
|
146
149
|
const c = ctx.value;
|
|
147
|
-
const p = easeOut(progress.value);
|
|
150
|
+
const p = props.animated ? easeOut(progress.value) : 1;
|
|
148
151
|
const bars = [];
|
|
152
|
+
const isHorizontal = props.orientation === "horizontal";
|
|
149
153
|
const catCount = visibleCategories.value.length;
|
|
150
154
|
const gCount = groups.value.length || 1;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
155
|
+
if (isHorizontal) {
|
|
156
|
+
const catHeight = plotHeight.value / (catCount || 1);
|
|
157
|
+
let barHeight = catHeight * 0.7;
|
|
158
|
+
if (props.mode === "clustered" && gCount > 1) {
|
|
159
|
+
barHeight = catHeight * 0.8 / gCount;
|
|
160
|
+
}
|
|
161
|
+
c.save();
|
|
162
|
+
c.strokeStyle = "rgba(100,100,120,0.1)";
|
|
163
|
+
c.lineWidth = 1;
|
|
164
|
+
c.fillStyle = "rgba(150,150,170,0.8)";
|
|
165
|
+
c.font = "11px system-ui, sans-serif";
|
|
166
|
+
c.textAlign = "center";
|
|
167
|
+
for (let i = 0; i <= 5; i++) {
|
|
168
|
+
const val = maxValue.value / 5 * i;
|
|
169
|
+
const x = plotLeft + val / maxValue.value * plotWidth.value;
|
|
170
|
+
c.beginPath();
|
|
171
|
+
c.moveTo(x, plotTop);
|
|
172
|
+
c.lineTo(x, plotTop + plotHeight.value);
|
|
173
|
+
c.stroke();
|
|
174
|
+
c.fillText(val.toFixed(0), x, plotTop + plotHeight.value + 16);
|
|
175
|
+
}
|
|
176
|
+
c.strokeStyle = "rgba(100,100,120,0.05)";
|
|
177
|
+
for (let i = 0; i <= catCount; i++) {
|
|
178
|
+
const y = plotTop + i * catHeight;
|
|
179
|
+
c.beginPath();
|
|
180
|
+
c.moveTo(plotLeft, y);
|
|
181
|
+
c.lineTo(plotLeft + plotWidth.value, y);
|
|
182
|
+
c.stroke();
|
|
183
|
+
}
|
|
184
|
+
c.strokeStyle = "rgba(150,150,170,0.5)";
|
|
165
185
|
c.beginPath();
|
|
166
|
-
c.moveTo(plotLeft,
|
|
167
|
-
c.lineTo(plotLeft + plotWidth.value,
|
|
186
|
+
c.moveTo(plotLeft, plotTop + plotHeight.value);
|
|
187
|
+
c.lineTo(plotLeft + plotWidth.value, plotTop + plotHeight.value);
|
|
168
188
|
c.stroke();
|
|
169
|
-
c.fillText(val.toFixed(0), plotLeft - 8, y + 4);
|
|
170
|
-
}
|
|
171
|
-
c.strokeStyle = "rgba(100,100,120,0.05)";
|
|
172
|
-
for (let i = 0; i <= catCount; i++) {
|
|
173
|
-
const x = plotLeft + i * catWidth;
|
|
174
189
|
c.beginPath();
|
|
175
|
-
c.moveTo(
|
|
176
|
-
c.lineTo(
|
|
190
|
+
c.moveTo(plotLeft, plotTop);
|
|
191
|
+
c.lineTo(plotLeft, plotTop + plotHeight.value);
|
|
177
192
|
c.stroke();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
193
|
+
c.restore();
|
|
194
|
+
c.save();
|
|
195
|
+
c.beginPath();
|
|
196
|
+
c.rect(plotLeft, plotTop, plotWidth.value, plotHeight.value);
|
|
197
|
+
c.clip();
|
|
198
|
+
visibleCategories.value.forEach((cat, catIdx) => {
|
|
199
|
+
const catItems = items.value.filter((r) => String(r[props.categoryField]) === cat);
|
|
200
|
+
const catY = plotTop + catIdx * catHeight;
|
|
201
|
+
if (props.mode === "stacked") {
|
|
202
|
+
let xOffset = 0;
|
|
203
|
+
groups.value.forEach((group, gIdx) => {
|
|
204
|
+
const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
|
|
205
|
+
if (!item) return;
|
|
206
|
+
const val = cleanNumber(item[props.valueField]) * p;
|
|
207
|
+
const bw = val / maxValue.value * plotWidth.value;
|
|
208
|
+
const bx = plotLeft + xOffset / maxValue.value * plotWidth.value;
|
|
209
|
+
const by = catY + (catHeight - barHeight) / 2;
|
|
210
|
+
const isHovered = hoveredIndex.value === bars.length;
|
|
211
|
+
const color = props.colors[gIdx % props.colors.length];
|
|
212
|
+
c.fillStyle = isHovered ? color : color + "dd";
|
|
213
|
+
c.fillRect(bx, by, bw, barHeight);
|
|
214
|
+
c.strokeStyle = "rgba(255,255,255,0.3)";
|
|
215
|
+
c.lineWidth = 1;
|
|
216
|
+
c.strokeRect(bx, by, bw, barHeight);
|
|
217
|
+
bars.push({ x: bx, y: by, w: bw, h: barHeight, item, label: `${cat} (${group}): ${item[props.valueField]}` });
|
|
218
|
+
xOffset += cleanNumber(item[props.valueField]);
|
|
219
|
+
});
|
|
220
|
+
} else if (props.mode === "simple") {
|
|
221
|
+
const item = catItems[0];
|
|
222
|
+
if (item) {
|
|
223
|
+
const isHovered = hoveredIndex.value === bars.length;
|
|
224
|
+
const val = cleanNumber(item[props.valueField]) * p;
|
|
225
|
+
const bw = val / maxValue.value * plotWidth.value;
|
|
226
|
+
const bx = plotLeft;
|
|
227
|
+
const by = catY + (catHeight - barHeight) / 2;
|
|
228
|
+
const color = props.colors[0] || "#6366f1";
|
|
229
|
+
c.fillStyle = isHovered ? color : color + "dd";
|
|
230
|
+
c.fillRect(bx, by, bw, barHeight);
|
|
231
|
+
bars.push({ x: bx, y: by, w: bw, h: barHeight, item, label: `${cat}: ${item[props.valueField]}` });
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
groups.value.forEach((group, gIdx) => {
|
|
235
|
+
const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
|
|
236
|
+
if (!item) return;
|
|
237
|
+
const isHovered = hoveredIndex.value === bars.length;
|
|
238
|
+
const val = cleanNumber(item[props.valueField]) * p;
|
|
239
|
+
const bw = val / maxValue.value * plotWidth.value;
|
|
240
|
+
const gCountTotal = groups.value.length;
|
|
241
|
+
const totalBarsH = gCountTotal * barHeight + (gCountTotal - 1) * 2;
|
|
242
|
+
const startY = catY + (catHeight - totalBarsH) / 2;
|
|
243
|
+
const by = startY + gIdx * (barHeight + 2);
|
|
244
|
+
const bx = plotLeft;
|
|
245
|
+
const color = props.colors[gIdx % props.colors.length];
|
|
246
|
+
c.fillStyle = isHovered ? color : color + "dd";
|
|
247
|
+
c.fillRect(bx, by, bw, barHeight);
|
|
248
|
+
bars.push({ x: bx, y: by, w: bw, h: barHeight, item, label: `${cat} (${group}): ${item[props.valueField]}` });
|
|
249
|
+
});
|
|
227
250
|
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
251
|
+
});
|
|
252
|
+
c.restore();
|
|
253
|
+
c.save();
|
|
254
|
+
c.fillStyle = "rgba(150,150,170,0.9)";
|
|
255
|
+
c.font = "11px system-ui, sans-serif";
|
|
256
|
+
c.textAlign = "right";
|
|
257
|
+
visibleCategories.value.forEach((cat, catIdx) => {
|
|
258
|
+
const catY = plotTop + catIdx * catHeight;
|
|
259
|
+
const label = cat.length > 10 ? cat.slice(0, 10) + "\u2026" : cat;
|
|
260
|
+
c.fillText(label, plotLeft - 8, catY + catHeight / 2 + 4);
|
|
261
|
+
});
|
|
262
|
+
c.restore();
|
|
263
|
+
} else {
|
|
264
|
+
const catWidth = plotWidth.value / (catCount || 1);
|
|
265
|
+
let barWidth = catWidth * 0.7;
|
|
266
|
+
if (props.mode === "clustered" && gCount > 1) {
|
|
267
|
+
barWidth = catWidth * 0.8 / gCount;
|
|
245
268
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
269
|
+
c.save();
|
|
270
|
+
c.strokeStyle = "rgba(100,100,120,0.1)";
|
|
271
|
+
c.lineWidth = 1;
|
|
272
|
+
c.fillStyle = "rgba(150,150,170,0.8)";
|
|
273
|
+
c.font = "11px system-ui, sans-serif";
|
|
274
|
+
c.textAlign = "right";
|
|
275
|
+
for (let i = 0; i <= 5; i++) {
|
|
276
|
+
const val = maxValue.value / 5 * i;
|
|
277
|
+
const y = plotTop + plotHeight.value - val / maxValue.value * plotHeight.value;
|
|
278
|
+
c.beginPath();
|
|
279
|
+
c.moveTo(plotLeft, y);
|
|
280
|
+
c.lineTo(plotLeft + plotWidth.value, y);
|
|
281
|
+
c.stroke();
|
|
282
|
+
c.fillText(val.toFixed(0), plotLeft - 8, y + 4);
|
|
283
|
+
}
|
|
284
|
+
c.strokeStyle = "rgba(100,100,120,0.05)";
|
|
285
|
+
for (let i = 0; i <= catCount; i++) {
|
|
286
|
+
const x = plotLeft + i * catWidth;
|
|
287
|
+
c.beginPath();
|
|
288
|
+
c.moveTo(x, plotTop);
|
|
289
|
+
c.lineTo(x, plotTop + plotHeight.value);
|
|
290
|
+
c.stroke();
|
|
291
|
+
}
|
|
292
|
+
c.strokeStyle = "rgba(150,150,170,0.5)";
|
|
293
|
+
c.beginPath();
|
|
294
|
+
c.moveTo(plotLeft, plotTop + plotHeight.value);
|
|
295
|
+
c.lineTo(plotLeft + plotWidth.value, plotTop + plotHeight.value);
|
|
296
|
+
c.stroke();
|
|
297
|
+
c.beginPath();
|
|
298
|
+
c.moveTo(plotLeft, plotTop);
|
|
299
|
+
c.lineTo(plotLeft, plotTop + plotHeight.value);
|
|
300
|
+
c.stroke();
|
|
301
|
+
c.restore();
|
|
302
|
+
c.save();
|
|
303
|
+
c.beginPath();
|
|
304
|
+
c.rect(plotLeft, plotTop, plotWidth.value, plotHeight.value);
|
|
305
|
+
c.clip();
|
|
306
|
+
visibleCategories.value.forEach((cat, catIdx) => {
|
|
307
|
+
const catItems = items.value.filter((r) => String(r[props.categoryField]) === cat);
|
|
308
|
+
const catX = plotLeft + catIdx * catWidth;
|
|
309
|
+
if (props.mode === "stacked") {
|
|
310
|
+
let yOffset = 0;
|
|
311
|
+
groups.value.forEach((group, gIdx) => {
|
|
312
|
+
const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
|
|
313
|
+
if (!item) return;
|
|
314
|
+
const val = cleanNumber(item[props.valueField]) * p;
|
|
315
|
+
const bh = val / maxValue.value * plotHeight.value;
|
|
316
|
+
const bx = catX + (catWidth - barWidth) / 2;
|
|
317
|
+
const by = plotTop + plotHeight.value - (yOffset + val) / maxValue.value * plotHeight.value;
|
|
318
|
+
const isHovered = hoveredIndex.value === bars.length;
|
|
319
|
+
const color = props.colors[gIdx % props.colors.length];
|
|
320
|
+
c.fillStyle = isHovered ? color : color + "dd";
|
|
321
|
+
c.fillRect(bx, by, barWidth, bh);
|
|
322
|
+
c.strokeStyle = "rgba(255,255,255,0.3)";
|
|
323
|
+
c.lineWidth = 1;
|
|
324
|
+
c.strokeRect(bx, by, barWidth, bh);
|
|
325
|
+
bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat} (${group}): ${item[props.valueField]}` });
|
|
326
|
+
yOffset += cleanNumber(item[props.valueField]);
|
|
327
|
+
});
|
|
328
|
+
} else if (props.mode === "simple") {
|
|
329
|
+
const item = catItems[0];
|
|
330
|
+
if (item) {
|
|
331
|
+
const isHovered = hoveredIndex.value === bars.length;
|
|
332
|
+
const val = cleanNumber(item[props.valueField]) * p;
|
|
333
|
+
const bh = val / maxValue.value * plotHeight.value;
|
|
334
|
+
const bx = catX + (catWidth - barWidth) / 2;
|
|
335
|
+
const by = plotTop + plotHeight.value - bh;
|
|
336
|
+
const color = props.colors[0] || "#6366f1";
|
|
337
|
+
c.fillStyle = isHovered ? color : color + "dd";
|
|
338
|
+
c.fillRect(bx, by, barWidth, bh);
|
|
339
|
+
bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat}: ${item[props.valueField]}` });
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
groups.value.forEach((group, gIdx) => {
|
|
343
|
+
const item = catItems.find((r) => String(r[props.groupField] ?? "default") === group);
|
|
344
|
+
if (!item) return;
|
|
345
|
+
const isHovered = hoveredIndex.value === bars.length;
|
|
346
|
+
const val = cleanNumber(item[props.valueField]) * p;
|
|
347
|
+
const bh = val / maxValue.value * plotHeight.value;
|
|
348
|
+
const gCountTotal = groups.value.length;
|
|
349
|
+
const totalBarsW = gCountTotal * barWidth + (gCountTotal - 1) * 2;
|
|
350
|
+
const startX = catX + (catWidth - totalBarsW) / 2;
|
|
351
|
+
const bx = startX + gIdx * (barWidth + 2);
|
|
352
|
+
const by = plotTop + plotHeight.value - bh;
|
|
353
|
+
const color = props.colors[gIdx % props.colors.length];
|
|
354
|
+
c.fillStyle = isHovered ? color : color + "dd";
|
|
355
|
+
c.fillRect(bx, by, barWidth, bh);
|
|
356
|
+
bars.push({ x: bx, y: by, w: barWidth, h: bh, item, label: `${cat} (${group}): ${item[props.valueField]}` });
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
c.restore();
|
|
361
|
+
c.save();
|
|
362
|
+
c.fillStyle = "rgba(150,150,170,0.9)";
|
|
363
|
+
c.font = "11px system-ui, sans-serif";
|
|
364
|
+
c.textAlign = "center";
|
|
365
|
+
visibleCategories.value.forEach((cat, catIdx) => {
|
|
366
|
+
const catX = plotLeft + catIdx * catWidth;
|
|
367
|
+
const label = cat.length > 10 ? cat.slice(0, 10) + "\u2026" : cat;
|
|
368
|
+
c.fillText(label, catX + catWidth / 2, plotTop + plotHeight.value + 18);
|
|
369
|
+
});
|
|
370
|
+
c.restore();
|
|
371
|
+
}
|
|
258
372
|
c.save();
|
|
259
373
|
c.fillStyle = "rgba(120,120,140,0.8)";
|
|
260
374
|
if (props.xAxisTitle) {
|
|
@@ -291,10 +405,12 @@ const drawLegend = (c) => {
|
|
|
291
405
|
});
|
|
292
406
|
c.restore();
|
|
293
407
|
};
|
|
294
|
-
watch(
|
|
408
|
+
watch(progress, draw);
|
|
409
|
+
watch([scale, panOffset, hoveredIndex, resizeTick, width, height, () => props.mode, () => props.orientation], draw);
|
|
295
410
|
watch(items, () => {
|
|
296
411
|
if (props.animated) animate();
|
|
297
|
-
|
|
412
|
+
else draw();
|
|
413
|
+
});
|
|
298
414
|
watch(scale, (v) => emit("zoom-change", v));
|
|
299
415
|
watch(panOffset, (v) => emit("pan-change", v), { deep: true });
|
|
300
416
|
onMounted(async () => {
|
|
@@ -339,7 +455,7 @@ defineExpose({
|
|
|
339
455
|
zoomOut: viewport.zoomOut,
|
|
340
456
|
resetZoom: viewport.resetZoom,
|
|
341
457
|
exportPng: () => canvasRef.value && exportPng(canvasRef.value, "bar-chart.png"),
|
|
342
|
-
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "bar-chart.svg")
|
|
458
|
+
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "bar-chart.svg", canvasRef.value)
|
|
343
459
|
});
|
|
344
460
|
</script>
|
|
345
461
|
|
|
@@ -347,6 +463,13 @@ defineExpose({
|
|
|
347
463
|
.k-chart-wrapper {
|
|
348
464
|
position: relative;
|
|
349
465
|
user-select: none;
|
|
466
|
+
cursor: default;
|
|
467
|
+
}
|
|
468
|
+
.k-chart-wrapper.k-chart--pannable {
|
|
469
|
+
cursor: grab;
|
|
470
|
+
}
|
|
471
|
+
.k-chart-wrapper.k-chart--grabbing {
|
|
472
|
+
cursor: grabbing;
|
|
350
473
|
}
|
|
351
474
|
.k-chart-wrapper .k-chart-controls {
|
|
352
475
|
position: absolute;
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
:width="hoveredCell.w"
|
|
22
22
|
:height="hoveredCell.h"
|
|
23
23
|
fill="transparent"
|
|
24
|
-
style="cursor: pointer"
|
|
25
24
|
@click="emit('click-point', hoveredCell.item, hoveredIndex)"
|
|
26
25
|
)
|
|
27
26
|
rect(
|
|
@@ -37,7 +36,7 @@
|
|
|
37
36
|
)
|
|
38
37
|
|
|
39
38
|
// Labels remain in SVG for sharpness and easy styling
|
|
40
|
-
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="
|
|
39
|
+
.k-chart-tooltip(v-if="tooltipState.visible && !isLoading" :style="tooltipStyle")
|
|
41
40
|
slot(name="tooltip" :item="tooltipState.item" :index="tooltipState.index")
|
|
42
41
|
span {{ tooltipState.content }}
|
|
43
42
|
</template>
|
|
@@ -48,6 +47,7 @@ import { useChartSvg } from "../../composables/useChartSvg";
|
|
|
48
47
|
import { useChartData } from "../../composables/useChartData";
|
|
49
48
|
import { useChartCanvas } from "../../composables/useChartCanvas";
|
|
50
49
|
import { useChartExport } from "../../composables/useChartExport";
|
|
50
|
+
import { cleanNumber, safeMax, safeMin } from "../../composables/chartUtils";
|
|
51
51
|
const props = defineProps({
|
|
52
52
|
dataProvider: { type: null, required: false },
|
|
53
53
|
rowField: { type: String, required: false, default: "row" },
|
|
@@ -73,20 +73,34 @@ const { exportPng, exportSvg: exportSvgFile } = useChartExport();
|
|
|
73
73
|
const isLoading = computed(() => {
|
|
74
74
|
return props.dataProvider?.loading?.value || props.dataProvider?.initialLoad?.value || false;
|
|
75
75
|
});
|
|
76
|
+
const tooltipStyle = computed(() => ({
|
|
77
|
+
left: `${tooltipState.value.x}px`,
|
|
78
|
+
top: `${tooltipState.value.y}px`
|
|
79
|
+
}));
|
|
76
80
|
const padLeft = 80;
|
|
77
81
|
const padTop = 30;
|
|
78
82
|
const padRight = 20;
|
|
79
83
|
const padBottom = 20;
|
|
80
84
|
const rows = computed(() => [...new Set(items.value.map((r) => String(r[props.rowField])))]);
|
|
81
85
|
const cols = computed(() => [...new Set(items.value.map((r) => String(r[props.columnField])))]);
|
|
86
|
+
const rowIndexMap = computed(() => {
|
|
87
|
+
const map = /* @__PURE__ */ new Map();
|
|
88
|
+
rows.value.forEach((r, i) => map.set(r, i));
|
|
89
|
+
return map;
|
|
90
|
+
});
|
|
91
|
+
const colIndexMap = computed(() => {
|
|
92
|
+
const map = /* @__PURE__ */ new Map();
|
|
93
|
+
cols.value.forEach((c, i) => map.set(c, i));
|
|
94
|
+
return map;
|
|
95
|
+
});
|
|
82
96
|
const cellW = computed(() => Math.max(8, (width.value - padLeft - padRight) / (cols.value.length || 1)));
|
|
83
97
|
const cellH = computed(() => Math.max(8, (height.value - padTop - padBottom) / (rows.value.length || 1)));
|
|
84
|
-
const maxVal = computed(() =>
|
|
98
|
+
const maxVal = computed(() => safeMax(items.value.map((r) => cleanNumber(r[props.valueField])), 1));
|
|
99
|
+
const minVal = computed(() => safeMin(items.value.map((r) => cleanNumber(r[props.valueField])), 0));
|
|
85
100
|
const hoveredCell = computed(() => {
|
|
86
101
|
if (hoveredIndex.value === -1) return null;
|
|
87
102
|
return renderedCells.value[hoveredIndex.value];
|
|
88
103
|
});
|
|
89
|
-
const minVal = computed(() => Math.min(...items.value.map((r) => Number(r[props.valueField])), 0));
|
|
90
104
|
const lerp = (t) => {
|
|
91
105
|
const low = parseInt((props.colorLow || "#1e1e2e").replace("#", ""), 16);
|
|
92
106
|
const high = parseInt((props.colorHigh || "#6366f1").replace("#", ""), 16);
|
|
@@ -104,9 +118,10 @@ const lerp = (t) => {
|
|
|
104
118
|
const renderedCells = computed(() => {
|
|
105
119
|
const range = maxVal.value - minVal.value || 1;
|
|
106
120
|
return items.value.map((item) => {
|
|
107
|
-
const ri =
|
|
108
|
-
const ci =
|
|
109
|
-
const
|
|
121
|
+
const ri = rowIndexMap.value.get(String(item[props.rowField])) ?? 0;
|
|
122
|
+
const ci = colIndexMap.value.get(String(item[props.columnField])) ?? 0;
|
|
123
|
+
const val = cleanNumber(item[props.valueField]);
|
|
124
|
+
const t = Math.max(0, Math.min(1, (val - minVal.value) / range));
|
|
110
125
|
return {
|
|
111
126
|
x: padLeft + ci * cellW.value,
|
|
112
127
|
y: padTop + ri * cellH.value,
|
|
@@ -114,7 +129,7 @@ const renderedCells = computed(() => {
|
|
|
114
129
|
h: cellH.value - 1,
|
|
115
130
|
color: lerp(t),
|
|
116
131
|
item,
|
|
117
|
-
label: `${item[props.rowField]} \xD7 ${item[props.columnField]}: ${
|
|
132
|
+
label: `${item[props.rowField]} \xD7 ${item[props.columnField]}: ${val}`
|
|
118
133
|
};
|
|
119
134
|
});
|
|
120
135
|
});
|
|
@@ -124,12 +139,9 @@ const draw = () => {
|
|
|
124
139
|
const c = ctx.value;
|
|
125
140
|
drawAxes(c);
|
|
126
141
|
renderedCells.value.forEach((cell, i) => {
|
|
127
|
-
const isHovered = hoveredIndex.value === i;
|
|
128
142
|
c.fillStyle = cell.color;
|
|
129
|
-
c.
|
|
130
|
-
|
|
131
|
-
c.fill();
|
|
132
|
-
if (isHovered) {
|
|
143
|
+
c.fillRect(cell.x, cell.y, cell.w, cell.h);
|
|
144
|
+
if (hoveredIndex.value === i) {
|
|
133
145
|
c.strokeStyle = "#fff";
|
|
134
146
|
c.lineWidth = 2;
|
|
135
147
|
c.strokeRect(cell.x + 1, cell.y + 1, cell.w - 2, cell.h - 2);
|
|
@@ -137,7 +149,6 @@ const draw = () => {
|
|
|
137
149
|
});
|
|
138
150
|
if (props.showLegend) drawLegend(c);
|
|
139
151
|
};
|
|
140
|
-
watch([items, renderedCells, hoveredIndex, width, height, resizeTick], draw, { deep: true });
|
|
141
152
|
const drawAxes = (c) => {
|
|
142
153
|
c.save();
|
|
143
154
|
c.fillStyle = "rgba(120,120,140,0.8)";
|
|
@@ -186,6 +197,8 @@ const drawLegend = (c) => {
|
|
|
186
197
|
c.fillText(maxVal.value.toString(), x + gradW, y + gradH + 10);
|
|
187
198
|
c.restore();
|
|
188
199
|
};
|
|
200
|
+
watch([renderedCells, hoveredIndex, width, height, resizeTick], draw);
|
|
201
|
+
watch(items, draw);
|
|
189
202
|
watch(isLoading, (val) => {
|
|
190
203
|
if (!val) {
|
|
191
204
|
nextTick(() => {
|
|
@@ -227,7 +240,7 @@ const onMouseLeave = () => {
|
|
|
227
240
|
};
|
|
228
241
|
defineExpose({
|
|
229
242
|
exportPng: () => canvasRef.value && exportPng(canvasRef.value, "heatmap.png"),
|
|
230
|
-
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "heatmap.svg")
|
|
243
|
+
exportSvg: () => svgRef.value && exportSvgFile(svgRef.value, "heatmap.svg", canvasRef.value)
|
|
231
244
|
});
|
|
232
245
|
</script>
|
|
233
246
|
|