Skip to content

Commit e8ab3f9

Browse files
authored
Merge pull request #2186 from visualize-admin/feat/add-limit-symbols
feat: Add limit symbols
2 parents 2ba6163 + f2b33a1 commit e8ab3f9

File tree

13 files changed

+256
-46
lines changed

13 files changed

+256
-46
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ You can also check the
1717
go beyond the data range
1818
- Limits are now also rendered when the axis dimension is a single filter
1919
- It's now possible to display limits in map charts
20+
- Added a way to set different limit symbols for area and line charts
2021
- Interactive filters can now be set to be opened by default
2122
- Statistics page is now linked in the footer
2223
- Fixes

app/charts/index.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ascending, descending, group, rollup, rollups } from "d3-array";
33
import produce from "immer";
44
import get from "lodash/get";
55
import sortBy from "lodash/sortBy";
6+
import mapValues from "lodash/mapValues";
67

78
import {
89
AREA_SEGMENT_SORTING,
@@ -995,7 +996,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
995996
},
996997
limits: ({ oldValue, newChartConfig }) => {
997998
return produce(newChartConfig, (draft) => {
998-
draft.limits = oldValue;
999+
draft.limits = mapValues(oldValue, (limits) =>
1000+
limits.map(({ symbolType, ...rest }) => rest)
1001+
);
9991002
});
10001003
},
10011004
fields: {
@@ -1113,7 +1116,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
11131116
},
11141117
limits: ({ oldValue, newChartConfig }) => {
11151118
return produce(newChartConfig, (draft) => {
1116-
draft.limits = oldValue;
1119+
draft.limits = mapValues(oldValue, (limits) =>
1120+
limits.map(({ symbolType, ...rest }) => rest)
1121+
);
11171122
});
11181123
},
11191124
fields: {
@@ -1240,7 +1245,12 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
12401245
},
12411246
limits: ({ oldValue, newChartConfig }) => {
12421247
return produce(newChartConfig, (draft) => {
1243-
draft.limits = oldValue;
1248+
draft.limits = mapValues(oldValue, (limits) =>
1249+
limits.map(({ symbolType = "circle", ...rest }) => ({
1250+
...rest,
1251+
symbolType,
1252+
}))
1253+
);
12441254
});
12451255
},
12461256
fields: {
@@ -1339,7 +1349,12 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
13391349
},
13401350
limits: ({ oldValue, newChartConfig }) => {
13411351
return produce(newChartConfig, (draft) => {
1342-
draft.limits = oldValue;
1352+
draft.limits = mapValues(oldValue, (limits) =>
1353+
limits.map(({ symbolType = "circle", ...rest }) => ({
1354+
...rest,
1355+
symbolType,
1356+
}))
1357+
);
13431358
});
13441359
},
13451360
fields: {
@@ -1450,7 +1465,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
14501465
},
14511466
limits: ({ oldValue, newChartConfig }) => {
14521467
return produce(newChartConfig, (draft) => {
1453-
draft.limits = oldValue;
1468+
draft.limits = mapValues(oldValue, (limits) =>
1469+
limits.map(({ symbolType, ...rest }) => rest)
1470+
);
14541471
});
14551472
},
14561473
fields: {
@@ -1524,7 +1541,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
15241541
},
15251542
limits: ({ oldValue, newChartConfig }) => {
15261543
return produce(newChartConfig, (draft) => {
1527-
draft.limits = oldValue;
1544+
draft.limits = mapValues(oldValue, (limits) =>
1545+
limits.map(({ symbolType, ...rest }) => rest)
1546+
);
15281547
});
15291548
},
15301549
fields: {
@@ -1645,7 +1664,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
16451664
},
16461665
limits: ({ oldValue, newChartConfig }) => {
16471666
return produce(newChartConfig, (draft) => {
1648-
draft.limits = oldValue;
1667+
draft.limits = mapValues(oldValue, (limits) =>
1668+
limits.map(({ symbolType, ...rest }) => rest)
1669+
);
16491670
});
16501671
},
16511672
fields: {
@@ -1695,7 +1716,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
16951716
},
16961717
limits: ({ oldValue, newChartConfig }) => {
16971718
return produce(newChartConfig, (draft) => {
1698-
draft.limits = oldValue;
1719+
draft.limits = mapValues(oldValue, (limits) =>
1720+
limits.map(({ symbolType, ...rest }) => rest)
1721+
);
16991722
});
17001723
},
17011724
fields: {
@@ -1766,7 +1789,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
17661789
},
17671790
limits: ({ oldValue, newChartConfig }) => {
17681791
return produce(newChartConfig, (draft) => {
1769-
draft.limits = oldValue;
1792+
draft.limits = mapValues(oldValue, (limits) =>
1793+
limits.map(({ symbolType, ...rest }) => rest)
1794+
);
17701795
});
17711796
},
17721797
fields: {
@@ -1887,7 +1912,9 @@ const chartConfigsAdjusters: ChartConfigsAdjusters = {
18871912
},
18881913
limits: ({ oldValue, newChartConfig }) => {
18891914
return produce(newChartConfig, (draft) => {
1890-
draft.limits = oldValue;
1915+
draft.limits = mapValues(oldValue, (limits) =>
1916+
limits.map(({ symbolType, ...rest }) => rest)
1917+
);
18911918
});
18921919
},
18931920
fields: {

app/charts/shared/legend-color.tsx

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ import { useChartInteractiveFilters } from "@/stores/interactive-filters";
4040
import { interlace } from "@/utils/interlace";
4141
import { makeDimensionValueSorters } from "@/utils/sorting-values";
4242
import useEvent from "@/utils/use-event";
43+
import { Icon } from "@/icons";
4344

4445
import { DimensionsById } from "./ChartProps";
4546

46-
export type LegendSymbol = "square" | "line" | "circle";
47+
export type LegendSymbol = "square" | "line" | "circle" | "cross";
4748

4849
type LegendItemUsage = "legend" | "tooltip" | "colorPicker";
4950

@@ -99,7 +100,7 @@ const useItemStyles = makeStyles<Theme, ItemStyleProps>((theme) => {
99100
"&::before": {
100101
content: "''",
101102
position: "relative",
102-
display: "block",
103+
display: ({ symbol }) => (symbol === "cross" ? "none" : "block"),
103104
width: `calc(0.5rem * var(--size-adjust, 1))`,
104105
height: ({ symbol }) =>
105106
`calc(${["square", "circle"].includes(symbol) ? "0.5rem" : "2px"} * var(--size-adjust, 1))`,
@@ -221,6 +222,7 @@ export const LegendColor = memo(function LegendColor({
221222
? [measureLimit.value]
222223
: [measureLimit.from, measureLimit.to],
223224
color: configLimit.color,
225+
symbol: !configLimit.symbolType ? "line" : configLimit.symbolType,
224226
}))}
225227
getColor={colors}
226228
getLabel={getColorLabel}
@@ -312,7 +314,12 @@ const LegendColorContent = ({
312314
numberOfOptions,
313315
}: {
314316
groups: ReturnType<typeof useLegendGroups>;
315-
limits?: { label: string; values: number[]; color: string }[];
317+
limits?: {
318+
label: string;
319+
values: number[];
320+
color: string;
321+
symbol: LegendSymbol;
322+
}[];
316323
getColor: (d: string) => string;
317324
getLabel: (d: string) => string;
318325
getItemDimension?: (dimensionLabel: string) => Measure | undefined;
@@ -391,16 +398,17 @@ const LegendColorContent = ({
391398
onToggle={handleToggle}
392399
checked={interactive && active}
393400
disabled={soleItemChecked && active}
401+
usage="legend"
394402
/>
395403
);
396404
})}
397405
{isLastGroup && limits
398-
? limits.map(({ label, values, color }, i) => (
406+
? limits.map(({ label, values, color, symbol }, i) => (
399407
<LegendItem
400408
key={i}
401409
item={`${label}: ${values.map(formatNumber).join("-")}`}
402410
color={color}
403-
symbol="line"
411+
symbol={symbol}
404412
/>
405413
))
406414
: null}
@@ -434,10 +442,12 @@ export const LegendItem = (props: LegendItemProps) => {
434442
onToggle,
435443
checked,
436444
disabled,
437-
usage = "legend",
445+
usage: _usage,
438446
} = props;
447+
const usage = _usage ?? "legend";
439448
const classes = useItemStyles({ symbol, color, usage });
440-
const shouldBeBigger = symbol === "circle" || usage === "colorPicker";
449+
const shouldBeBigger =
450+
(symbol === "circle" && _usage !== "legend") || usage === "colorPicker";
441451

442452
return interactive && onToggle ? (
443453
<MaybeTooltip
@@ -472,7 +482,14 @@ export const LegendItem = (props: LegendItemProps) => {
472482
<Flex
473483
data-testid="legendItem"
474484
className={clsx(classes.legendItem, shouldBeBigger && classes.bigger)}
485+
sx={{
486+
alignItems: symbol === "cross" ? "center !important" : "flex-start",
487+
}}
475488
>
489+
{/* TODO: Use icons instead of ::before when migrating to new CI / CD */}
490+
{symbol === "cross" ? (
491+
<Icon size={16} name="close" color={color} />
492+
) : null}
476493
{dimension ? (
477494
<OpenMetadataPanelWrapper component={dimension}>
478495
{/* Account for the added space, to align the symbol and label. */}

app/charts/shared/limits.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export const VerticalLimits = ({
160160
const y2 = yScale(limit.y2);
161161
const fill = limit.color;
162162
const lineType = limit.lineType;
163+
const symbolType = limit.symbolType;
163164

164165
const axisObservation: Observation = {
165166
[axisDimension?.id ?? ""]: limit.relatedAxisDimensionValueLabel ?? "",
@@ -181,6 +182,7 @@ export const VerticalLimits = ({
181182
width,
182183
fill,
183184
lineType,
185+
symbolType,
184186
} as RenderVerticalLimitDatum)
185187
: null;
186188
})

app/charts/shared/rendering-utils.ts

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,24 @@ export type RenderVerticalLimitDatum = {
160160
width: number;
161161
fill: string;
162162
lineType: Limit["lineType"];
163+
symbolType: Limit["symbolType"];
164+
};
165+
166+
const getTopBottomLineHeight = (d: RenderVerticalLimitDatum) => {
167+
return d.symbolType
168+
? d.symbolType === "cross"
169+
? LIMIT_SIZE
170+
: 0
171+
: LIMIT_SIZE;
172+
};
173+
const getTopRotate = (d: RenderVerticalLimitDatum) => {
174+
return d.symbolType === "cross" ? "rotate(45deg)" : "rotate(0deg)";
175+
};
176+
const getBottomRotate = (d: RenderVerticalLimitDatum) => {
177+
return d.symbolType === "cross" ? "rotate(-45deg)" : "rotate(0deg)";
178+
};
179+
const getMiddleRadius = (d: RenderVerticalLimitDatum) => {
180+
return d.symbolType === "circle" ? LIMIT_SIZE * 1.5 : 0;
163181
};
164182

165183
export const renderVerticalLimits = (
@@ -183,14 +201,17 @@ export const renderVerticalLimits = (
183201
.attr("x", (d) => d.x)
184202
.attr("y", (d) => d.y2 - LIMIT_SIZE / 2)
185203
.attr("width", (d) => d.width)
186-
.attr("height", LIMIT_SIZE)
204+
.attr("height", getTopBottomLineHeight)
187205
.attr("fill", (d) => d.fill)
188206
.attr("stroke", "none")
207+
.style("transform-box", "fill-box")
208+
.style("transform-origin", "center")
209+
.style("transform", getTopRotate)
189210
)
190211
.call((g) =>
191212
g
192213
.append("line")
193-
.attr("class", "middle")
214+
.attr("class", "middle-line")
194215
.attr("x1", (d) => d.x + d.width / 2)
195216
.attr("x2", (d) => d.x + d.width / 2)
196217
.attr("y1", (d) => d.y1 - LIMIT_SIZE / 2)
@@ -201,16 +222,28 @@ export const renderVerticalLimits = (
201222
d.lineType === "dashed" ? "3 3" : "none"
202223
)
203224
)
225+
.call((g) =>
226+
g
227+
.append("circle")
228+
.attr("class", "middle-dot")
229+
.attr("cx", (d) => d.x + d.width / 2)
230+
.attr("cy", (d) => (d.y1 + d.y2) / 2)
231+
.attr("r", getMiddleRadius)
232+
.attr("fill", (d) => d.fill)
233+
)
204234
.call((g) =>
205235
g
206236
.append("rect")
207237
.attr("class", "bottom")
208238
.attr("x", (d) => d.x)
209239
.attr("y", (d) => d.y1 - LIMIT_SIZE / 2)
210240
.attr("width", (d) => d.width)
211-
.attr("height", LIMIT_SIZE)
241+
.attr("height", getTopBottomLineHeight)
212242
.attr("fill", (d) => d.fill)
213243
.attr("stroke", "none")
244+
.style("transform-box", "fill-box")
245+
.style("transform-origin", "center")
246+
.style("transform", getBottomRotate)
214247
)
215248
.call((enter) =>
216249
maybeTransition(enter, {
@@ -229,11 +262,13 @@ export const renderVerticalLimits = (
229262
.attr("x", (d) => d.x)
230263
.attr("y", (d) => d.y2 - LIMIT_SIZE / 2)
231264
.attr("width", (d) => d.width)
265+
.attr("height", getTopBottomLineHeight)
232266
.attr("fill", (d) => d.fill)
267+
.style("transform", getTopRotate)
233268
)
234269
.call((g) =>
235270
g
236-
.select(".middle")
271+
.select(".middle-line")
237272
.attr("x1", (d) => d.x + d.width / 2)
238273
.attr("x2", (d) => d.x + d.width / 2)
239274
.attr("y1", (d) => d.y1 - LIMIT_SIZE / 2)
@@ -244,13 +279,23 @@ export const renderVerticalLimits = (
244279
d.lineType === "dashed" ? "3 3" : "none"
245280
)
246281
)
282+
.call((g) =>
283+
g
284+
.select(".middle-dot")
285+
.attr("cx", (d) => d.x + d.width / 2)
286+
.attr("cy", (d) => (d.y1 + d.y2) / 2)
287+
.attr("r", getMiddleRadius)
288+
.attr("fill", (d) => d.fill)
289+
)
247290
.call((g) =>
248291
g
249292
.select(".bottom")
250293
.attr("x", (d) => d.x)
251294
.attr("y", (d) => d.y1 - LIMIT_SIZE / 2)
252295
.attr("width", (d) => d.width)
296+
.attr("height", getTopBottomLineHeight)
253297
.attr("fill", (d) => d.fill)
298+
.style("transform", getBottomRotate)
254299
),
255300
transition,
256301
}),

app/config-types.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -278,16 +278,21 @@ const Cube = t.intersection([
278278
]);
279279
export type Cube = t.TypeOf<typeof Cube>;
280280

281-
const Limit = t.type({
282-
related: t.array(
283-
t.type({
284-
dimensionId: t.string,
285-
dimensionValue: t.string,
286-
})
287-
),
288-
color: t.string,
289-
lineType: t.union([t.literal("dashed"), t.literal("solid")]),
290-
});
281+
const Limit = t.intersection([
282+
t.type({
283+
related: t.array(
284+
t.type({
285+
dimensionId: t.string,
286+
dimensionValue: t.string,
287+
})
288+
),
289+
color: t.string,
290+
lineType: t.union([t.literal("dashed"), t.literal("solid")]),
291+
}),
292+
t.partial({
293+
symbolType: t.union([t.literal("cross"), t.literal("circle")]),
294+
}),
295+
]);
291296
export type Limit = t.TypeOf<typeof Limit>;
292297

293298
const GenericChartConfig = t.type({

0 commit comments

Comments
 (0)