Skip to content

Commit 1b56813

Browse files
committed
VueUiHeatmap improved color management
1 parent 8260df5 commit 1b56813

File tree

6 files changed

+140
-11
lines changed

6 files changed

+140
-11
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vue-data-ui",
33
"private": false,
4-
"version": "1.9.78",
4+
"version": "1.9.79",
55
"type": "module",
66
"description": "A user-empowering data visualization Vue components library",
77
"keywords": [

src/components/vue-ui-heatmap.vue

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup>
22
import { ref, computed, nextTick, onMounted } from "vue";
3-
import { opacity, createUid, createCsvContent, downloadCsv } from "../lib";
3+
import { opacity, createUid, createCsvContent, downloadCsv, interpolateColorHex, adaptColorToBackground } from "../lib";
44
import mainConfig from "../default_configs.json";
55
import pdf from "../pdf";
66
import img from "../img";
@@ -80,14 +80,18 @@ const svg = computed(() => {
8080
}
8181
});
8282
83+
const legendPosition = computed(() => {
84+
return heatmapConfig.value.style.legend.position;
85+
})
86+
8387
const drawingArea = computed(() => {
8488
return {
8589
top: heatmapConfig.value.style.layout.padding.top + (mutableConfig.value.inside ? 36 : 0) ,
8690
left: heatmapConfig.value.style.layout.padding.left,
87-
right: svg.value.width - heatmapConfig.value.style.layout.padding.right,
91+
right: svg.value.width - heatmapConfig.value.style.layout.padding.right - (legendPosition.value === "right" && heatmapConfig.value.style.legend.show ? 92 : 0),
8892
bottom: svg.value.height - heatmapConfig.value.style.layout.padding.bottom,
8993
height: svg.value.height - heatmapConfig.value.style.layout.padding.top - heatmapConfig.value.style.layout.padding.bottom,
90-
width: svg.value.width - heatmapConfig.value.style.layout.padding.right - heatmapConfig.value.style.layout.padding.left
94+
width: svg.value.width - heatmapConfig.value.style.layout.padding.right - heatmapConfig.value.style.layout.padding.left - (legendPosition.value === "right" && heatmapConfig.value.style.legend.show ? 92 : 0)
9195
}
9296
});
9397
@@ -132,6 +136,7 @@ const mutableDataset = computed(() => {
132136
if (v >= average.value) {
133137
return {
134138
side: "up",
139+
color: interpolateColorHex(heatmapConfig.value.style.layout.cells.colors.cold, heatmapConfig.value.style.layout.cells.colors.hot, minValue.value, maxValue.value, v),
135140
ratio: Math.abs((Math.abs(v - average.value) / Math.abs(maxValue.value - average.value))) > 1 ? 1 : Math.abs((Math.abs(v - average.value) / Math.abs(maxValue.value - average.value))),
136141
value: v,
137142
yAxisName: ds.name,
@@ -142,6 +147,7 @@ const mutableDataset = computed(() => {
142147
return {
143148
side: "down",
144149
ratio: Math.abs(1 - (Math.abs(v) / Math.abs(average.value))) > 1 ? 1 : Math.abs(1 - (Math.abs(v) / Math.abs(average.value))),
150+
color: interpolateColorHex(heatmapConfig.value.style.layout.cells.colors.cold, heatmapConfig.value.style.layout.cells.colors.hot, minValue.value, maxValue.value, v),
145151
value: v,
146152
yAxisName: ds.name,
147153
xAxisName: dataLabels.value.xLabels[i],
@@ -160,7 +166,7 @@ function useTooltip(datapoint) {
160166
let html = "";
161167
162168
html += `<div data-cy="heatmap-tootlip-name">${yAxisName} ${xAxisName ? `${xAxisName}` : ''}</div>`;
163-
html += `<div data-cy="heatmap-tooltip-value" style="margin-top:6px;padding-top:6px;border-top:1px solid #e1e5e8;font-weight:bold">${isNaN(value) ? "-" : Number(value.toFixed(heatmapConfig.value.style.tooltip.roundingValue)).toLocaleString()}</div>`
169+
html += `<div data-cy="heatmap-tooltip-value" style="margin-top:6px;padding-top:6px;border-top:1px solid #e1e5e8;font-weight:bold;display:flex;flex-direction:row;gap:12px;align-items:center;justify-content:center"><span style="color:${interpolateColorHex(heatmapConfig.value.style.layout.cells.colors.cold, heatmapConfig.value.style.layout.cells.colors.hot, minValue.value, maxValue.value, value)}">⬤</span><span>${isNaN(value) ? "-" : Number(value.toFixed(heatmapConfig.value.style.tooltip.roundingValue)).toLocaleString()}</span></div>`
164170
tooltipContent.value = `<div style="font-size:${heatmapConfig.value.style.tooltip.fontSize}px">${html}</div>`;
165171
}
166172
@@ -340,7 +346,7 @@ defineExpose({
340346
:y="drawingArea.top + cellSize.width * i"
341347
:width="cellSize.width - heatmapConfig.style.layout.cells.spacing"
342348
:height="cellSize.width - heatmapConfig.style.layout.cells.spacing"
343-
:fill="`${cell.side === 'up' ? `${heatmapConfig.style.layout.cells.colors.hot}${opacity[Math.round(cell.ratio * 100)]}` : `${heatmapConfig.style.layout.cells.colors.cold}${opacity[Math.round(cell.ratio * 100)]}`}`"
349+
:fill="cell.color"
344350
:stroke="hoveredCell && hoveredCell === cell.id ? heatmapConfig.style.layout.cells.selected.color : heatmapConfig.style.backgroundColor"
345351
:stroke-width="heatmapConfig.style.layout.cells.spacing"
346352
/>
@@ -349,7 +355,7 @@ defineExpose({
349355
text-anchor="middle"
350356
:font-size="heatmapConfig.style.layout.cells.value.fontSize"
351357
:font-weight="heatmapConfig.style.layout.cells.value.bold ? 'bold': 'normal'"
352-
:fill="heatmapConfig.style.layout.cells.value.color"
358+
:fill="adaptColorToBackground(cell.color)"
353359
:x="(drawingArea.left + cellSize.width * j) + (cellSize.width / 2)"
354360
:y="(drawingArea.top + cellSize.width * i) + (cellSize.width / 2) + heatmapConfig.style.layout.cells.value.fontSize / 3"
355361
>
@@ -396,6 +402,41 @@ defineExpose({
396402
</text>
397403
</g>
398404

405+
<g v-if="heatmapConfig.style.legend.show && legendPosition === 'right'">
406+
<defs>
407+
<linearGradient id="colorScaleVertical" x2="0%" y2="100%" >
408+
<stop offset="0%" :stop-color="heatmapConfig.style.layout.cells.colors.hot"/>
409+
<stop offset="100%" :stop-color="heatmapConfig.style.layout.cells.colors.cold"/>
410+
</linearGradient>
411+
</defs>
412+
<text
413+
:x="drawingArea.right + 36 + 18"
414+
:y="drawingArea.top - heatmapConfig.style.legend.fontSize * 2"
415+
text-anchor="middle"
416+
:font-size="heatmapConfig.style.legend.fontSize * 2"
417+
:fill="heatmapConfig.style.legend.color"
418+
>
419+
{{ Number(maxValue.toFixed(heatmapConfig.style.legend.roundingValue)).toLocaleString() }}
420+
</text>
421+
<rect
422+
:x="drawingArea.right + 36"
423+
:y="drawingArea.top"
424+
:width="36"
425+
:height="drawingArea.height - (heatmapConfig.style.layout.cells.spacing * mutableDataset.length)"
426+
:rx="heatmapConfig.style.legend.scaleBorderRadius"
427+
fill="url(#colorScaleVertical)"
428+
/>
429+
<text
430+
:x="drawingArea.right + 36 + 18"
431+
:y="drawingArea.bottom + heatmapConfig.style.legend.fontSize * 2"
432+
text-anchor="middle"
433+
:font-size="heatmapConfig.style.legend.fontSize * 2"
434+
:fill="heatmapConfig.style.legend.color"
435+
>
436+
{{ Number(minValue.toFixed(heatmapConfig.style.legend.roundingValue)).toLocaleString() }}
437+
</text>
438+
</g>
439+
399440
<!-- LEGEND AS G -->
400441
<foreignObject
401442
v-if="heatmapConfig.style.legend.show && mutableConfig.inside && !isPrinting"
@@ -430,7 +471,7 @@ defineExpose({
430471
</svg>
431472

432473
<!-- LEGEND AS DIV -->
433-
<div v-if="heatmapConfig.style.legend.show && (!mutableConfig.inside || isPrinting)" class="vue-ui-heatmap-legend" :style="`background:${heatmapConfig.style.legend.backgroundColor};color:${heatmapConfig.style.legend.color};font-size:${heatmapConfig.style.legend.fontSize}px;padding-bottom:12px;font-weight:${heatmapConfig.style.legend.bold ? 'bold' : ''};display:flex; flex-direction:row;gap:3px;align-items:center;justify-content:center;font-weight:${heatmapConfig.style.legend.bold ? 'bold':'normal'}`" >
474+
<div v-if="heatmapConfig.style.legend.show && heatmapConfig.style.legend.position === 'bottom' && (!mutableConfig.inside || isPrinting)" class="vue-ui-heatmap-legend" :style="`background:${heatmapConfig.style.legend.backgroundColor};color:${heatmapConfig.style.legend.color};font-size:${heatmapConfig.style.legend.fontSize}px;padding-bottom:12px;font-weight:${heatmapConfig.style.legend.bold ? 'bold' : ''};display:flex; flex-direction:row;gap:3px;align-items:center;justify-content:center;font-weight:${heatmapConfig.style.legend.bold ? 'bold':'normal'}`" >
434475
<span data-cy="heatmap-legend-min" style="text-align:right">{{ Number(minValue.toFixed(heatmapConfig.style.legend.roundingValue)).toLocaleString() }}</span>
435476
<svg viewBox="0 0 132 12" style="width: 300px">
436477
<defs>

src/default_configs.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1645,7 +1645,9 @@
16451645
"color": "#2D353C",
16461646
"fontSize" : 12,
16471647
"bold": true,
1648-
"roundingValue": 0
1648+
"roundingValue": 0,
1649+
"position": "right",
1650+
"scaleBorderRadius": 18
16491651
},
16501652
"tooltip": {
16511653
"show": true,

src/lib.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,89 @@ export function calculateNiceScale(minValue, maxValue, maxTicks) {
832832
};
833833
}
834834

835+
export function interpolateColorHex(minColor, maxColor, minValue, maxValue, value) {
836+
const hexToRgb = (hex) => ({
837+
r: parseInt(hex.substring(1, 3), 16),
838+
g: parseInt(hex.substring(3, 5), 16),
839+
b: parseInt(hex.substring(5, 7), 16),
840+
});
841+
842+
const rgbToHex = ({ r, g, b }) => {
843+
return `#${decimalToHex(r)}${decimalToHex(g)}${decimalToHex(b)}`;
844+
};
845+
846+
const rgbToHsl = ({ r, g, b }) => {
847+
r /= 255;
848+
g /= 255;
849+
b /= 255;
850+
const max = Math.max(r, g, b);
851+
const min = Math.min(r, g, b);
852+
let h, s, l = (max + min) / 2;
853+
854+
if (max === min) {
855+
h = s = 0;
856+
} else {
857+
const d = max - min;
858+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
859+
switch (max) {
860+
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
861+
case g: h = (b - r) / d + 2; break;
862+
case b: h = (r - g) / d + 4; break;
863+
}
864+
h /= 6;
865+
}
866+
return { h, s, l };
867+
};
868+
869+
const hslToRgb = ({ h, s, l }) => {
870+
let r, g, b;
871+
872+
if (s === 0) {
873+
r = g = b = l;
874+
} else {
875+
const hue2rgb = (p, q, t) => {
876+
if (t < 0) t += 1;
877+
if (t > 1) t -= 1;
878+
if (t < 1 / 6) return p + (q - p) * 6 * t;
879+
if (t < 1 / 2) return q;
880+
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
881+
return p;
882+
};
883+
884+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
885+
const p = 2 * l - q;
886+
r = hue2rgb(p, q, h + 1 / 3);
887+
g = hue2rgb(p, q, h);
888+
b = hue2rgb(p, q, h - 1 / 3);
889+
}
890+
891+
return {
892+
r: Math.round(r * 255),
893+
g: Math.round(g * 255),
894+
b: Math.round(b * 255),
895+
};
896+
};
897+
898+
const minRgbColor = hexToRgb(minColor);
899+
const maxRgbColor = hexToRgb(maxColor);
900+
901+
value = Math.min(Math.max(value, minValue), maxValue);
902+
903+
const normalizedValue = (value - minValue) / (maxValue - minValue);
904+
905+
const interpolatedRgbColor = {
906+
r: Math.round(minRgbColor.r + (maxRgbColor.r - minRgbColor.r) * normalizedValue),
907+
g: Math.round(minRgbColor.g + (maxRgbColor.g - minRgbColor.g) * normalizedValue),
908+
b: Math.round(minRgbColor.b + (maxRgbColor.b - minRgbColor.b) * normalizedValue),
909+
};
910+
911+
const interpolatedHslColor = rgbToHsl(interpolatedRgbColor);
912+
const finalRgbColor = hslToRgb(interpolatedHslColor);
913+
const finalHexColor = rgbToHex(finalRgbColor);
914+
915+
return finalHexColor;
916+
}
917+
835918
const lib = {
836919
adaptColorToBackground,
837920
addVector,
@@ -856,6 +939,7 @@ const lib = {
856939
degreesToRadians,
857940
downloadCsv,
858941
giftWrap,
942+
interpolateColorHex,
859943
isSafeValue,
860944
isValidUserValue,
861945
lightenHexColor,

types/vue-data-ui.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,8 @@ declare module 'vue-data-ui' {
15551555
fontSize?: number;
15561556
bold?: boolean;
15571557
roundingValue?: number;
1558+
position?: "right" | "bottom";
1559+
scaleBorderRadius?: number;
15581560
};
15591561
tooltip?: {
15601562
show?: boolean;

0 commit comments

Comments
 (0)