Skip to content

Commit 5a8300d

Browse files
authored
Merge pull request #2486 from visualize-admin/feat/links-in-tables
feat: Links in table charts
2 parents 00cefb9 + 36c4e6c commit 5a8300d

File tree

22 files changed

+748
-133
lines changed

22 files changed

+748
-133
lines changed

CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,21 @@ You can also check the
1111

1212
## Unreleased
1313

14-
Nothing yet.
14+
- Features
15+
- Added a way to create a link column in table charts
1516

1617
### 6.1.3 - 2025-11-12
1718

1819
- Features
1920
- Added dynamic iframe resize support for LivingDocs
20-
- Fix
21+
- Fixes
2122
- Clicking on Y axis title in bar charts now open Data tab in the details
2223
panels, as in other chart types
2324
- Fixed parsing problems with API routes
2425

2526
### 6.1.2 - 2025-10-22
2627

27-
- Fix
28+
- Fixes
2829
- Correctly typed DataSourceUrl variable when querying version history of a
2930
cube
3031

app/charts/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,12 @@ export const getInitialConfig = (
617617
showSearch: true,
618618
showAllRows: false,
619619
},
620+
links: {
621+
enabled: false,
622+
baseUrl: "",
623+
componentId: "",
624+
targetComponentId: "",
625+
},
620626
sorting: [],
621627
fields: Object.fromEntries<TableColumn>(
622628
allDimensionsSorted.map((d, i) => [

app/charts/table/cell-desktop.tsx

Lines changed: 50 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { hcl } from "d3-color";
44
import { ScaleLinear } from "d3-scale";
55
import { Cell } from "react-table";
66

7+
import { useChartState } from "@/charts/shared/chart-state";
78
import { BAR_CELL_PADDING } from "@/charts/table/constants";
8-
import { ColumnMeta } from "@/charts/table/table-state";
9+
import { LinkedCellWrapper } from "@/charts/table/linked-cell-wrapper";
10+
import { ColumnMeta, TableChartState } from "@/charts/table/table-state";
911
import { Tag } from "@/charts/table/tag";
1012
import { Flex } from "@/components/flex";
1113
import { Observation } from "@/domain/data";
@@ -59,6 +61,7 @@ export const CellDesktop = ({
5961
barShowBackground,
6062
} = columnMeta;
6163
const classes = useStyles();
64+
const { links } = useChartState() as TableChartState;
6265

6366
switch (columnMeta.type) {
6467
case "text":
@@ -77,7 +80,9 @@ export const CellDesktop = ({
7780
}}
7881
{...cell.getCellProps()}
7982
>
80-
{columnMeta.formatter(cell)}
83+
<LinkedCellWrapper cell={cell} columnMeta={columnMeta} links={links}>
84+
{columnMeta.formatter(cell)}
85+
</LinkedCellWrapper>
8186
</Flex>
8287
);
8388
case "category":
@@ -87,9 +92,11 @@ export const CellDesktop = ({
8792
sx={{ alignItems: "center", fontWeight: textStyle, pl: 1, pr: 3 }}
8893
{...cell.getCellProps()}
8994
>
90-
<Tag tagColor={cColorScale(cell.value)}>
91-
{columnMeta.formatter(cell)}
92-
</Tag>
95+
<LinkedCellWrapper cell={cell} columnMeta={columnMeta} links={links}>
96+
<Tag tagColor={cColorScale(cell.value)}>
97+
{columnMeta.formatter(cell)}
98+
</Tag>
99+
</LinkedCellWrapper>
93100
</Flex>
94101
);
95102
case "heatmap":
@@ -109,7 +116,9 @@ export const CellDesktop = ({
109116
}}
110117
{...cell.getCellProps()}
111118
>
112-
{columnMeta.formatter(cell)}
119+
<LinkedCellWrapper cell={cell} columnMeta={columnMeta} links={links}>
120+
{columnMeta.formatter(cell)}
121+
</LinkedCellWrapper>
113122
</Flex>
114123
);
115124
case "bar":
@@ -125,39 +134,41 @@ export const CellDesktop = ({
125134
}}
126135
{...cell.getCellProps()}
127136
>
128-
<Box>{columnMeta.formatter(cell)}</Box>
129-
{cell.value !== null && widthScale && (
130-
<Box
131-
sx={{
132-
width: widthScale.range()[1],
133-
height: 18,
134-
position: "relative",
135-
backgroundColor: barShowBackground
136-
? barColorBackground
137-
: "grey.100",
138-
}}
139-
>
137+
<LinkedCellWrapper cell={cell} columnMeta={columnMeta} links={links}>
138+
<Box>{columnMeta.formatter(cell)}</Box>
139+
{cell.value !== null && widthScale && (
140140
<Box
141-
className={classes.barForeground}
142141
sx={{
143-
left: `${getBarLeftOffset(cell.value, widthScale)}px`,
144-
width: `${getBarWidth(cell.value, widthScale)}px`,
145-
backgroundColor:
146-
cell.value > 0 ? barColorPositive : barColorNegative,
142+
width: widthScale.range()[1],
143+
height: 18,
144+
position: "relative",
145+
backgroundColor: barShowBackground
146+
? barColorBackground
147+
: "grey.100",
147148
}}
148-
/>
149-
<Box
150-
className={classes.barBackground}
151-
sx={{
152-
left: `${
153-
cell.value < 0
154-
? widthScale(0)
155-
: getBarLeftOffset(cell.value, widthScale)
156-
}px`,
157-
}}
158-
/>
159-
</Box>
160-
)}
149+
>
150+
<Box
151+
className={classes.barForeground}
152+
sx={{
153+
left: `${getBarLeftOffset(cell.value, widthScale)}px`,
154+
width: `${getBarWidth(cell.value, widthScale)}px`,
155+
backgroundColor:
156+
cell.value > 0 ? barColorPositive : barColorNegative,
157+
}}
158+
/>
159+
<Box
160+
className={classes.barBackground}
161+
sx={{
162+
left: `${
163+
cell.value < 0
164+
? widthScale(0)
165+
: getBarLeftOffset(cell.value, widthScale)
166+
}px`,
167+
}}
168+
/>
169+
</Box>
170+
)}
171+
</LinkedCellWrapper>
161172
</Flex>
162173
);
163174
default:
@@ -177,7 +188,9 @@ export const CellDesktop = ({
177188
}}
178189
{...cell.getCellProps()}
179190
>
180-
{columnMeta.formatter(cell)}
191+
<LinkedCellWrapper cell={cell} columnMeta={columnMeta} links={links}>
192+
{columnMeta.formatter(cell)}
193+
</LinkedCellWrapper>
181194
</Flex>
182195
);
183196
}

app/charts/table/cell-mobile.tsx

Lines changed: 87 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { Cell } from "react-table";
44

55
import { useChartState } from "@/charts/shared/chart-state";
66
import { getBarLeftOffset, getBarWidth } from "@/charts/table/cell-desktop";
7-
import { ColumnMeta } from "@/charts/table/table-state";
7+
import { LinkedCellWrapper } from "@/charts/table/linked-cell-wrapper";
8+
import { ColumnMeta, TableChartState } from "@/charts/table/table-state";
89
import { Tag } from "@/charts/table/tag";
910
import { Flex } from "@/components/flex";
1011
import { Observation } from "@/domain/data";
@@ -17,7 +18,8 @@ export const DDContent = ({
1718
cell: Cell<Observation>;
1819
columnMeta: ColumnMeta;
1920
}) => {
20-
const { bounds } = useChartState();
21+
const chartState = useChartState() as TableChartState;
22+
const { bounds, links } = chartState;
2123
const { chartWidth } = bounds;
2224

2325
const formatNumber = useFormatNumber();
@@ -43,103 +45,116 @@ export const DDContent = ({
4345
fontWeight: textStyle,
4446
}}
4547
>
46-
{columnComponentType === "NumericalMeasure"
47-
? formatNumber(cell.value)
48-
: cell.render("Cell")}
48+
<LinkedCellWrapper cell={cell} columnMeta={columnMeta} links={links}>
49+
{columnComponentType === "NumericalMeasure"
50+
? formatNumber(cell.value)
51+
: cell.render("Cell")}
52+
</LinkedCellWrapper>
4953
</Box>
5054
);
5155
}
5256
case "category": {
5357
const { colorScale } = columnMeta;
5458
return (
55-
<Tag tagColor={colorScale(cell.value)} small>
56-
{cell.render("Cell")}
57-
</Tag>
59+
<LinkedCellWrapper cell={cell} columnMeta={columnMeta} links={links}>
60+
<Tag tagColor={colorScale(cell.value)} small>
61+
{cell.render("Cell")}
62+
</Tag>
63+
</LinkedCellWrapper>
5864
);
5965
}
6066
case "heatmap": {
6167
const { colorScale } = columnMeta;
6268
const isNull = cell.value === null;
6369
return (
64-
<Box
65-
sx={{
66-
width: "fit-content",
67-
px: 1,
68-
borderRadius: "2px",
69-
backgroundColor: isNull ? "grey.100" : colorScale(cell.value),
70-
color: isNull
71-
? textColor
72-
: hcl(colorScale(cell.value)).l < 55
73-
? "#fff"
74-
: "#000",
75-
fontWeight: textStyle,
76-
}}
77-
>
78-
{formatNumber(cell.value)}
79-
</Box>
70+
<LinkedCellWrapper cell={cell} columnMeta={columnMeta} links={links}>
71+
<Box
72+
sx={{
73+
width: "fit-content",
74+
px: 1,
75+
borderRadius: "2px",
76+
backgroundColor: isNull ? "grey.100" : colorScale(cell.value),
77+
color: isNull
78+
? textColor
79+
: hcl(colorScale(cell.value)).l < 55
80+
? "#fff"
81+
: "#000",
82+
fontWeight: textStyle,
83+
}}
84+
>
85+
{formatNumber(cell.value)}
86+
</Box>
87+
</LinkedCellWrapper>
8088
);
8189
}
8290
case "bar": {
8391
const { widthScale } = columnMeta;
8492
// Reset width scale range based on current viewport
8593
widthScale.range([0, chartWidth / 2]);
8694
return (
87-
<Flex
88-
sx={{
89-
flexDirection: "column",
90-
justifyContent: "center",
91-
width: chartWidth / 2,
92-
}}
93-
>
94-
<Box sx={{ width: chartWidth / 2 }}>{formatNumber(cell.value)}</Box>
95-
{cell.value !== null && (
96-
<Box
97-
sx={{
98-
position: "relative",
99-
width: chartWidth / 2,
100-
height: 14,
101-
backgroundColor: barShowBackground
102-
? barColorBackground
103-
: "grey.100",
104-
}}
105-
>
95+
<LinkedCellWrapper cell={cell} columnMeta={columnMeta} links={links}>
96+
<Flex
97+
sx={{
98+
flexDirection: "column",
99+
justifyContent: "center",
100+
width: chartWidth / 2,
101+
}}
102+
>
103+
<Box sx={{ width: chartWidth / 2 }}>{formatNumber(cell.value)}</Box>
104+
{cell.value !== null && (
106105
<Box
107106
sx={{
108-
position: "absolute",
109-
top: 0,
110-
left: `${getBarLeftOffset(cell.value, widthScale)}px`,
111-
width: `${getBarWidth(cell.value, widthScale)}px`,
107+
position: "relative",
108+
width: chartWidth / 2,
112109
height: 14,
113-
backgroundColor:
114-
cell.value > 0 ? barColorPositive : barColorNegative,
115-
}}
116-
/>
117-
<Box
118-
sx={{
119-
position: "absolute",
120-
top: "-2px",
121-
left: `${
122-
cell.value < 0
123-
? widthScale(0)
124-
: getBarLeftOffset(cell.value, widthScale)
125-
}px`,
126-
width: "1px",
127-
height: 18,
128-
backgroundColor: "grey.700",
110+
backgroundColor: barShowBackground
111+
? barColorBackground
112+
: "grey.100",
129113
}}
130-
/>
131-
</Box>
132-
)}
133-
</Flex>
114+
>
115+
<Box
116+
sx={{
117+
position: "absolute",
118+
top: 0,
119+
left: `${getBarLeftOffset(cell.value, widthScale)}px`,
120+
width: `${getBarWidth(cell.value, widthScale)}px`,
121+
height: 14,
122+
backgroundColor:
123+
cell.value > 0 ? barColorPositive : barColorNegative,
124+
}}
125+
/>
126+
<Box
127+
sx={{
128+
position: "absolute",
129+
top: "-2px",
130+
left: `${
131+
cell.value < 0
132+
? widthScale(0)
133+
: getBarLeftOffset(cell.value, widthScale)
134+
}px`,
135+
width: "1px",
136+
height: 18,
137+
backgroundColor: "grey.700",
138+
}}
139+
/>
140+
</Box>
141+
)}
142+
</Flex>
143+
</LinkedCellWrapper>
134144
);
135145
}
136146
default: {
137147
return (
138-
<Box component="span" sx={{ color: textColor, fontWeight: textStyle }}>
139-
{columnComponentType === "NumericalMeasure"
140-
? formatNumber(cell.value)
141-
: cell.render("Cell")}
142-
</Box>
148+
<LinkedCellWrapper cell={cell} columnMeta={columnMeta} links={links}>
149+
<Box
150+
component="span"
151+
sx={{ color: textColor, fontWeight: textStyle }}
152+
>
153+
{columnComponentType === "NumericalMeasure"
154+
? formatNumber(cell.value)
155+
: cell.render("Cell")}
156+
</Box>
157+
</LinkedCellWrapper>
143158
);
144159
}
145160
}

0 commit comments

Comments
 (0)