@redseed/redseed-ui-vue3 8.66.1 → 8.67.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/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
// A magnitude grid: one cell per (row, column), shaded light-to-dark by the
|
|
3
|
+
// size of its value. The GitHub contributions grid is the familiar example —
|
|
4
|
+
// days down, weeks across.
|
|
5
|
+
//
|
|
6
|
+
// ApexCharts touches window/DOM, so this renders client-side only (guarded by
|
|
7
|
+
// the `mounted` ref), matching the other rsui charts.
|
|
8
|
+
//
|
|
9
|
+
// Colour comes from the rsui sequential scale, never from the consumer: pass
|
|
10
|
+
// `ranges` as plain numeric buckets and each one takes the next step of the
|
|
11
|
+
// ramp, so a consuming app still writes no colours of its own.
|
|
12
|
+
import { ref, computed, onMounted } from 'vue'
|
|
13
|
+
import VueApexCharts from 'vue3-apexcharts'
|
|
14
|
+
import { resolveTheme, baseChartOptions, deepMergeOptions } from './chartTheme'
|
|
15
|
+
|
|
16
|
+
const props = defineProps({
|
|
17
|
+
// ApexCharts heatmap series: [{ name: <row label>, data: [{ x: <column
|
|
18
|
+
// label>, y: <value> }] }]. Rows render bottom-up, so pass them in the
|
|
19
|
+
// order you want read from the bottom of the grid.
|
|
20
|
+
series: {
|
|
21
|
+
type: Array,
|
|
22
|
+
default: () => [],
|
|
23
|
+
},
|
|
24
|
+
// Value buckets, lowest first: [{ from, to, name }]. Each takes the next
|
|
25
|
+
// step of the rsui scale, so five is the most that shade distinctly.
|
|
26
|
+
// Omitted, ApexCharts shades continuously from one colour.
|
|
27
|
+
ranges: {
|
|
28
|
+
type: Array,
|
|
29
|
+
default: () => [],
|
|
30
|
+
},
|
|
31
|
+
height: {
|
|
32
|
+
type: [Number, String],
|
|
33
|
+
default: 240,
|
|
34
|
+
},
|
|
35
|
+
// Escape hatch — deep-merged over the rsui defaults
|
|
36
|
+
options: {
|
|
37
|
+
type: Object,
|
|
38
|
+
default: () => ({}),
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
const root = ref(null)
|
|
43
|
+
const mounted = ref(false)
|
|
44
|
+
const theme = ref(resolveTheme(null))
|
|
45
|
+
|
|
46
|
+
onMounted(() => {
|
|
47
|
+
theme.value = resolveTheme(root.value)
|
|
48
|
+
mounted.value = true
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Buckets past the end of the ramp reuse its darkest step rather than coming
|
|
52
|
+
// back undefined, which ApexCharts renders as a black cell.
|
|
53
|
+
const shadedRanges = computed(() =>
|
|
54
|
+
props.ranges.map((range, index) => ({
|
|
55
|
+
...range,
|
|
56
|
+
color: theme.value.scale[index] ?? theme.value.scale[theme.value.scale.length - 1],
|
|
57
|
+
})),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const hasRanges = computed(() => props.ranges.length > 0)
|
|
61
|
+
|
|
62
|
+
const chartOptions = computed(() =>
|
|
63
|
+
deepMergeOptions(
|
|
64
|
+
deepMergeOptions(
|
|
65
|
+
baseChartOptions({
|
|
66
|
+
type: 'heatmap',
|
|
67
|
+
theme: theme.value,
|
|
68
|
+
height: props.height,
|
|
69
|
+
}),
|
|
70
|
+
hasRanges.value
|
|
71
|
+
? {
|
|
72
|
+
plotOptions: {
|
|
73
|
+
heatmap: {
|
|
74
|
+
shadeIntensity: 0,
|
|
75
|
+
enableShades: false,
|
|
76
|
+
colorScale: { ranges: shadedRanges.value },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
: {},
|
|
81
|
+
),
|
|
82
|
+
props.options,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<template>
|
|
88
|
+
<div
|
|
89
|
+
ref="root"
|
|
90
|
+
class="rsui-chart"
|
|
91
|
+
>
|
|
92
|
+
<VueApexCharts
|
|
93
|
+
v-if="mounted"
|
|
94
|
+
type="heatmap"
|
|
95
|
+
:height="height"
|
|
96
|
+
:series="series"
|
|
97
|
+
:options="chartOptions"
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
@@ -31,9 +31,24 @@ const PALETTE_TOKENS = [
|
|
|
31
31
|
{ name: '--Colors-RedSeed-Purple-600', fallback: 'hsla(271, 55%, 54%, 1)' },
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
+
// Sequential light-to-dark ramp for magnitude charts (heatmap). Ordered, not
|
|
35
|
+
// categorical: step 0 is the "nothing here" ground and every step after it
|
|
36
|
+
// reads as more. Grey rather than the palest brand at the bottom so an empty
|
|
37
|
+
// cell cannot be mistaken for a small value.
|
|
38
|
+
const SCALE_TOKENS = [
|
|
39
|
+
{ name: '--Colors-Grey-100', fallback: '#f5f5f5' },
|
|
40
|
+
{ name: '--Colors-Brand-100', fallback: '#c8eadf' },
|
|
41
|
+
{ name: '--Colors-Brand-300', fallback: '#53c6a3' },
|
|
42
|
+
{ name: '--Colors-Brand-400', fallback: '#279b78' },
|
|
43
|
+
{ name: '--Colors-Brand-500', fallback: '#136c52' },
|
|
44
|
+
]
|
|
45
|
+
|
|
34
46
|
const TEXT_TOKEN = { name: '--Colors-Grey-900', fallback: 'hsla(220, 24%, 12%, 1)' }
|
|
35
47
|
const TEXT_MUTED_TOKEN = { name: '--Colors-Grey-700', fallback: 'hsla(220, 11%, 29%, 1)' }
|
|
36
48
|
const GRID_TOKEN = { name: '--Colors-Grey-200', fallback: 'hsla(220, 5%, 92%, 1)' }
|
|
49
|
+
// The surface the chart sits on. Heatmap cells tile edge to edge, so the gaps
|
|
50
|
+
// between them are painted in this colour rather than left as holes.
|
|
51
|
+
const SURFACE_TOKEN = { name: '--color-background-primary', fallback: 'hsla(0, 0%, 100%, 1)' }
|
|
37
52
|
const FONT_TOKEN = { name: '--font-redseed', fallback: "'Inter', sans-serif" }
|
|
38
53
|
|
|
39
54
|
function readToken(el, token) {
|
|
@@ -50,9 +65,11 @@ function readToken(el, token) {
|
|
|
50
65
|
export function resolveTheme(el) {
|
|
51
66
|
return {
|
|
52
67
|
colors: PALETTE_TOKENS.map((token) => readToken(el, token)),
|
|
68
|
+
scale: SCALE_TOKENS.map((token) => readToken(el, token)),
|
|
53
69
|
text: readToken(el, TEXT_TOKEN),
|
|
54
70
|
textMuted: readToken(el, TEXT_MUTED_TOKEN),
|
|
55
71
|
grid: readToken(el, GRID_TOKEN),
|
|
72
|
+
surface: readToken(el, SURFACE_TOKEN),
|
|
56
73
|
fontFamily: readToken(el, FONT_TOKEN),
|
|
57
74
|
}
|
|
58
75
|
}
|
|
@@ -61,7 +78,7 @@ export function resolveTheme(el) {
|
|
|
61
78
|
* Build the rsui ApexCharts defaults for a given chart type. Consumers layer
|
|
62
79
|
* their own `options` over the result via deepMergeOptions().
|
|
63
80
|
*
|
|
64
|
-
* @param {'line'|'bar'|'donut'} type
|
|
81
|
+
* @param {'line'|'bar'|'donut'|'heatmap'} type
|
|
65
82
|
* @param {object} theme result of resolveTheme()
|
|
66
83
|
* @param {number|string} height
|
|
67
84
|
* @param {string[]} [categories] x-axis labels (line/bar)
|
|
@@ -118,6 +135,31 @@ export function baseChartOptions({ type, theme, height, categories, labels, stac
|
|
|
118
135
|
options.yaxis = { labels: { style: { colors: theme.textMuted } } }
|
|
119
136
|
}
|
|
120
137
|
|
|
138
|
+
if (type === 'heatmap') {
|
|
139
|
+
// The cell's own value is the reading, so no axis line, no grid and no
|
|
140
|
+
// legend — the colour scale carries the key instead.
|
|
141
|
+
//
|
|
142
|
+
// No entry animation either: ApexCharts fades a heatmap in cell by cell,
|
|
143
|
+
// which reads as a loading grid rather than a chart. The whole point of
|
|
144
|
+
// the grid is the shape you see at a glance, so show it at once.
|
|
145
|
+
options.chart.animations = { enabled: false }
|
|
146
|
+
options.legend = { show: false }
|
|
147
|
+
// Cells tile edge to edge, so the 2px gutter is a stroke painted in the
|
|
148
|
+
// surface colour — a transparent stroke paints nothing and the cells
|
|
149
|
+
// read as one solid block.
|
|
150
|
+
options.stroke = { width: 2, colors: [theme.surface] }
|
|
151
|
+
// No grid lines, but keep a gutter between the row labels and the first
|
|
152
|
+
// column so the key on the left is not jammed against the cells.
|
|
153
|
+
options.grid = { show: false, padding: { left: 8, right: 0 } }
|
|
154
|
+
options.xaxis = {
|
|
155
|
+
...axis(categories, theme),
|
|
156
|
+
axisBorder: { show: false },
|
|
157
|
+
axisTicks: { show: false },
|
|
158
|
+
tooltip: { enabled: false },
|
|
159
|
+
}
|
|
160
|
+
options.yaxis = { labels: { style: { colors: theme.textMuted } } }
|
|
161
|
+
}
|
|
162
|
+
|
|
121
163
|
if (type === 'donut') {
|
|
122
164
|
options.labels = labels || []
|
|
123
165
|
options.stroke = { width: 0 }
|