Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

style: Update metadata and filters positions + styles #1552

Merged
merged 10 commits into from
Jun 3, 2024
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ You can also check the [release page](https://github.com/visualize-admin/visuali
- Features
- Add the "Free canvas" layout, allowing users to freely resize and move charts for dashboards
- Ability to start a chart from another dataset than the current one

- Style
- Improved the styles of metadata panel and interactive filters toggle buttons

# [4.5.1] - 2024-05-21

- Fixes
Expand Down
181 changes: 102 additions & 79 deletions app/charts/shared/chart-data-filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import {
import { useTimeFormatLocale } from "@/formatters";
import { useDataCubesComponentsQuery } from "@/graphql/hooks";
import {
DataCubeObservationFilter,
PossibleFiltersDocument,
PossibleFiltersQuery,
PossibleFiltersQueryVariables,
Expand All @@ -52,6 +51,7 @@ import {
useChartInteractiveFilters,
useInteractiveFiltersGetState,
} from "@/stores/interactive-filters";
import { assert } from "@/utils/assert";
import { hierarchyToOptions } from "@/utils/hierarchy";
import useEvent from "@/utils/use-event";

Expand All @@ -62,64 +62,72 @@ type PreparedFilter = {
mappedFilters: Filters;
};

type ChartDataFiltersProps = {
export const useChartDataFiltersState = ({
dataSource,
chartConfig,
}: {
dataSource: DataSource;
chartConfig: ChartConfig;
};

export const ChartDataFilters = (props: ChartDataFiltersProps) => {
const { dataSource, chartConfig } = props;
}) => {
const componentIris =
chartConfig.interactiveFiltersConfig?.dataFilters.componentIris;
assert(componentIris, "Data filters are not enabled for this chart.");
const [open, setOpen] = useState(false);
useEffect(() => {
if (componentIris.length === 0) {
setOpen(false);
}
}, [componentIris.length]);
const { loading } = useLoadingState();
const dataFilters = useChartInteractiveFilters((d) => d.dataFilters);
const componentIris = chartConfig.interactiveFiltersConfig?.dataFilters
.componentIris as string[];
const queryFilters = useQueryFilters({
chartConfig,
allowNoneValues: true,
componentIris,
});
const [filtersVisible, setFiltersVisible] = useState(false);

useEffect(() => {
if (componentIris.length === 0) {
setFiltersVisible(false);
}
}, [componentIris.length]);

const preparedFilters: PreparedFilter[] | undefined = useMemo(() => {
const preparedFilters = useMemo(() => {
return chartConfig.cubes.map((cube) => {
const cubeQueryFilters = queryFilters.find(
(d) => d.iri === cube.iri
) as DataCubeObservationFilter;
const cubeQueryFilters = queryFilters.find((d) => d.iri === cube.iri);
assert(cubeQueryFilters, "Cube query filters not found.");
const filtersByMappingStatus = getFiltersByMappingStatus(chartConfig, {
cubeIri: cube.iri,
});
const { unmappedFilters, mappedFilters } = filtersByMappingStatus;
const unmappedKeys = Object.keys(unmappedFilters);
const unmappedFiltersArray = Object.entries(
const unmappedEntries = Object.entries(
cubeQueryFilters.filters as Filters
).filter(([k]) => unmappedKeys.includes(k));
const interactiveFiltersArray = unmappedFiltersArray.filter(([k]) =>
const interactiveFiltersList = unmappedEntries.filter(([k]) =>
componentIris.includes(k)
);

return {
cubeIri: cube.iri,
interactiveFilters: Object.fromEntries(interactiveFiltersArray),
unmappedFilters: Object.fromEntries(
unmappedFiltersArray
) as SingleFilters,
interactiveFilters: Object.fromEntries(interactiveFiltersList),
unmappedFilters: Object.fromEntries(unmappedEntries) as SingleFilters,
mappedFilters,
};
});
}, [chartConfig, componentIris, queryFilters]);

const { error } = useEnsurePossibleInteractiveFilters({
dataSource,
chartConfig,
preparedFilters,
});
return {
open,
setOpen,
dataSource,
chartConfig,
loading,
error,
preparedFilters,
componentIris,
};
};

export const ChartDataFiltersToggle = (
props: ReturnType<typeof useChartDataFiltersState>
) => {
const { open, setOpen, loading, error, componentIris } = props;
return error ? (
<Typography variant="body2" color="error">
<Trans id="controls.section.data.filters.possible-filters-error">
Expand All @@ -128,10 +136,9 @@ export const ChartDataFilters = (props: ChartDataFiltersProps) => {
</Trans>
</Typography>
) : (
<Flex sx={{ flexDirection: "column", mt: 4 }}>
<Flex sx={{ flexDirection: "column", width: "100%" }}>
<Flex
sx={{
justifyContent: "flex-end",
alignItems: "flex-start",
gap: 3,
minHeight: 20,
Expand All @@ -140,91 +147,107 @@ export const ChartDataFilters = (props: ChartDataFiltersProps) => {
{componentIris.length > 0 && (
<Button
variant="text"
color="primary"
size="small"
endIcon={
<Icon
name="add"
size={16}
style={{
transform: filtersVisible ? "rotate(45deg)" : "rotate(0deg)",
transform: open ? "rotate(45deg)" : "rotate(0deg)",
transition: "transform 0.2s ease-in-out",
}}
/>
}
sx={{
display: "flex",
fontSize: ["0.75rem", "0.75rem", "0.75rem"],
alignItems: "center",
minWidth: "fit-content",
minHeight: 0,
ml: -2,
px: 2,
py: 1,
}}
onClick={() => setFiltersVisible(!filtersVisible)}
onClick={() => setOpen(!open)}
>
{loading && (
<span style={{ marginTop: "0.1rem", marginRight: "0.5rem" }}>
<LoadingIndicator />
</span>
)}
{filtersVisible ? (
<Trans id="interactive.data.filters.hide">Hide Filters</Trans>
) : (
<Trans id="interactive.data.filters.show">Show Filters</Trans>
)}
<Typography variant="body2">
{open ? (
<Trans id="interactive.data.filters.hide">Hide Filters</Trans>
) : (
<Trans id="interactive.data.filters.show">Show Filters</Trans>
)}
</Typography>
</Button>
)}
</Flex>

{componentIris.length > 0 && (
<Box
data-testid="published-chart-interactive-filters"
sx={{
display: filtersVisible ? "grid" : "none",
columnGap: 3,
rowGap: 2,
gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))",
}}
>
{preparedFilters?.map(({ cubeIri, interactiveFilters }) =>
Object.keys(interactiveFilters).map((dimensionIri) => (
<DataFilter
key={dimensionIri}
cubeIri={cubeIri}
dimensionIri={dimensionIri}
dataSource={dataSource}
chartConfig={chartConfig}
dataFilters={dataFilters}
interactiveFilters={interactiveFilters}
disabled={loading}
/>
))
)}
</Box>
)}
</Flex>
);
};

type DataFilterProps = {
export const ChartDataFiltersList = (
props: ReturnType<typeof useChartDataFiltersState>
) => {
const {
open,
dataSource,
chartConfig,
loading,
preparedFilters,
componentIris,
} = props;
const dataFilters = useChartInteractiveFilters((d) => d.dataFilters);
return componentIris.length > 0 ? (
<Box
data-testid="published-chart-interactive-filters"
sx={{
display: open ? "grid" : "none",
columnGap: 3,
rowGap: 2,
gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))",
}}
>
{preparedFilters.map(({ cubeIri, interactiveFilters }) => {
return Object.keys(interactiveFilters).map((dimensionIri) => {
return (
<DataFilter
key={dimensionIri}
cubeIri={cubeIri}
dimensionIri={dimensionIri}
dataSource={dataSource}
chartConfig={chartConfig}
dataFilters={dataFilters}
interactiveFilters={interactiveFilters}
disabled={loading}
/>
);
});
})}
</Box>
) : null;
};

const DataFilter = ({
cubeIri,
dimensionIri,
dataSource,
chartConfig,
dataFilters,
interactiveFilters,
disabled,
}: {
cubeIri: string;
dimensionIri: string;
dataSource: DataSource;
chartConfig: ChartConfig;
dataFilters: DataFilters;
interactiveFilters: Filters;
disabled: boolean;
};

const DataFilter = (props: DataFilterProps) => {
const {
cubeIri,
dimensionIri,
dataSource,
chartConfig,
dataFilters,
interactiveFilters,
disabled,
} = props;
}) => {
const locale = useLocale();
const filters = useChartConfigFilters(chartConfig);
const chartLoadingState = useLoadingState();
Expand Down
Loading
Loading