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

fix: Color segmentation filtering #359

Merged
merged 7 commits into from
Feb 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions app/charts/shared/use-chart-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createContext, useContext } from "react";
import { ChartFields, InteractiveFiltersConfig } from "../../configurator";
import { Observation } from "../../domain/data";
import { DimensionMetaDataFragment } from "../../graphql/query-hooks";
import { Has } from "../../lib/has";
import { AreasState } from "../area/areas-state";
import { GroupedBarsState } from "../bar/bars-grouped-state";
import { BarsState } from "../bar/bars-state";
Expand Down Expand Up @@ -36,10 +37,6 @@ export type ChartState =
| MapState
| undefined;

type Has<T extends unknown, R extends string> = T extends { [k in R]: any }
? T
: never;

export type ColorsChartState = Has<ChartState, "colors">;
export const ChartContext = createContext<ChartState>(undefined);

Expand Down
68 changes: 53 additions & 15 deletions app/configurator/components/chart-configurator.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { t, Trans } from "@lingui/macro";
import { Menu, MenuButton, MenuItem, MenuList } from "@reach/menu-button";
import { isEqual, sortBy } from "lodash";
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
DragDropContext,
Draggable,
Expand All @@ -15,7 +15,6 @@ import {
ConfiguratorStateConfiguringChart,
ConfiguratorStateSelectingChartType,
isMapConfig,
useConfiguratorState,
} from "..";
import { getFieldComponentIris } from "../../charts";
import { chartConfigOptionsUISpec } from "../../charts/chart-config-ui-options";
Expand All @@ -29,7 +28,11 @@ import {
import { DataCubeMetadata } from "../../graphql/types";
import { Icon } from "../../icons";
import { useLocale } from "../../locales/use-locale";
import { moveFilterField } from "../configurator-state";
import {
getFiltersByMappingStatus,
moveFilterField,
useConfiguratorState,
} from "../configurator-state";
import { FIELD_VALUE_NONE } from "../constants";
import {
ControlSection,
Expand Down Expand Up @@ -119,6 +122,13 @@ const DataFilterSelectGeneric = ({
);
};

const orderedIsEqual = (
obj1: Record<string, unknown>,
obj2: Record<string, unknown>
) => {
return isEqual(Object.keys(obj1), Object.keys(obj2)) && isEqual(obj1, obj2);
};

/**
* This runs every time the state changes and it ensures that the selected filters
* return at least 1 observation. Otherwise filters are reloaded.
Expand All @@ -133,19 +143,32 @@ export const useEnsurePossibleFilters = ({
const [, dispatch] = useConfiguratorState();
const [fetching, setFetching] = useState(false);
const [error, setError] = useState<Error>();

const lastFilters = useRef<ChartConfig["filters"]>();
const client = useClient();

useEffect(() => {
const run = async () => {
const { mapped: mappedFilters, unmapped: unmappedFilters } =
getFiltersByMappingStatus(
state.chartConfig.fields,
state.chartConfig.filters
);
if (
lastFilters.current &&
orderedIsEqual(lastFilters.current, unmappedFilters)
) {
return;
}
lastFilters.current = unmappedFilters;

setFetching(true);
const {
data,
error,
}: { data?: PossibleFiltersQuery; error?: CombinedError } = await client
.query(PossibleFiltersDocument, {
iri: state.dataSet,
filters: state.chartConfig.filters,
filters: unmappedFilters,
})
.toPromise();
if (error || !data) {
Expand All @@ -156,12 +179,15 @@ export const useEnsurePossibleFilters = ({
setError(undefined);
setFetching(false);

const filters = Object.fromEntries(
data.possibleFilters.map((x) => [
x.iri,
{ type: x.type, value: x.value },
])
) as ChartConfig["filters"];
const filters = Object.assign(
Object.fromEntries(
data.possibleFilters.map((x) => [
x.iri,
{ type: x.type, value: x.value },
])
) as ChartConfig["filters"],
mappedFilters
);

if (!isEqual(filters, state.chartConfig.filters)) {
dispatch({
Expand All @@ -174,7 +200,14 @@ export const useEnsurePossibleFilters = ({
};

run();
}, [client, dispatch, state.chartConfig, state.dataSet]);
}, [
client,
dispatch,
state,
state.chartConfig.fields,
state.chartConfig.filters,
state.dataSet,
]);
return { error, fetching };
};

Expand All @@ -185,18 +218,23 @@ export const ChartConfigurator = ({
}) => {
const locale = useLocale();
const [, dispatch] = useConfiguratorState();
const { fields, filters } = state.chartConfig;
const unmappedFilters = React.useMemo(() => {
return getFiltersByMappingStatus(fields, filters).unmapped;
}, [fields, filters]);

const variables = React.useMemo(
() => ({
iri: state.dataSet,
locale,
filters: state.chartConfig.filters,
filters: unmappedFilters,
// This is important for urql not to think that filters
// are the same while the order of the keys has changed.
// If this is not present, we'll have outdated dimension
// values after we change the filter order
filterKeys: Object.keys(state.chartConfig.filters).join(", "),
filterKeys: Object.keys(unmappedFilters).join(", "),
}),
[state, locale]
[state.dataSet, locale, unmappedFilters]
);
const [{ data, fetching: dataFetching }, executeQuery] =
useDataCubeMetadataWithComponentValuesQuery({
Expand Down
15 changes: 14 additions & 1 deletion app/configurator/configurator-state.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import produce from "immer";
import { mapValues } from "lodash";
import { mapValues, pickBy } from "lodash";
import setWith from "lodash/setWith";
import { useRouter } from "next/router";
import {
Expand All @@ -12,6 +12,7 @@ import {
import { Client, useClient } from "urql";
import { Reducer, useImmerReducer } from "use-immer";
import {
ConfiguratorStateConfiguringChart,
ImputationType,
isAreaConfig,
isColumnConfig,
Expand Down Expand Up @@ -569,6 +570,18 @@ export const canTransitionToPreviousStep = (
return true;
};

export const getFiltersByMappingStatus = (
fields: ConfiguratorStateConfiguringChart["chartConfig"]["fields"],
filters: ConfiguratorStateConfiguringChart["chartConfig"]["filters"]
) => {
const mappedIris = new Set(
Object.values(fields).map((fieldValue) => fieldValue.componentIri)
);
const unmapped = pickBy(filters, (value, iri) => !mappedIris.has(iri));
const mapped = pickBy(filters, (value, iri) => mappedIris.has(iri));
return { unmapped, mapped };
};

const reducer: Reducer<ConfiguratorState, ConfiguratorStateAction> = (
draft,
action
Expand Down
4 changes: 2 additions & 2 deletions app/configurator/table/table-chart-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
import { useConfiguratorState } from "../configurator-state";
import { TableSortingOptions } from "./table-chart-sorting-options";
import { updateIsGroup, updateIsHidden } from "./table-config-state";
import { canDimensionBeMultiFiltered } from "../../domain/data";

const useTableColumnGroupHiddenField = ({
path,
Expand Down Expand Up @@ -294,8 +295,7 @@ export const TableColumnOptions = ({
</ControlSectionContent>
</ControlSection>
)}
{component.__typename === "NominalDimension" ||
component.__typename === "OrdinalDimension" ? (
{canDimensionBeMultiFiltered(component) ? (
<ControlSection>
<SectionTitle disabled={!component} iconName="filter">
<Trans id="controls.section.filter">Filter</Trans>
Expand Down
36 changes: 31 additions & 5 deletions app/domain/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
DimensionMetaDataFragment,
GeoCoordinatesDimension,
GeoShapesDimension,
NominalDimension,
OrdinalDimension,
} from "../graphql/query-hooks";

export type RawObservationValue = Literal | NamedNode;
Expand Down Expand Up @@ -128,16 +130,28 @@ export const parseObservationValue = ({
*/
export const getTimeDimensions = (dimensions: DimensionMetaDataFragment[]) =>
dimensions.filter((d) => d.__typename === "TemporalDimension");

export const isCategoricalDimension = (
d: DimensionMetaDataFragment
): d is NominalDimension | OrdinalDimension => {
return isNominalDimension(d) || isOrdinalDimension(d);
};

export const canDimensionBeMultiFiltered = (d: DimensionMetaDataFragment) => {
return (
isNominalDimension(d) ||
isOrdinalDimension(d) ||
isGeoCoordinatesDimension(d) ||
isGeoShapesDimension(d)
);
};

/**
* @fixme use metadata to filter categorical dimension!
*/
export const getCategoricalDimensions = (
dimensions: DimensionMetaDataFragment[]
) =>
dimensions.filter(
(d) =>
d.__typename === "NominalDimension" || d.__typename === "OrdinalDimension"
);
) => dimensions.filter(isCategoricalDimension);

export const getGeoCoordinatesDimensions = (
dimensions: DimensionMetaDataFragment[]
Expand Down Expand Up @@ -165,6 +179,18 @@ export const getDimensionsByDimensionType = ({
dimensionTypes.includes(component.__typename)
);

export const isNominalDimension = (
dimension?: DimensionMetaDataFragment
): dimension is GeoCoordinatesDimension => {
return dimension?.__typename === "GeoCoordinatesDimension";
};

export const isOrdinalDimension = (
dimension?: DimensionMetaDataFragment
): dimension is GeoCoordinatesDimension => {
return dimension?.__typename === "GeoCoordinatesDimension";
};

export const isGeoCoordinatesDimension = (
dimension?: DimensionMetaDataFragment
): dimension is GeoCoordinatesDimension => {
Expand Down
5 changes: 5 additions & 0 deletions app/lib/has.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type Has<T extends unknown, R extends string> = T extends {
[k in R]: any;
}
? T
: never;
3 changes: 3 additions & 0 deletions app/utils/dimension-hierarchy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ type TreeSorters = Record<

/** Recursively sorts tree children */
export const sortTree = (tree: HierarchyValue[], sorters?: TreeSorters) => {
if (!tree.length) {
return;
}
const dimensionIri = tree[0].dimensionIri;
const sorter = sorters?.[dimensionIri] || defaultSorter;
if (dimensionIri && sorter) {
Expand Down