@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseed/redseed-ui-vue3",
3
- "version": "8.66.1",
3
+ "version": "8.67.0",
4
4
  "description": "RedSeed UI Vue 3 components",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/redseedtraining/redseed-ui",
@@ -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 }
@@ -1,9 +1,11 @@
1
1
  import LineChart from './LineChart.vue'
2
2
  import BarChart from './BarChart.vue'
3
3
  import DoughnutChart from './DoughnutChart.vue'
4
+ import HeatmapChart from './HeatmapChart.vue'
4
5
 
5
6
  export {
6
7
  LineChart,
7
8
  BarChart,
8
9
  DoughnutChart,
10
+ HeatmapChart,
9
11
  }