Skip to content

Commit a343e18

Browse files
authored
Merge pull request #1910 from visualize-admin/feat/non-symmetrical-error-bar
feat: Add support for showing confidence intervals
2 parents 3ddc292 + 2623209 commit a343e18

31 files changed

+576
-313
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ You can also check the
2222
application will try to use dual-line chart with measures from different
2323
cubes)
2424
- Added tooltip that explains why a given chart type can't be selected
25+
- Added support for confidence intervals
2526
- Fixes
2627
- Introduced a `componentId` concept which makes the dimensions and measures
2728
unique by adding an unversioned cube iri to the unversioned component iri on
@@ -30,6 +31,7 @@ You can also check the
3031
- Map legend is now correctly updated (in some cases it was rendered
3132
incorrectly on the initial render)
3233
- Vertical Axis measure names are now correctly displayed in the left panel
34+
- Uncertainties are now correctly displayed in map symbol layer tooltip
3335
- Performance
3436
- We no longer load non-key dimensions when initializing a chart
3537
- Maintenance

app/charts/chart-config-ui-options.ts

+5
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ type EncodingOption<T extends ChartConfig = ChartConfig> =
111111
| {
112112
field: "showStandardError";
113113
}
114+
| {
115+
field: "showConfidenceInterval";
116+
}
114117
| {
115118
field: "sorting";
116119
}
@@ -643,6 +646,7 @@ const chartConfigOptionsUISpec: ChartSpecs = {
643646
},
644647
options: {
645648
showStandardError: {},
649+
showConfidenceInterval: {},
646650
},
647651
},
648652
{
@@ -776,6 +780,7 @@ const chartConfigOptionsUISpec: ChartSpecs = {
776780
filters: false,
777781
options: {
778782
showStandardError: {},
783+
showConfidenceInterval: {},
779784
},
780785
},
781786
{

app/charts/column/columns-grouped-state-props.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export const useColumnsGroupedStateVariables = (
6262
measuresById,
6363
});
6464
const numericalYErrorVariables = useNumericalYErrorVariables(y, {
65-
numericalYVariables,
65+
getValue: numericalYVariables.getY,
6666
dimensions,
6767
measures,
6868
});

app/charts/column/columns-grouped-state.tsx

+3-13
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,8 @@ const useColumnsGroupedState = (
8484
yMeasure,
8585
getY,
8686
getMinY,
87-
showYStandardError,
88-
yErrorMeasure,
89-
getYError,
9087
getYErrorRange,
88+
getFormattedYUncertainty,
9189
segmentDimension,
9290
segmentsByAbbreviationOrLabel,
9391
getSegment,
@@ -398,14 +396,6 @@ const useColumnsGroupedState = (
398396
topAnchor: !fields.segment,
399397
});
400398

401-
const getError = (d: Observation) => {
402-
if (!showYStandardError || !getYError || getYError(d) == null) {
403-
return;
404-
}
405-
406-
return `${getYError(d)}${yErrorMeasure?.unit ?? ""}`;
407-
};
408-
409399
return {
410400
xAnchor: xAnchorRaw + (placement.x === "right" ? 0.5 : -0.5) * bw,
411401
yAnchor,
@@ -414,15 +404,15 @@ const useColumnsGroupedState = (
414404
datum: {
415405
label: fields.segment && getSegmentAbbreviationOrLabel(datum),
416406
value: yValueFormatter(getY(datum)),
417-
error: getError(datum),
407+
error: getFormattedYUncertainty(datum),
418408
color: colors(getSegment(datum)) as string,
419409
},
420410
values: sortedTooltipValues.map((td) => ({
421411
label: getSegmentAbbreviationOrLabel(td),
422412
value: yMeasure.unit
423413
? `${formatNumber(getY(td))}${yMeasure.unit}`
424414
: formatNumber(getY(td)),
425-
error: getError(td),
415+
error: getFormattedYUncertainty(td),
426416
color: colors(getSegment(td)) as string,
427417
})),
428418
};

app/charts/column/columns-grouped.tsx

+6-7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
import { useChartState } from "@/charts/shared/chart-state";
99
import {
1010
RenderWhiskerDatum,
11-
filterWithoutErrors,
1211
renderContainer,
1312
renderWhiskers,
1413
} from "@/charts/shared/rendering-utils";
@@ -20,24 +19,24 @@ export const ErrorWhiskers = () => {
2019
xScale,
2120
xScaleIn,
2221
getYErrorRange,
23-
getYError,
22+
getYErrorPresent,
2423
yScale,
2524
getSegment,
2625
grouped,
27-
showYStandardError,
26+
showYUncertainty,
2827
} = useChartState() as GroupedColumnsState;
2928
const { margins, width, height } = bounds;
3029
const ref = useRef<SVGGElement>(null);
3130
const enableTransition = useTransitionStore((state) => state.enable);
3231
const transitionDuration = useTransitionStore((state) => state.duration);
3332
const renderData: RenderWhiskerDatum[] = useMemo(() => {
34-
if (!getYErrorRange || !showYStandardError) {
33+
if (!getYErrorRange || !showYUncertainty) {
3534
return [];
3635
}
3736

3837
const bandwidth = xScaleIn.bandwidth();
3938
return grouped
40-
.filter((d) => d[1].some(filterWithoutErrors(getYError)))
39+
.filter((d) => d[1].some(getYErrorPresent))
4140
.flatMap(([segment, observations]) =>
4241
observations.map((d) => {
4342
const x0 = xScaleIn(getSegment(d)) as number;
@@ -56,9 +55,9 @@ export const ErrorWhiskers = () => {
5655
}, [
5756
getSegment,
5857
getYErrorRange,
59-
getYError,
58+
getYErrorPresent,
6059
grouped,
61-
showYStandardError,
60+
showYUncertainty,
6261
xScale,
6362
xScaleIn,
6463
yScale,

app/charts/column/columns-state-props.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export const useColumnsStateVariables = (
5757
measuresById,
5858
});
5959
const numericalYErrorVariables = useNumericalYErrorVariables(y, {
60-
numericalYVariables,
60+
getValue: numericalYVariables.getY,
6161
dimensions,
6262
measures,
6363
});

app/charts/column/columns-state.tsx

+2-13
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,8 @@ const useColumnsState = (
7575
yMeasure,
7676
getY,
7777
getMinY,
78-
showYStandardError,
79-
yErrorMeasure,
80-
getYError,
8178
getYErrorRange,
79+
getFormattedYUncertainty,
8280
} = variables;
8381
const { chartData, scalesData, timeRangeData, paddingData, allData } = data;
8482
const { fields, interactiveFiltersConfig } = chartConfig;
@@ -245,15 +243,6 @@ const useColumnsState = (
245243
formatters[yMeasure.id] ?? formatNumber,
246244
yMeasure.unit
247245
);
248-
249-
const getError = (d: Observation) => {
250-
if (!showYStandardError || !getYError || getYError(d) === null) {
251-
return;
252-
}
253-
254-
return `${getYError(d)}${yErrorMeasure?.unit ?? ""}`;
255-
};
256-
257246
const y = getY(d);
258247

259248
return {
@@ -264,7 +253,7 @@ const useColumnsState = (
264253
datum: {
265254
label: undefined,
266255
value: y !== null && isNaN(y) ? "-" : `${yValueFormatter(getY(d))}`,
267-
error: getError(d),
256+
error: getFormattedYUncertainty(d),
268257
color: "",
269258
},
270259
values: undefined,

app/charts/column/columns.tsx

+6-7
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
import { useChartState } from "@/charts/shared/chart-state";
1010
import {
1111
RenderWhiskerDatum,
12-
filterWithoutErrors,
1312
renderContainer,
1413
renderWhiskers,
1514
} from "@/charts/shared/rendering-utils";
@@ -19,25 +18,25 @@ import { useTheme } from "@/themes";
1918
export const ErrorWhiskers = () => {
2019
const {
2120
getX,
22-
getYError,
21+
getYErrorPresent,
2322
getYErrorRange,
2423
chartData,
2524
yScale,
2625
xScale,
27-
showYStandardError,
26+
showYUncertainty,
2827
bounds,
2928
} = useChartState() as ColumnsState;
3029
const { margins, width, height } = bounds;
3130
const ref = useRef<SVGGElement>(null);
3231
const enableTransition = useTransitionStore((state) => state.enable);
3332
const transitionDuration = useTransitionStore((state) => state.duration);
3433
const renderData: RenderWhiskerDatum[] = useMemo(() => {
35-
if (!getYErrorRange || !showYStandardError) {
34+
if (!getYErrorRange || !showYUncertainty) {
3635
return [];
3736
}
3837

3938
const bandwidth = xScale.bandwidth();
40-
return chartData.filter(filterWithoutErrors(getYError)).map((d, i) => {
39+
return chartData.filter(getYErrorPresent).map((d, i) => {
4140
const x0 = xScale(getX(d)) as number;
4241
const barWidth = Math.min(bandwidth, 15);
4342
const [y1, y2] = getYErrorRange(d);
@@ -53,9 +52,9 @@ export const ErrorWhiskers = () => {
5352
}, [
5453
chartData,
5554
getX,
56-
getYError,
55+
getYErrorPresent,
5756
getYErrorRange,
58-
showYStandardError,
57+
showYUncertainty,
5958
xScale,
6059
yScale,
6160
width,

app/charts/line/lines-state-props.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const useLinesStateVariables = (
5353
measuresById,
5454
});
5555
const numericalYErrorVariables = useNumericalYErrorVariables(y, {
56-
numericalYVariables,
56+
getValue: numericalYVariables.getY,
5757
dimensions,
5858
measures,
5959
});

app/charts/line/lines-state.tsx

+2-12
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,8 @@ const useLinesState = (
8080
getXAsString,
8181
yMeasure,
8282
getY,
83-
showYStandardError,
84-
getYError,
8583
getYErrorRange,
86-
yErrorMeasure,
84+
getFormattedYUncertainty,
8785
getMinY,
8886
segmentDimension,
8987
segmentsByAbbreviationOrLabel,
@@ -279,14 +277,6 @@ const useLinesState = (
279277
yMeasure.unit
280278
);
281279

282-
const getError = (d: Observation) => {
283-
if (!showYStandardError || !getYError || getYError(d) === null) {
284-
return;
285-
}
286-
287-
return `${getYError(d)}${yErrorMeasure?.unit ?? ""}`;
288-
};
289-
290280
return {
291281
xAnchor,
292282
yAnchor,
@@ -295,7 +285,7 @@ const useLinesState = (
295285
datum: {
296286
label: fields.segment && getSegmentAbbreviationOrLabel(datum),
297287
value: yValueFormatter(getY(datum)),
298-
error: getError(datum),
288+
error: getFormattedYUncertainty(datum),
299289
color: colors(getSegment(datum)) as string,
300290
},
301291
values: sortedTooltipValues.map((td) => ({

app/charts/line/lines.tsx

+10-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { LinesState } from "@/charts/line/lines-state";
55
import { useChartState } from "@/charts/shared/chart-state";
66
import {
77
RenderWhiskerDatum,
8-
filterWithoutErrors,
98
renderContainer,
109
renderWhiskers,
1110
} from "@/charts/shared/rendering-utils";
@@ -15,12 +14,13 @@ import { useTransitionStore } from "@/stores/transition";
1514
export const ErrorWhiskers = () => {
1615
const {
1716
getX,
18-
getYError,
17+
getY,
18+
getYErrorPresent,
1919
getYErrorRange,
2020
chartData,
2121
yScale,
2222
xScale,
23-
showYStandardError,
23+
showYUncertainty,
2424
colors,
2525
getSegment,
2626
bounds,
@@ -30,18 +30,20 @@ export const ErrorWhiskers = () => {
3030
const enableTransition = useTransitionStore((state) => state.enable);
3131
const transitionDuration = useTransitionStore((state) => state.duration);
3232
const renderData: RenderWhiskerDatum[] = useMemo(() => {
33-
if (!getYErrorRange || !showYStandardError) {
33+
if (!getYErrorRange || !showYUncertainty) {
3434
return [];
3535
}
3636

37-
return chartData.filter(filterWithoutErrors(getYError)).map((d, i) => {
37+
return chartData.filter(getYErrorPresent).map((d, i) => {
3838
const x0 = xScale(getX(d)) as number;
3939
const segment = getSegment(d);
4040
const barWidth = 15;
41+
const y = getY(d) as number;
4142
const [y1, y2] = getYErrorRange(d);
4243
return {
4344
key: `${i}`,
4445
x: x0 - barWidth / 2,
46+
y: yScale(y),
4547
y1: yScale(y1),
4648
y2: yScale(y2),
4749
width: barWidth,
@@ -54,9 +56,10 @@ export const ErrorWhiskers = () => {
5456
colors,
5557
getSegment,
5658
getX,
57-
getYError,
59+
getY,
60+
getYErrorPresent,
5861
getYErrorRange,
59-
showYStandardError,
62+
showYUncertainty,
6063
xScale,
6164
yScale,
6265
]);

0 commit comments

Comments
 (0)