@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.
- package/dist/lib/lib/datavisualisations/ColumnChart/ColumnChart.vue.d.ts +17 -0
- package/dist/lib/lib/datavisualisations/LineChart/LineChart.vue.d.ts +19 -0
- package/dist/lib/murmur.css +1 -1
- package/dist/lib/murmur.js +10419 -10307
- package/dist/lib/murmur.js.map +1 -1
- package/dist/lib/murmur.umd.cjs +23 -23
- 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/config.default.ts +0 -0
- package/lib/datavisualisations/ColumnChart/ColumnChart.vue +97 -14
- package/lib/datavisualisations/LineChart/LineChart.vue +158 -13
- package/lib/datavisualisations/StackedBarChart/StackedBarChart.vue +56 -29
- package/lib/datavisualisations/StackedColumnChart/StackedColumnChart.vue +59 -30
- 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
|
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,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
|
-
|
|
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) =>
|
|
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
|
-
//
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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="{
|
|
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
|
-
|
|
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>
|