@icij/murmur-next 4.7.5 → 4.8.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.
Files changed (32) hide show
  1. package/dist/lib/lib/datavisualisations/ColumnChart/ColumnChart.vue.d.ts +17 -0
  2. package/dist/lib/lib/datavisualisations/LineChart/LineChart.vue.d.ts +19 -0
  3. package/dist/lib/murmur.css +1 -1
  4. package/dist/lib/murmur.js +10419 -10307
  5. package/dist/lib/murmur.js.map +1 -1
  6. package/dist/lib/murmur.umd.cjs +23 -23
  7. package/dist/lib/murmur.umd.cjs.map +1 -1
  8. package/lib/assets/images/icij-full-white.svg +0 -0
  9. package/lib/assets/images/icij-full.svg +0 -0
  10. package/lib/assets/images/icij.png +0 -0
  11. package/lib/assets/images/icij.svg +0 -0
  12. package/lib/assets/images/icij@2x.png +0 -0
  13. package/lib/assets/images/murmur-dark.png +0 -0
  14. package/lib/assets/images/murmur-dark.svg +0 -0
  15. package/lib/assets/images/murmur-textured.png +0 -0
  16. package/lib/assets/images/murmur-white.png +0 -0
  17. package/lib/assets/images/murmur-white.svg +0 -0
  18. package/lib/config.default.ts +0 -0
  19. package/lib/datavisualisations/ColumnChart/ColumnChart.vue +97 -14
  20. package/lib/datavisualisations/LineChart/LineChart.vue +158 -13
  21. package/lib/datavisualisations/StackedBarChart/StackedBarChart.vue +56 -29
  22. package/lib/datavisualisations/StackedColumnChart/StackedColumnChart.vue +59 -30
  23. package/lib/keys.ts +0 -0
  24. package/lib/locales/fr.json +0 -0
  25. package/lib/styles/lib.scss +0 -0
  26. package/lib/styles/utilities.scss +0 -0
  27. package/lib/styles/variables_dark.scss +0 -0
  28. package/lib/types/d3-geo-projection.d.ts +0 -0
  29. package/lib/types/querystring-es3.d.ts +0 -0
  30. package/lib/types/shims-bootstrap-vue.d.ts +0 -0
  31. package/lib/utils/clipboard.ts +0 -0
  32. package/package.json +1 -1
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -16,6 +16,7 @@ interface ColumnBar {
16
16
  height: number
17
17
  x: number
18
18
  y: number
19
+ isTotal?: boolean
19
20
  }
20
21
 
