@icij/murmur-next 4.7.4 → 4.8.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/dist/lib/lib/datavisualisations/ColumnChart/ColumnChart.vue.d.ts +17 -0
- package/dist/lib/murmur.css +1 -1
- package/dist/lib/murmur.js +10167 -10116
- package/dist/lib/murmur.js.map +1 -1
- package/dist/lib/murmur.umd.cjs +22 -22
- package/dist/lib/murmur.umd.cjs.map +1 -1
- package/lib/assets/images/icij-full-white.svg +0 -0
- package/lib/assets/images/icij-full.svg +0 -0
- package/lib/assets/images/icij.png +0 -0
- package/lib/assets/images/icij.svg +0 -0
- package/lib/assets/images/icij@2x.png +0 -0
- package/lib/assets/images/murmur-dark.png +0 -0
- package/lib/assets/images/murmur-dark.svg +0 -0
- package/lib/assets/images/murmur-textured.png +0 -0
- package/lib/assets/images/murmur-white.png +0 -0
- package/lib/assets/images/murmur-white.svg +0 -0
- package/lib/components/App/AppIcon.vue +17 -1
- package/lib/config.default.ts +0 -0
- package/lib/datavisualisations/ColumnChart/ColumnChart.vue +90 -12
- package/lib/datavisualisations/StackedBarChart/StackedBarChart.vue +56 -29
- package/lib/datavisualisations/StackedColumnChart/StackedColumnChart.vue +58 -29
- package/lib/keys.ts +0 -0
- package/lib/locales/fr.json +0 -0
- package/lib/styles/lib.scss +0 -0
- package/lib/styles/utilities.scss +0 -0
- package/lib/styles/variables_dark.scss +0 -0
- package/lib/types/d3-geo-projection.d.ts +0 -0
- package/lib/types/querystring-es3.d.ts +0 -0
- package/lib/types/shims-bootstrap-vue.d.ts +0 -0
- package/lib/utils/clipboard.ts +0 -0
- 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
|
|
@@ -97,8 +97,12 @@ const color = computed(() => {
|
|
|
97
97
|
return colorVariant
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
+
const isPercentSize = computed(() => {
|
|
101
|
+
return typeof props.size === 'string' && props.size.endsWith('%')
|
|
102
|
+
})
|
|
103
|
+
|
|
100
104
|
const isRawSize = computed(() => {
|
|
101
|
-
return !['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', undefined].includes(props.size)
|
|
105
|
+
return !['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', undefined].includes(props.size) && !isPercentSize.value
|
|
102
106
|
})
|
|
103
107
|
|
|
104
108
|
const hasSize = computed(() => {
|
|
@@ -109,6 +113,7 @@ const style = computed(() => {
|
|
|
109
113
|
return {
|
|
110
114
|
'--app-icon-color': color.value,
|
|
111
115
|
'--app-icon-raw-size': isRawSize.value ? props.size : undefined,
|
|
116
|
+
'--app-icon-percent-size': isPercentSize.value ? props.size : undefined,
|
|
112
117
|
'--app-icon-size': hasSize.value ? props.size : undefined,
|
|
113
118
|
'--app-icon-scale': props.scale ?? 1,
|
|
114
119
|
'--app-icon-spin-duration': props.spinDuration,
|
|
@@ -122,6 +127,7 @@ const classList = computed(() => {
|
|
|
122
127
|
[`app-icon--size-${props.size}`]: hasSize.value,
|
|
123
128
|
[`app-icon--has-size`]: hasSize.value,
|
|
124
129
|
[`app-icon--raw-size`]: isRawSize.value,
|
|
130
|
+
[`app-icon--percent-size`]: isPercentSize.value,
|
|
125
131
|
[`app-icon--hover`]: currentHover.value,
|
|
126
132
|
[`app-icon--spin`]: props.spin,
|
|
127
133
|
[`app-icon--spin-reverse`]: props.spinReverse,
|
|
@@ -171,6 +177,16 @@ const classList = computed(() => {
|
|
|
171
177
|
font-size: var(--app-icon-raw-size);
|
|
172
178
|
}
|
|
173
179
|
|
|
180
|
+
&--percent-size {
|
|
181
|
+
width: var(--app-icon-percent-size);
|
|
182
|
+
height: auto;
|
|
183
|
+
|
|
184
|
+
:deep(svg) {
|
|
185
|
+
width: 100%;
|
|
186
|
+
height: auto;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
174
190
|
&--spin :deep(svg) {
|
|
175
191
|
animation: app-icon-spin var(--app-icon-spin-duration, 1s) linear infinite;
|
|
176
192
|
}
|
package/lib/config.default.ts
CHANGED
|
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(
|
|
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
|
-
|
|
257
|
-
|
|
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:
|
|
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,9 +364,14 @@ 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
|
-
|
|
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> => {
|
|
@@ -383,11 +454,13 @@ watch(() => props.socialMode, update, { immediate: true })
|
|
|
383
454
|
'column-chart--hover': hover,
|
|
384
455
|
'column-chart--stripped': stripped,
|
|
385
456
|
'column-chart--social-mode': socialMode,
|
|
386
|
-
'column-chart--loaded': isLoaded
|
|
457
|
+
'column-chart--loaded': isLoaded,
|
|
458
|
+
'column-chart--waterfall': waterfall
|
|
387
459
|
}"
|
|
388
460
|
:style="{
|
|
389
461
|
'--column-color': columnColor,
|
|
390
|
-
'--column-highlight-color': columnHighlightColor
|
|
462
|
+
'--column-highlight-color': columnHighlightColor,
|
|
463
|
+
'--column-total-color': waterfallTotalColor
|
|
391
464
|
}"
|
|
392
465
|
class="column-chart"
|
|
393
466
|
>
|
|
@@ -414,7 +487,8 @@ watch(() => props.socialMode, update, { immediate: true })
|
|
|
414
487
|
v-for="(bar, index) in bars"
|
|
415
488
|
:key="index"
|
|
416
489
|
:class="{
|
|
417
|
-
'column-chart__columns__item--highlight': highlighted(bar.datum)
|
|
490
|
+
'column-chart__columns__item--highlight': highlighted(bar.datum),
|
|
491
|
+
'column-chart__columns__item--total': bar.isTotal
|
|
418
492
|
}"
|
|
419
493
|
:style="{ transform: `translate(${bar.x}px, 0px)` }"
|
|
420
494
|
class="column-chart__columns__item"
|
|
@@ -500,6 +574,10 @@ watch(() => props.socialMode, update, { immediate: true })
|
|
|
500
574
|
fill: var(--column-highlight-color, var(--primary, $primary));
|
|
501
575
|
}
|
|
502
576
|
|
|
577
|
+
&--total {
|
|
578
|
+
fill: var(--column-total-color, currentColor);
|
|
579
|
+
}
|
|
580
|
+
|
|
503
581
|
&__placeholder {
|
|
504
582
|
opacity: 0;
|
|
505
583
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import * as d3 from 'd3'
|
|
3
3
|
import keysFn from 'lodash/keys'
|
|
4
|
-
import find from 'lodash/find'
|
|
5
4
|
import get from 'lodash/get'
|
|
6
5
|
import identity from 'lodash/identity'
|
|
7
6
|
import sortByFn from 'lodash/sortBy'
|
|
@@ -15,7 +14,6 @@ import {
|
|
|
15
14
|
watch
|
|
16
15
|
} from 'vue'
|
|
17
16
|
import { getChartProps, useChart } from '@/composables/useChart'
|
|
18
|
-
import { useQueryObserver } from '@/composables/useQueryObserver'
|
|
19
17
|
|
|
20
18
|
defineOptions({
|
|
21
19
|
name: 'StackedColumnChart'
|
|
@@ -157,7 +155,6 @@ const highlightedKeys = ref(props.highlights)
|
|
|
157
155
|
const highlightTimeout = ref<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
158
156
|
const isLoaded = ref(false)
|
|
159
157
|
const el = ref<ComponentPublicInstance<HTMLElement> | null>(null)
|
|
160
|
-
const { querySelector, querySelectorAll } = useQueryObserver(el.value)
|
|
161
158
|
|
|
162
159
|
const {
|
|
163
160
|
elementsMaxBBox,
|
|
@@ -383,11 +380,12 @@ function stackBarAndValue(i: string | number): StackItem[] {
|
|
|
383
380
|
}
|
|
384
381
|
|
|
385
382
|
function queryBarAndValue(i: number, key: string) {
|
|
386
|
-
|
|
383
|
+
const root = el.value as unknown as HTMLElement
|
|
384
|
+
if (!mounted.value || !root) {
|
|
387
385
|
return {}
|
|
388
386
|
}
|
|
389
387
|
const rowSelector = '.stacked-column-chart__groups__item'
|
|
390
|
-
const row = querySelectorAll(rowSelector)[i] as HTMLElement
|
|
388
|
+
const row = root.querySelectorAll(rowSelector)[i] as HTMLElement
|
|
391
389
|
const barSelector = `.stacked-column-chart__groups__item__bars__item--${key}`
|
|
392
390
|
const bar = row.querySelector(barSelector) as HTMLElement
|
|
393
391
|
const valueSelector = '.stacked-column-chart__groups__item__bars__item__value'
|
|
@@ -399,33 +397,60 @@ function isHidden(i: string | number, key: string) {
|
|
|
399
397
|
return props.hideEmptyValues && !sortedData.value[i as number][key]
|
|
400
398
|
}
|
|
401
399
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
400
|
+
interface LabelState {
|
|
401
|
+
overflow: boolean
|
|
402
|
+
pushed: boolean
|
|
403
|
+
hidden: boolean
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const labelStates = ref<Record<string, LabelState>>({})
|
|
407
|
+
|
|
408
|
+
function labelStateKey(i: number | string, key: string) {
|
|
409
|
+
return `${i}-${key}`
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function computeLabelStates() {
|
|
413
|
+
const root = el.value as unknown as HTMLElement
|
|
414
|
+
if (!root || !mounted.value || !sortedData.value?.length) return
|
|
415
|
+
|
|
416
|
+
const states: Record<string, LabelState> = {}
|
|
417
|
+
|
|
418
|
+
for (let i = 0; i < sortedData.value.length; i++) {
|
|
419
|
+
try {
|
|
420
|
+
const stack = stackBarAndValue(i)
|
|
421
|
+
for (const item of stack) {
|
|
422
|
+
states[labelStateKey(i, item.key)] = {
|
|
423
|
+
overflow: item.overflow,
|
|
424
|
+
pushed: item.pushed,
|
|
425
|
+
hidden: false
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// A value is hidden when both it and the next key overflow
|
|
429
|
+
for (let j = 0; j < stack.length; j++) {
|
|
430
|
+
const nextItem = stack[j + 1]
|
|
431
|
+
if (nextItem && stack[j].overflow && nextItem.overflow) {
|
|
432
|
+
states[labelStateKey(i, stack[j].key)].hidden = true
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// If measurement fails for a row, skip it
|
|
438
|
+
}
|
|
409
439
|
}
|
|
440
|
+
|
|
441
|
+
labelStates.value = states
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function hasValueOverflow(i: string | number, key: string) {
|
|
445
|
+
return labelStates.value[labelStateKey(i, key)]?.overflow ?? false
|
|
410
446
|
}
|
|
411
447
|
|
|
412
448
|
function hasValuePushed(i: string | number, key: string) {
|
|
413
|
-
|
|
414
|
-
const stack = stackBarAndValue(i)
|
|
415
|
-
return find(stack, { key })?.pushed
|
|
416
|
-
}
|
|
417
|
-
catch {
|
|
418
|
-
return false
|
|
419
|
-
}
|
|
449
|
+
return labelStates.value[labelStateKey(i, key)]?.pushed ?? false
|
|
420
450
|
}
|
|
421
451
|
|
|
422
452
|
function hasValueHidden(i: string | number, key: string) {
|
|
423
|
-
|
|
424
|
-
const nextKey = discoveredKeys.value[keyIndex + 1]
|
|
425
|
-
if (!nextKey) {
|
|
426
|
-
return false
|
|
427
|
-
}
|
|
428
|
-
return hasValueOverflow(i, key) && hasValueOverflow(i, nextKey)
|
|
453
|
+
return labelStates.value[labelStateKey(i, key)]?.hidden ?? false
|
|
429
454
|
}
|
|
430
455
|
|
|
431
456
|
function formatXDatum(d: string) {
|
|
@@ -445,13 +470,17 @@ watch(
|
|
|
445
470
|
|
|
446
471
|
watch(sortedData, async () => {
|
|
447
472
|
await nextTick()
|
|
473
|
+
const root = el.value as unknown as HTMLElement
|
|
474
|
+
if (!root) return
|
|
448
475
|
// This must be set after the column have been rendered
|
|
449
|
-
const element = querySelector('.stacked-column-chart__groups__item__bars')
|
|
476
|
+
const element = root.querySelector('.stacked-column-chart__groups__item__bars') as HTMLElement
|
|
450
477
|
// Update the left axis only if the bars exists
|
|
451
478
|
if (element) {
|
|
452
|
-
leftAxisHeight.value =
|
|
479
|
+
leftAxisHeight.value = element.offsetHeight
|
|
453
480
|
leftAxisCanvas.value.call(leftAxis.value as any)
|
|
454
481
|
}
|
|
482
|
+
// Compute label overflow/pushed/hidden states after DOM layout
|
|
483
|
+
computeLabelStates()
|
|
455
484
|
})
|
|
456
485
|
</script>
|
|
457
486
|
|
|
@@ -484,8 +513,8 @@ watch(sortedData, async () => {
|
|
|
484
513
|
<span
|
|
485
514
|
class="stacked-column-chart__legend__item__box"
|
|
486
515
|
:style="{ 'background-color': colorScale(key) }"
|
|
487
|
-
|
|
488
|
-
|
|
516
|
+
/>
|
|
517
|
+
<span class="stacked-column-chart__legend__item__label">{{ groupName(key) }}</span>
|
|
489
518
|
</li>
|
|
490
519
|
</ul>
|
|
491
520
|
<div class="d-flex flex-grow-1 position-relative overflow-hidden">
|
package/lib/keys.ts
CHANGED
|
File without changes
|
package/lib/locales/fr.json
CHANGED
|
File without changes
|
package/lib/styles/lib.scss
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/lib/utils/clipboard.ts
CHANGED
|
File without changes
|