Skip to content

Commit bcf550c

Browse files
authored
Merge pull request #1909 from visualize-admin/feat/tooltip-for-disabled-chart-types
feat: Tooltip for disabled chart types
2 parents 2a8d00a + 2d724e1 commit bcf550c

25 files changed

+515
-195
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ You can also check the
2121
- Improved default chart selection when adding a new cube (by default the
2222
application will try to use dual-line chart with measures from different
2323
cubes)
24+
- Added tooltip that explains why a given chart type can't be selected
2425
- Fixes
2526
- Introduced a `componentId` concept which makes the dimensions and measures
2627
unique by adding an unversioned cube iri to the unversioned component iri on

app/browse/datatable.tsx

+7-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
TableRow,
99
TableSortLabel,
1010
Theme,
11-
Tooltip,
1211
TooltipProps,
1312
} from "@mui/material";
1413
import { ascending, descending } from "d3-array";
@@ -19,7 +18,9 @@ import {
1918
useQueryFilters,
2019
} from "@/charts/shared/chart-helpers";
2120
import { Loading } from "@/components/hint";
21+
import { MaybeTooltip } from "@/components/maybe-tooltip";
2222
import { OpenMetadataPanelWrapper } from "@/components/metadata-panel";
23+
import { TooltipTitle } from "@/components/tooltip-utils";
2324
import {
2425
ChartConfig,
2526
DashboardFiltersConfig,
@@ -59,11 +60,14 @@ const ComponentLabel = (props: ComponentLabelProps) => {
5960
<span style={{ fontWeight: "bold" }}>{label}</span>
6061
</OpenMetadataPanelWrapper>
6162
) : component.description ? (
62-
<Tooltip title={component.description} arrow {...tooltipProps}>
63+
<MaybeTooltip
64+
title={<TooltipTitle text={component.description} />}
65+
tooltipProps={tooltipProps}
66+
>
6367
<span style={{ fontWeight: "bold", textDecoration: "underline" }}>
6468
{label}
6569
</span>
66-
</Tooltip>
70+
</MaybeTooltip>
6771
) : (
6872
<span style={{ fontWeight: "bold" }}>{label}</span>
6973
);

app/charts/index.spec.ts

+27-12
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@ import forestAreaData from "../test/__fixtures/data/forest-area-by-production-re
1313

1414
import {
1515
getChartConfigAdjustedToChartType,
16+
getEnabledChartTypes,
1617
getInitialConfig,
17-
getPossibleChartTypes,
1818
} from "./index";
1919

2020
jest.mock("../rdf/extended-cube", () => ({
2121
ExtendedCube: jest.fn(),
2222
}));
2323

24+
jest.mock("@lingui/macro", () => ({
25+
defineMessage: (str: string) => str,
26+
t: (str: string) => str,
27+
}));
28+
2429
const mockDimensions: Record<string, Dimension> = {
2530
geoCoordinates: {
2631
__typename: "GeoCoordinatesDimension",
@@ -175,42 +180,51 @@ describe("initial config", () => {
175180
});
176181
});
177182

178-
describe("possible chart types", () => {
183+
describe("enabled chart types", () => {
179184
it("should allow appropriate chart types based on available dimensions", () => {
180185
const expectedChartTypes = ["area", "column", "line", "pie", "table"];
181-
const possibleChartTypes = getPossibleChartTypes({
186+
const { enabledChartTypes, possibleChartTypesDict } = getEnabledChartTypes({
182187
dimensions: bathingWaterData.data.dataCubeByIri
183188
.dimensions as any as Dimension[],
184189
measures: bathingWaterData.data.dataCubeByIri
185190
.measures as any as Measure[],
186191
cubeCount: 1,
187-
}).sort();
192+
});
188193

189-
expect(possibleChartTypes).toEqual(expectedChartTypes);
194+
expect(enabledChartTypes.sort()).toEqual(expectedChartTypes);
195+
expect(possibleChartTypesDict["comboLineSingle"].message).toBeDefined();
196+
expect(possibleChartTypesDict["comboLineDual"].message).toBeDefined();
197+
expect(possibleChartTypesDict["map"].message).toBeDefined();
190198
});
191199

192200
it("should only allow table if there are only measures available", () => {
193-
const possibleChartTypes = getPossibleChartTypes({
201+
const { enabledChartTypes, possibleChartTypesDict } = getEnabledChartTypes({
194202
dimensions: [],
195203
measures: [{ __typename: "NumericalMeasure" }] as any,
196204
cubeCount: 1,
197205
});
198206

199-
expect(possibleChartTypes).toEqual(["table"]);
207+
expect(enabledChartTypes).toEqual(["table"]);
208+
expect(
209+
Object.entries(possibleChartTypesDict)
210+
.filter(([k]) => k !== "table")
211+
.every(([, { message }]) => !!message)
212+
).toBe(true);
200213
});
201214

202215
it("should only allow column, map, pie and table if only geo dimensions are available", () => {
203-
const possibleChartTypes = getPossibleChartTypes({
216+
const { enabledChartTypes, possibleChartTypesDict } = getEnabledChartTypes({
204217
dimensions: [{ __typename: "GeoShapesDimension" }] as any,
205218
measures: [{ __typename: "NumericalMeasure" }] as any,
206219
cubeCount: 1,
207-
}).sort();
220+
});
208221

209-
expect(possibleChartTypes).toEqual(["column", "map", "pie", "table"]);
222+
expect(enabledChartTypes.sort()).toEqual(["column", "map", "pie", "table"]);
223+
expect(possibleChartTypesDict["line"].message).toBeDefined();
210224
});
211225

212226
it("should not allow multiline chart if there are no several measures of the same unit", () => {
213-
const possibleChartTypes = getPossibleChartTypes({
227+
const { enabledChartTypes, possibleChartTypesDict } = getEnabledChartTypes({
214228
dimensions: [],
215229
measures: [
216230
{ __typename: "NumericalMeasure", unit: "m" },
@@ -219,7 +233,8 @@ describe("possible chart types", () => {
219233
cubeCount: 1,
220234
});
221235

222-
expect(possibleChartTypes).not.toContain("comboLineSingle");
236+
expect(enabledChartTypes).not.toContain("comboLineSingle");
237+
expect(possibleChartTypesDict["comboLineSingle"].message).toBeDefined();
223238
});
224239
});
225240

app/charts/index.ts

+140-26
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { t } from "@lingui/macro";
12
import { ascending, descending, group, rollup, rollups } from "d3-array";
23
import produce from "immer";
34
import get from "lodash/get";
@@ -563,7 +564,7 @@ export const getInitialConfig = (
563564
) as TableFields,
564565
};
565566
case "comboLineSingle": {
566-
// It's guaranteed by getPossibleChartTypes that there are at least two units.
567+
// It's guaranteed by getEnabledChartTypes that there are at least two units.
567568
const mostCommonUnit = rollups(
568569
numericalMeasures.filter((d) => d.unit),
569570
(v) => v.length,
@@ -597,7 +598,7 @@ export const getInitialConfig = (
597598
};
598599
}
599600
case "comboLineDual": {
600-
// It's guaranteed by getPossibleChartTypes that there are at least two units.
601+
// It's guaranteed by getEnabledChartTypes that there are at least two units.
601602
const [firstUnit, secondUnit] = Array.from(
602603
new Set(numericalMeasures.filter((d) => d.unit).map((d) => d.unit))
603604
);
@@ -634,7 +635,7 @@ export const getInitialConfig = (
634635
};
635636
}
636637
case "comboLineColumn": {
637-
// It's guaranteed by getPossibleChartTypes that there are at least two units.
638+
// It's guaranteed by getEnabledChartTypes that there are at least two units.
638639
const [firstUnit, secondUnit] = Array.from(
639640
new Set(numericalMeasures.filter((d) => d.unit).map((d) => d.unit))
640641
);
@@ -1948,17 +1949,22 @@ const adjustSegmentSorting = ({
19481949
return newSorting;
19491950
};
19501951

1951-
export const getPossibleChartTypes = ({
1952+
const categoricalEnabledChartTypes: RegularChartType[] = ["column", "pie"];
1953+
const geoEnabledChartTypes: RegularChartType[] = ["column", "map", "pie"];
1954+
const multipleNumericalMeasuresEnabledChartTypes: RegularChartType[] = [
1955+
"scatterplot",
1956+
];
1957+
const timeEnabledChartTypes: RegularChartType[] = ["area", "column", "line"];
1958+
1959+
export const getEnabledChartTypes = ({
19521960
dimensions,
19531961
measures,
1954-
allowedChartTypes,
19551962
cubeCount,
19561963
}: {
19571964
dimensions: Dimension[];
19581965
measures: Measure[];
1959-
allowedChartTypes?: ChartType[];
19601966
cubeCount: number;
1961-
}): ChartType[] => {
1967+
}) => {
19621968
const numericalMeasures = measures.filter(isNumericalMeasure);
19631969
const ordinalMeasures = measures.filter(isOrdinalMeasure);
19641970
const categoricalDimensions = getCategoricalDimensions(dimensions);
@@ -1967,23 +1973,76 @@ export const getPossibleChartTypes = ({
19671973
(d) => isTemporalDimension(d) || isTemporalEntityDimension(d)
19681974
);
19691975

1970-
const categoricalEnabled: RegularChartType[] = ["column", "pie"];
1971-
const geoEnabled: RegularChartType[] = ["column", "map", "pie"];
1972-
const multipleNumericalMeasuresEnabled: RegularChartType[] = ["scatterplot"];
1973-
const timeEnabled: RegularChartType[] = ["area", "column", "line"];
1976+
const possibleChartTypesDict = Object.fromEntries(
1977+
chartTypes.map((chartType) => [
1978+
chartType,
1979+
{
1980+
enabled: chartType === "table",
1981+
message: undefined,
1982+
},
1983+
])
1984+
) as Record<
1985+
ChartType,
1986+
{
1987+
enabled: boolean;
1988+
message: string | undefined;
1989+
}
1990+
>;
1991+
const enableChartType = (chartType: ChartType) => {
1992+
possibleChartTypesDict[chartType] = {
1993+
enabled: true,
1994+
message: undefined,
1995+
};
1996+
};
1997+
const enableChartTypes = (chartTypes: ChartType[]) => {
1998+
for (const chartType of chartTypes) {
1999+
enableChartType(chartType);
2000+
}
2001+
};
2002+
const maybeDisableChartType = (chartType: ChartType, message: string) => {
2003+
if (
2004+
!possibleChartTypesDict[chartType].enabled &&
2005+
!possibleChartTypesDict[chartType].message
2006+
) {
2007+
possibleChartTypesDict[chartType] = {
2008+
enabled: false,
2009+
message,
2010+
};
2011+
}
2012+
};
2013+
const maybeDisableChartTypes = (chartTypes: ChartType[], message: string) => {
2014+
for (const chartType of chartTypes) {
2015+
maybeDisableChartType(chartType, message);
2016+
}
2017+
};
19742018

1975-
const possibles: ChartType[] = ["table"];
19762019
if (numericalMeasures.length > 0) {
19772020
if (categoricalDimensions.length > 0) {
1978-
possibles.push(...categoricalEnabled);
2021+
enableChartTypes(categoricalEnabledChartTypes);
2022+
} else {
2023+
maybeDisableChartTypes(
2024+
categoricalEnabledChartTypes,
2025+
t({
2026+
id: "controls.chart.disabled.categorical",
2027+
message: "At least one categorical dimension is required.",
2028+
})
2029+
);
19792030
}
19802031

19812032
if (geoDimensions.length > 0) {
1982-
possibles.push(...geoEnabled);
2033+
enableChartTypes(geoEnabledChartTypes);
2034+
} else {
2035+
maybeDisableChartTypes(
2036+
geoEnabledChartTypes,
2037+
t({
2038+
id: "controls.chart.disabled.geographical",
2039+
message: "At least one geographical dimension is required.",
2040+
})
2041+
);
19832042
}
19842043

19852044
if (numericalMeasures.length > 1) {
1986-
possibles.push(...multipleNumericalMeasuresEnabled);
2045+
enableChartTypes(multipleNumericalMeasuresEnabledChartTypes);
19872046

19882047
if (temporalDimensions.length > 0) {
19892048
const measuresWithUnit = numericalMeasures.filter((d) => d.unit);
@@ -1992,7 +2051,16 @@ export const getPossibleChartTypes = ({
19922051
);
19932052

19942053
if (uniqueUnits.length > 1) {
1995-
possibles.push(...comboDifferentUnitChartTypes);
2054+
enableChartTypes(comboDifferentUnitChartTypes);
2055+
} else {
2056+
maybeDisableChartTypes(
2057+
comboDifferentUnitChartTypes,
2058+
t({
2059+
id: "controls.chart.disabled.different-unit",
2060+
message:
2061+
"At least two numerical measures with different units are required.",
2062+
})
2063+
);
19962064
}
19972065

19982066
const unitCounts = rollup(
@@ -2002,29 +2070,75 @@ export const getPossibleChartTypes = ({
20022070
);
20032071

20042072
if (Array.from(unitCounts.values()).some((d) => d > 1)) {
2005-
possibles.push(...comboSameUnitChartTypes);
2073+
enableChartTypes(comboSameUnitChartTypes);
2074+
} else {
2075+
maybeDisableChartTypes(
2076+
comboSameUnitChartTypes,
2077+
t({
2078+
id: "controls.chart.disabled.same-unit",
2079+
message:
2080+
"At least two numerical measures with the same unit are required.",
2081+
})
2082+
);
20062083
}
2084+
} else {
2085+
maybeDisableChartTypes(
2086+
comboChartTypes,
2087+
t({
2088+
id: "controls.chart.disabled.temporal",
2089+
message: "At least one temporal dimension is required.",
2090+
})
2091+
);
20072092
}
2093+
} else {
2094+
maybeDisableChartTypes(
2095+
[...multipleNumericalMeasuresEnabledChartTypes, ...comboChartTypes],
2096+
t({
2097+
id: "controls.chart.disabled.multiple-measures",
2098+
message: "At least two numerical measures are required.",
2099+
})
2100+
);
20082101
}
20092102

20102103
if (temporalDimensions.length > 0) {
2011-
possibles.push(...timeEnabled);
2104+
enableChartTypes(timeEnabledChartTypes);
2105+
} else {
2106+
maybeDisableChartTypes(
2107+
timeEnabledChartTypes,
2108+
t({
2109+
id: "controls.chart.disabled.temporal",
2110+
message: "At least one temporal dimension is required.",
2111+
})
2112+
);
20122113
}
2114+
} else {
2115+
maybeDisableChartTypes(
2116+
chartTypes.filter((d) => d !== "table"),
2117+
t({
2118+
id: "controls.chart.disabled.numerical",
2119+
message: "At least one numerical measure is required.",
2120+
})
2121+
);
20132122
}
20142123

20152124
if (ordinalMeasures.length > 0 && geoDimensions.length > 0) {
2016-
possibles.push("map");
2125+
enableChartType("map");
2126+
} else {
2127+
maybeDisableChartType(
2128+
"map",
2129+
"At least one ordinal measure and one geographical dimension are required."
2130+
);
20172131
}
20182132

20192133
const chartTypesOrder = getChartTypeOrder({ cubeCount });
2020-
2021-
return chartTypes
2022-
.filter(
2023-
(d) =>
2024-
possibles.includes(d) &&
2025-
(!allowedChartTypes || allowedChartTypes.includes(d))
2026-
)
2134+
const enabledChartTypes = chartTypes
2135+
.filter((d) => possibleChartTypesDict[d].enabled)
20272136
.sort((a, b) => chartTypesOrder[a] - chartTypesOrder[b]);
2137+
2138+
return {
2139+
enabledChartTypes,
2140+
possibleChartTypesDict,
2141+
};
20282142
};
20292143

20302144
export const getFieldComponentIds = (fields: ChartConfig["fields"]) => {

0 commit comments

Comments
 (0)