21
22
  export interface ColumnChartProps {
@@ -131,6 +132,23 @@ export interface ColumnChartProps {
131
132
  * Aspect ratio to use in social mode.
132
133
  */
133
134
  socialModeRatio?: number
135
+ /**
136
+ * Display columns as a waterfall chart where each bar starts where the previous one ended.
137
+ */
138
+ waterfall?: boolean
139
+ /**
140
+ * Show a total bar at the end of the waterfall chart displaying the sum of all values.
141
+ * Only applies when `waterfall` is true.
142
+ */
143
+ waterfallTotal?: boolean
144
+ /**
145
+ * Label for the waterfall total bar on the x-axis.
146
+ */
147
+ waterfallTotalLabel?: string
148
+ /**
149
+ * Color for the waterfall total bar. Falls back to currentColor.
150
+ */
151
+ waterfallTotalColor?: string | null
134
152
  }
135
153
 
136
154
  const props = withDefaults(defineProps<ColumnChartProps>(), {
@@ -161,7 +179,11 @@ const props = withDefaults(defineProps<ColumnChartProps>(), {
161
179
  dataUrlType: 'json',
162
180
  chartHeightRatio: undefined,
163
181
  socialMode: false,
164
- socialModeRatio: 5 / 4
182
+ socialModeRatio: 5 / 4,
183
+ waterfall: false,
184
+ waterfallTotal: false,
185
+ waterfallTotalLabel: 'Total',
186
+ waterfallTotalColor: null
165
187
  })
166
188
 
167
189
  const emit = defineEmits<{
@@ -245,16 +267,29 @@ const padded = computed((): { width: number, height: number } => {
245
267
  })
246
268
 
247
269
  const scaleX = computed((): d3.ScaleBand<string> => {
270
+ const domain = sortedData.value.map(iteratee(props.timeseriesKey))
271
+ if (props.waterfall && props.waterfallTotal) {
272
+ domain.push(props.waterfallTotalLabel)
273
+ }
248
274
  return d3
249
275
  .scaleBand()
250
- .domain(sortedData.value.map(iteratee(props.timeseriesKey)))
276
+ .domain(domain)
251
277
  .range([0, padded.value.width])
252
278
  .padding(props.barPadding)
253
279
  })
254
280
 
281
+ const waterfallTotalValue = computed((): number => {
282
+ return d3.sum(sortedData.value, iteratee(props.seriesName)) ?? 0
283
+ })
284
+
255
285
  const scaleY = computed((): d3.ScaleLinear<number, number> => {
256
- const maxValue
257
- = props.maxValue ?? d3.max(sortedData.value, iteratee(props.seriesName))
286
+ let maxValue: number
287
+ if (props.waterfall) {
288
+ maxValue = props.maxValue ?? waterfallTotalValue.value
289
+ }
290
+ else {
291
+ maxValue = props.maxValue ?? d3.max(sortedData.value, iteratee(props.seriesName))
292
+ }
258
293
  return d3
259
294
  .scaleLinear()
260
295
  .domain([0, maxValue])
@@ -262,13 +297,44 @@ const scaleY = computed((): d3.ScaleLinear<number, number> => {
262
297
  })
263
298
 
264
299
  const bars = computed((): ColumnBar[] => {
300
+ const barWidth = Math.max(1, Math.abs(scaleX.value.bandwidth()) - props.barMargin)
301
+
302
+ if (props.waterfall) {
303
+ let cumulative = 0
304
+ const waterfallBars: ColumnBar[] = sortedData.value.map((datum: any) => {
305
+ const value = datum[props.seriesName]
306
+ cumulative += value
307
+ return {
308
+ datum,
309
+ width: barWidth,
310
+ height: Math.abs(padded.value.height - scaleY.value(value)),
311
+ x: (scaleX.value(datum[props.timeseriesKey]) ?? 0) + props.barMargin / 2,
312
+ y: scaleY.value(cumulative)
313
+ }
314
+ })
315
+
316
+ if (props.waterfallTotal) {
317
+ const totalDatum = {
318
+ [props.timeseriesKey]: props.waterfallTotalLabel,
319
+ [props.seriesName]: waterfallTotalValue.value
320
+ }
321
+ waterfallBars.push({
322
+ datum: totalDatum,
323
+ width: barWidth,
324
+ height: Math.abs(padded.value.height - scaleY.value(waterfallTotalValue.value)),
325
+ x: (scaleX.value(props.waterfallTotalLabel) ?? 0) + props.barMargin / 2,
326
+ y: scaleY.value(waterfallTotalValue.value),
327
+ isTotal: true
328
+ })
329
+ }
330
+
331
+ return waterfallBars
332
+ }
333
+
265
334
  return sortedData.value.map((datum: any) => {
266
335
  return {
267
336
  datum,
268
- width: Math.max(
269
- 1,
270
- Math.abs(scaleX.value.bandwidth()) - props.barMargin
271
- ),
337
+ width: barWidth,
272
338
  height: Math.abs(
273
339
  padded.value.height - scaleY.value(datum[props.seriesName])
274
340
  ),
@@ -298,15 +364,25 @@ const xAxisTickValues = computed((): string[] => {
298
364
  const ticks
299
365
  = props.xAxisTicks ?? sortedData.value.map(iteratee(props.timeseriesKey))
300
366
  // Then filter out ticks according to `this.xAxisHiddenTicks`
301
- return ticks.map((tick: string, i: number) => {
367
+ const filtered = ticks.map((tick: string, i: number) => {
302
368
  return (i + 1) % xAxisHiddenTicks.value ? null : tick
303
369
  }) as string[]
370
+ // Add the total label for waterfall charts
371
+ if (props.waterfall && props.waterfallTotal) {
372
+ filtered.push(props.waterfallTotalLabel)
373
+ }
374
+ return filtered
304
375
  })
305
376
 
306
377
  const xAxis = computed((): d3.Axis<string> => {
307
378
  return d3
308
379
  .axisBottom(scaleX.value)
309
- .tickFormat((d: any) => d3Formatter(d, props.xAxisTickFormat))
380
+ .tickFormat((d: any) => {
381
+ if (props.waterfall && props.waterfallTotal && d === props.waterfallTotalLabel) {
382
+ return d
383
+ }
384
+ return d3Formatter(d, props.xAxisTickFormat)
385
+ })
310
386
  .tickValues(xAxisTickValues.value)
311
387
  })
312
388
 
@@ -379,15 +455,17 @@ watch(() => props.socialMode, update, { immediate: true })
379
455
  <div
380
456
  ref="el"
381
457
  :class="{
382
- 'column-chart--has-highlights': dataHasHighlights,
458
+ 'column-chart--has-highlights': dataHasHighlights || highlights.length > 0,
383
459
  'column-chart--hover': hover,
384
460
  'column-chart--stripped': stripped,
385
461
  'column-chart--social-mode': socialMode,
386
- 'column-chart--loaded': isLoaded
462
+ 'column-chart--loaded': isLoaded,
463
+ 'column-chart--waterfall': waterfall
387
464
  }"
388
465
  :style="{
389
466
  '--column-color': columnColor,
390
- '--column-highlight-color': columnHighlightColor
467
+ '--column-highlight-color': columnHighlightColor,
468
+ '--column-total-color': waterfallTotalColor
391
469
  }"
392
470
  class="column-chart"
393
471
  >
@@ -414,7 +492,8 @@ watch(() => props.socialMode, update, { immediate: true })
414
492
  v-for="(bar, index) in bars"
415
493
  :key="index"
416
494
  :class="{
417
- 'column-chart__columns__item--highlight': highlighted(bar.datum)
495
+ 'column-chart__columns__item--highlight': highlighted(bar.datum),
496
+ 'column-chart__columns__item--total': bar.isTotal
418
497
  }"
419
498
  :style="{ transform: `translate(${bar.x}px, 0px)` }"
420
499
  class="column-chart__columns__item"
@@ -500,6 +579,10 @@ watch(() => props.socialMode, update, { immediate: true })
500
579
  fill: var(--column-highlight-color, var(--primary, $primary));
501
580
  }
502
581
 
582
+ &--total {
583
+ fill: var(--column-total-color, currentColor);
584
+ }
585
+
503
586
  &__placeholder {
504
587
  opacity: 0;
505
588
 
@@ -22,8 +22,26 @@ const castCall = (fnOrValue = identity, ...rest: any[]) =>
22
22
  export interface LineChartProps {
23
23
  /**
24
24
  * Color of the line. Falls back to theme's dark color.
25
+ * Used for single-series mode (when `keys` is not set).
25
26
  */
26
27
  lineColor?: string | null
28
+ /**
29
+ * Colors for each line when using multi-series mode (`keys`).
30
+ */
31
+ lineColors?: string[]
32
+ /**
33
+ * Field names in data objects for each line series.
34
+ * When set, enables multi-line mode. When empty, falls back to `seriesName`.
35
+ */
36
+ keys?: string[]
37
+ /**
38
+ * Display names for each key in the legend.
39
+ */
40
+ groups?: string[]
41
+ /**
42
+ * Hide the legend (only relevant in multi-line mode).
43
+ */
44
+ hideLegend?: boolean
27
45
  /**
28
46
  * Fixed width for y-axis labels in pixels. If not set, width is calculated automatically.
29
47
  */
@@ -34,6 +52,7 @@ export interface LineChartProps {
34
52
  fixedHeight?: number | null
35
53
  /**
36
54
  * Field name in data objects containing the y-axis value.
55
+ * Used for single-series mode (when `keys` is not set).
37
56
  */
38
57
  seriesName?: string
39
58
  /**
@@ -76,6 +95,10 @@ export interface LineChartProps {
76
95
 
77
96
  const props = withDefaults(defineProps<LineChartProps>(), {
78
97
  lineColor: null,
98
+ lineColors: () => [],
99
+ keys: () => [],
100
+ groups: () => [],
101
+ hideLegend: false,
79
102
  fixedLabelWidth: null,
80
103
  fixedHeight: null,
81
104
  seriesName: 'value',
@@ -95,10 +118,17 @@ const emit = defineEmits<{
95
118
  resized: []
96
119
  }>()
97
120
 
121
+ interface LineSeries {
122
+ key: string
123
+ path: string | null
124
+ color: string | null
125
+ }
126
+
98
127
  const width = ref(0)
99
128
  const height = ref(0)
100
129
  const el = ref<ComponentPublicInstance<HTMLElement> | null>(null)
101
130
  const line = ref<d3.Line<[number, number]> | null>(null)
131
+ const lines = ref<LineSeries[]>([])
102
132
  const isLoaded = ref(false)
103
133
 
104
134
  const {
@@ -109,6 +139,40 @@ const {
109
139
  baseHeightRatio
110
140
  } = useChart(el, getChartProps(props), { emit }, isLoaded, setSizes)
111
141
 
142
+ const isMultiLine = computed(() => props.keys.length > 0)
143
+
144
+ const activeKeys = computed(() => {
145
+ return isMultiLine.value ? props.keys : [props.seriesName]
146
+ })
147
+
148
+ const colorScale = computed(() => {
149
+ return d3
150
+ .scaleOrdinal<string>()
151
+ .domain(activeKeys.value)
152
+ .range(props.lineColors.length ? props.lineColors : d3.schemeCategory10)
153
+ })
154
+
155
+ const highlightedKey = ref<string | null>(null)
156
+
157
+ const hasHighlight = computed(() => highlightedKey.value !== null)
158
+
159
+ function highlight(key: string) {
160
+ highlightedKey.value = key
161
+ }
162
+
163
+ function resetHighlight() {
164
+ highlightedKey.value = null
165
+ }
166
+
167
+ function isHighlighted(key: string) {
168
+ return highlightedKey.value === key
169
+ }
170
+
171
+ function groupName(key: string) {
172
+ const index = props.keys.indexOf(key)
173
+ return props.groups[index] || key
174
+ }
175
+
112
176
  const labelWidth = computed(() => {
113
177
  if (props.fixedLabelWidth) {
114
178
  return props.fixedLabelWidth
@@ -162,10 +226,12 @@ const formattedData = computed(() => {
162
226
  return []
163
227
  }
164
228
  return loadedData.value.map((d: any) => {
165
- // toRaw prevent modifying the Proxy object created with the props.data
166
- let rawD = toRaw(d)
229
+ // Clone to avoid mutating reactive source data (parseTime on already-parsed Date returns null)
230
+ const rawD = { ...toRaw(d) }
167
231
  rawD[props.timeseriesKey] = parseTime(d[props.timeseriesKey])
168
- rawD[props.seriesName] = +d[props.seriesName]
232
+ for (const key of activeKeys.value) {
233
+ rawD[key] = +d[key]
234
+ }
169
235
  return rawD
170
236
  })
171
237
  })
@@ -191,19 +257,34 @@ function update() {
191
257
  scale.value.x.domain(
192
258
  d3.extent(formattedData.value, (d: any) => d[props.timeseriesKey]) as [Date, Date]
193
259
  )
194
- scale.value.y.domain([
195
- 0,
196
- d3.max(formattedData.value, (d: any) => d[props.seriesName]) as number
197
- ])
198
260
 
199
- const points = formattedData.value.map((d: any) => {
200
- return {
261
+ // Y domain covers all series
262
+ const maxY = d3.max(activeKeys.value, (key) => {
263
+ return d3.max(formattedData.value, (d: any) => d[key]) as number
264
+ }) as number
265
+ scale.value.y.domain([0, maxY])
266
+
267
+ if (isMultiLine.value) {
268
+ lines.value = activeKeys.value.map((key) => {
269
+ const points = formattedData.value.map((d: any) => ({
270
+ x: scale.value.x(d[props.timeseriesKey]),
271
+ y: scale.value.y(d[key])
272
+ }))
273
+ return {
274
+ key,
275
+ path: createLine(points as any) as unknown as string,
276
+ color: colorScale.value(key)
277
+ }
278
+ })
279
+ }
280
+ else {
281
+ const points = formattedData.value.map((d: any) => ({
201
282
  x: scale.value.x(d[props.timeseriesKey]),
202
283
  y: scale.value.y(d[props.seriesName])
203
- }
204
- })
284
+ }))
285
+ line.value = createLine(points as any) as any
286
+ }
205
287
 
206
- line.value = createLine(points as any) as any
207
288
  d3.select(el.value)
208
289
  .select('.line-chart__axis--x')
209
290
  .call(
@@ -234,8 +315,31 @@ watchEffect(() => {
234
315
  ref="el"
235
316
  class="line-chart"
236
317
  :style="{ '--line-color': lineColor }"
237
- :class="{ 'line-chart--social-mode': socialMode }"
318
+ :class="{
319
+ 'line-chart--social-mode': socialMode,
320
+ 'line-chart--multi': isMultiLine,
321
+ 'line-chart--has-highlight': hasHighlight
322
+ }"
238
323
  >
324
+ <ul
325
+ v-if="isMultiLine && !hideLegend"
326
+ class="line-chart__legend list-inline"
327
+ >
328
+ <li
329
+ v-for="key in activeKeys"
330
+ :key="key"
331
+ class="line-chart__legend__item list-inline-item d-inline-flex"
332
+ :class="{ 'line-chart__legend__item--highlighted': isHighlighted(key) }"
333
+ @mouseover="highlight(key)"
334
+ @mouseleave="resetHighlight()"
335
+ >
336
+ <span
337
+ class="line-chart__legend__item__box"
338
+ :style="{ 'background-color': colorScale(key) }"
339
+ />
340
+ <span class="line-chart__legend__item__label">{{ groupName(key) }}</span>
341
+ </li>
342
+ </ul>
239
343
  <svg
240
344
  :width="width"
241
345
  :height="height"
@@ -255,7 +359,20 @@ watchEffect(() => {
255
359
  >
256
360
  </g>
257
361
  <g :style="{ transform: `translate(${margin.left}px, ${margin.top}px)` }">
362
+ <template v-if="isMultiLine">
363
+ <path
364
+ v-for="series in lines"
365
+ :key="series.key"
366
+ class="line-chart__line"
367
+ :class="{ 'line-chart__line--highlighted': isHighlighted(series.key) }"
368
+ :d="series.path"
369
+ :style="{ stroke: series.color }"
370
+ @mouseover="highlight(series.key)"
371
+ @mouseleave="resetHighlight()"
372
+ />
373
+ </template>
258
374
  <path
375
+ v-else
259
376
  class="line-chart__line"
260
377
  :d="line"
261
378
  />
@@ -273,6 +390,29 @@ watchEffect(() => {
273
390
  fill: currentColor;
274
391
  }
275
392
 
393
+ &__legend {
394
+ &__item {
395
+ display: inline-flex;
396
+ flex-direction: row;
397
+ align-items: center;
398
+ padding-right: $spacer * 0.5;
399
+ transition: opacity 0.3s, filter 0.3s;
400
+
401
+ .line-chart--has-highlight &:not(&--highlighted) {
402
+ opacity: 0.2;
403
+ filter: grayscale(30%) brightness(10%);
404
+ }
405
+
406
+ &__box {
407
+ height: 1em;
408
+ width: 1em;
409
+ border-radius: 0.5em;
410
+ display: inline-block;
411
+ margin-right: $spacer * 0.5;
412
+ }
413
+ }
414
+ }
415
+
276
416
  &__axis {
277
417
  .domain {
278
418
  display: none;
@@ -291,6 +431,11 @@ watchEffect(() => {
291
431
  fill: none;
292
432
  stroke: var(--line-color, var(--dark, $dark));
293
433
  stroke-width: 3px;
434
+ transition: opacity 0.3s;
435
+
436
+ .line-chart--has-highlight &:not(&--highlighted) {
437
+ opacity: 0.15;
438
+ }
294
439
  }
295
440
  }
296
441
  </style>
@@ -1,15 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import * as d3 from 'd3'
3
- import find from 'lodash/find'
4
3
  import get from 'lodash/get'
5
4
  import identity from 'lodash/identity'
6
5
  import kebabCase from 'lodash/kebabCase'
7
6
  import keysFn from 'lodash/keys'
8
7
  import without from 'lodash/without'
9
8
  import sortByFn from 'lodash/sortBy'
10
- import { ComponentPublicInstance, computed, ref, watch } from 'vue'
9
+ import { ComponentPublicInstance, computed, nextTick, ref, watch } from 'vue'
11
10
  import { getChartProps, useChart } from '@/composables/useChart'
12
- import { useQueryObserver } from '@/composables/useQueryObserver'
13
11
  import { isArray } from 'lodash'
14
12
 
15
13
  defineOptions({
@@ -150,8 +148,6 @@ const {
150
148
  dataHasHighlights
151
149
  } = useChart(el, getChartProps(props), { emit }, isLoaded)
152
150
 
153
- const { querySelectorAll } = useQueryObserver(el.value)
154
-
155
151
  const hasConstraintHeight = computed(() => {
156
152
  return props.fixedHeight !== null || props.socialMode
157
153
  })
@@ -308,12 +304,13 @@ function stackBarAndValue(i: number | string): StackItem[] {
308
304
  }
309
305
 
310
306
  function queryBarAndValue(i: number, key: string) {
311
- if (!mounted.value) {
307
+ const root = el.value as unknown as HTMLElement
308
+ if (!mounted.value || !root) {
312
309
  return {}
313
310
  }
314
311
  const barClass = 'stacked-bar-chart__groups__item__bars__item'
315
312
  const rowSelector = '.stacked-bar-chart__groups__item'
316
- const row = querySelectorAll(rowSelector)[i] as HTMLElement
313
+ const row = root.querySelectorAll(rowSelector)[i] as HTMLElement
317
314
  const normalizedKey = normalizeKey(key)
318
315
  const barSelector = `.${barClass}--${normalizedKey}`
319
316
  const bar = row?.querySelector(barSelector) as HTMLElement
@@ -322,35 +319,59 @@ function queryBarAndValue(i: number, key: string) {
322
319
  return { bar, row, value }
323
320
  }
324
321
 
325
- function hasValueOverflow(i: number | string, key: string) {
326
- try {
327
- const stack = stackBarAndValue(i)
328
- return find(stack, { key })?.overflow
329
- }
330
- catch {
331
- return false
322
+ interface LabelState {
323
+ overflow: boolean
324
+ pushed: boolean
325
+ hidden: boolean
326
+ }
327
+
328
+ const labelStates = ref<Record<string, LabelState>>({})
329
+
330
+ function labelStateKey(i: number | string, key: string) {
331
+ return `${i}-${key}`
332
+ }
333
+
334
+ function computeLabelStates() {
335
+ const root = el.value as unknown as HTMLElement
336
+ if (!root || !mounted.value || !sortedData.value?.length) return
337
+
338
+ const states: Record<string, LabelState> = {}
339
+
340
+ for (let i = 0; i < sortedData.value.length; i++) {
341
+ try {
342
+ const stack = stackBarAndValue(i)
343
+ for (const item of stack) {
344
+ states[labelStateKey(i, item.key)] = {
345
+ overflow: item.overflow,
346
+ pushed: item.pushed,
347
+ hidden: false
348
+ }
349
+ }
350
+ for (let j = 0; j < stack.length; j++) {
351
+ const nextItem = stack[j + 1]
352
+ if (nextItem && stack[j].overflow && nextItem.overflow) {
353
+ states[labelStateKey(i, stack[j].key)].hidden = true
354
+ }
355
+ }
356
+ }
357
+ catch {
358
+ // If measurement fails for a row, skip it
359
+ }
332
360
  }
361
+
362
+ labelStates.value = states
363
+ }
364
+
365
+ function hasValueOverflow(i: number | string, key: string) {
366
+ return labelStates.value[labelStateKey(i, key)]?.overflow ?? false
333
367
  }
334
368
 
335
369
  function hasValuePushed(i: number | string, key: string) {
336
- try {
337
- const stack = stackBarAndValue(i)
338
- return find(stack, { key })?.pushed
339
- }
340
- catch {
341
- return false
342
- }
370
+ return labelStates.value[labelStateKey(i, key)]?.pushed ?? false
343
371
  }
344
372
 
345
373
  function hasValueHidden(i: number | string, key: string) {
346
- const keyIndex = discoveredKeys.value.indexOf(key)
347
- const nextKey = discoveredKeys.value[keyIndex + 1]
348
- if (!nextKey) {
349
- return false
350
- }
351
- const keyC = hasValueOverflow(i, key)
352
- const keyN = hasValueOverflow(i, nextKey)
353
- return keyC && keyN
374
+ return labelStates.value[labelStateKey(i, key)]?.hidden ?? false
354
375
  }
355
376
 
356
377
  function isHidden(i: number | string, key: string) {
@@ -364,6 +385,12 @@ function formatXDatum(d: string) {
364
385
  watch(() => props.highlights, (newHighlights: string[]) => {
365
386
  highlightedKeys.value = newHighlights
366
387
  })
388
+
389
+ // Compute label states after DOM layout
390
+ watch(sortedData, async () => {
391
+ await nextTick()
392
+ computeLabelStates()
393
+ })
367
394
  </script>
368
395
 
369
396
  <template>