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

Experiment: Add table column filtering selectable dimensions #6421

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
158 changes: 137 additions & 21 deletions lib/ModelMixins/TableMixin.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import i18next from "i18next";
import {
action,
computed,
isObservableArray,
observable,
runInAction
} from "mobx";
import { action, computed, observable, runInAction } from "mobx";
import { createTransformer, ITransformer } from "mobx-utils";
import DeveloperError from "terriajs-cesium/Source/Core/DeveloperError";
import JulianDate from "terriajs-cesium/Source/Core/JulianDate";
Expand All @@ -18,7 +12,7 @@ import Constructor from "../Core/Constructor";
import filterOutUndefined from "../Core/filterOutUndefined";
import flatten from "../Core/flatten";
import isDefined from "../Core/isDefined";
import { JsonObject } from "../Core/Json";
import { isJsonNumber, JsonObject } from "../Core/Json";
import { isLatLonHeight } from "../Core/LatLonHeight";
import TerriaError from "../Core/TerriaError";
import ConstantColorMap from "../Map/ColorMap/ConstantColorMap";
Expand Down Expand Up @@ -296,6 +290,26 @@ function TableMixin<T extends Constructor<Model<TableTraits>>>(Base: T) {
);
}

@computed get numberOfPoints() {
return this.activeTableStyle.isPoints()
? this.activeTableStyle.rowGroups.length
: 0;
}

@computed get numberOfRegions() {
const regionIds = new Set<number>();
const regions =
this.activeTableStyle.regionColumn?.valuesAsRegions.regionIds;

if (!regions) return 0;

for (let i = 0; i < this.rowIds.length; i++) {
const region = regions[this.rowIds[i]];
if (isJsonNumber(region)) regionIds.add(region);
}
return regionIds.size;
}

/**
* Gets the items to show on the map.
*/
Expand All @@ -309,21 +323,12 @@ function TableMixin<T extends Constructor<Model<TableTraits>>>(Base: T) {
)
return [];

const numRegions =
this.activeTableStyle.regionColumn?.valuesAsRegions?.uniqueRegionIds
?.length ?? 0;

// Estimate number of points based off number of rowGroups
const numPoints = this.activeTableStyle.isPoints()
? this.activeTableStyle.rowGroups.length
: 0;

// If we have more points than regions OR we have points are are using a ConstantColorMap - show points instead of regions
// (Using ConstantColorMap with regions will result in all regions being the same color - which isn't useful)
if (
(numPoints > 0 &&
(this.numberOfPoints > 0 &&
this.activeTableStyle.colorMap instanceof ConstantColorMap) ||
numPoints > numRegions
this.numberOfPoints > this.numberOfRegions
) {
const pointsDataSource = this.createLongitudeLatitudeDataSource(
this.activeTableStyle
Expand All @@ -332,7 +337,7 @@ function TableMixin<T extends Constructor<Model<TableTraits>>>(Base: T) {
// Make sure there are actually more points than regions
if (
pointsDataSource &&
pointsDataSource.entities.values.length > numRegions
pointsDataSource.entities.values.length > this.numberOfRegions
)
return [pointsDataSource];
}
Expand Down Expand Up @@ -508,6 +513,7 @@ function TableMixin<T extends Constructor<Model<TableTraits>>>(Base: T) {
? this.regionMappingDimensions
: undefined,
this.styleDimensions,
this.filterDimensions,
this.outlierFilterDimension
]);
}
Expand Down Expand Up @@ -543,6 +549,75 @@ function TableMixin<T extends Constructor<Model<TableTraits>>>(Base: T) {
};
}

/**
* Takes {@link TableStyle}s and returns a SelectableDimension which can be rendered in a Select dropdown
*/
@computed
get filterDimensions(): SelectableDimensionGroup | undefined {
return {
type: "group",
id: "filter",
name: "Filter",
selectableDimensions: this.tableColumns
.filter(
(col) =>
col.traits.filter.enable &&
(!isDefined(col.traits.filter.show) || col.traits.filter.show)
)
.map((col) =>
// Use multi select if allowMultipleValues
// Otherwise use select
col.traits.filter.allowMultipleValues
? {
type: "select-multi",
id: `filter-${col.name}`,
name: col.title,
options: col.uniqueValues.values.map((value) => ({
id: value
})),
allowUndefined: col.traits.filter.allowUndefined,
selectedIds: col.traits.filter.values as string[],
setDimensionValue: (stratumId: string, values: string[]) => {
(
this.columns?.find(
(colTraits) => colTraits.name === col.name
) ?? this.addObject(stratumId, "columns", col.name)
)?.filter.setTrait(stratumId, "values", values);
}
}
: {
type: "select",
id: `filter-${col.name}`,
name: col.title,
options: col.uniqueValues.values.map((value) => ({
id: value
})),
allowUndefined: col.traits.filter.allowUndefined,
selectedId:
col.traits.filter.values?.[0] ??
// If undefined is not allowed, set selected to the first column value
(!col.traits.filter.allowUndefined
? col.uniqueValues.values[0]
: undefined),
setDimensionValue: (
stratumId: string,
value: string | undefined
) => {
(
this.columns?.find(
(colTraits) => colTraits.name === col.name
) ?? this.addObject(stratumId, "columns", col.name)
)?.filter.setTrait(
stratumId,
"values",
value ? [value] : []
);
}
}
)
};
}

/**
* Creates SelectableDimension for regionProviderList - the list of all available region providers.
* {@link TableTraits#enableManualRegionMapping} must be enabled.
Expand Down Expand Up @@ -705,10 +780,51 @@ function TableMixin<T extends Constructor<Model<TableTraits>>>(Base: T) {
};
}

/** Array of row IDs to visualise
* This takes into account column filters.
*/
@computed
get rowIds(): number[] {
const nRows = (this.dataColumnMajor?.[0]?.length || 1) - 1;
const ids = [...new Array(nRows).keys()];
const ids: number[] = [];
for (let rowId = 0; rowId < nRows; rowId++) {
let include = true;

// Apply table column filters
for (let i = 0; i < this.tableColumns.length; i++) {
const column = this.tableColumns[i];
const filter = column.traits.filter;
const rowValue = column.values[rowId];
const filterValues = filter.values ?? [];

if (filter?.enable) {
if (filterValues.length === 0) {
// If filter has no values selected - and does NOT allow undefined - then don't include row
if (!filter.allowUndefined) {
include = false;
break;
}
} else {
// Match filter with multiple values (array of selected values)
if (
filter.allowMultipleValues &&
!filterValues.some((v) => v === rowValue)
) {
include = false;
break;
}

// Match filter with single value
if (filterValues[0] !== rowValue) {
include = false;
break;
}
}
}
}

if (include) ids.push(rowId);
}
return ids;
}

Expand Down
35 changes: 35 additions & 0 deletions lib/Models/SelectableDimensions/SelectableDimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export interface EnumDimension<T = string> extends Dimension {
readonly undefinedLabel?: string;
}

export interface MultiEnumDimension<T = string> extends Dimension {
readonly options?: readonly EnumDimensionOption<T>[];
readonly selectedIds?: T[];
readonly allowUndefined?: boolean;
}

export interface NumericalDimension extends Dimension {
readonly value?: number;
readonly min?: number;
Expand All @@ -49,6 +55,7 @@ export interface ButtonDimension extends Dimension {
export type SelectableDimensionType =
| undefined
| "select"
| "select-multi"
| "numeric"
| "text"
| "checkbox"
Expand Down Expand Up @@ -85,6 +92,14 @@ export interface SelectableDimensionEnum
optionRenderer?: OptionRenderer;
}

export interface SelectableDimensionMultiEnum
extends SelectableDimensionBase<string[]>,
MultiEnumDimension {
type?: undefined | "select-multi";
/** Render ReactNodes for each option - instead of plain label */
optionRenderer?: OptionRenderer;
}

export interface SelectableDimensionCheckbox
extends SelectableDimensionBase<"true" | "false">,
EnumDimension<"true" | "false"> {
Expand Down Expand Up @@ -156,6 +171,7 @@ export type FlatSelectableDimension = Exclude<

export type SelectableDimension =
| SelectableDimensionEnum
| SelectableDimensionMultiEnum
| SelectableDimensionCheckbox
| SelectableDimensionCheckboxGroup
| SelectableDimensionGroup
Expand All @@ -169,6 +185,10 @@ export const isEnum = (
): dim is SelectableDimensionEnum =>
dim.type === "select" || dim.type === undefined;

export const isMultiEnum = (
dim: SelectableDimension
): dim is SelectableDimensionMultiEnum => dim.type === "select-multi";

/** Return only SelectableDimensionSelect from array of SelectableDimension */
export const filterEnums = (
dims: SelectableDimension[]
Expand Down Expand Up @@ -216,6 +236,11 @@ const enumHasValidOptions = (dim: EnumDimension) => {
return isDefined(dim.options) && dim.options.length >= minLength;
};

/** Multi enums just need one option (the don't have `allowUndefined`) */
const multiEnumHasValidOptions = (dim: MultiEnumDimension) => {
return isDefined(dim.options) && dim.options.length > 0;
};

/** Filter with SelectableDimension should be shown for a given placement.
* This will take into account whether SelectableDimension is valid, not disabled, etc...
*/
Expand All @@ -229,6 +254,8 @@ export const filterSelectableDimensions =
isEnabled(dim) &&
// Check enum (select and checkbox) dimensions for valid options
((!isEnum(dim) && !isCheckbox(dim)) || enumHasValidOptions(dim)) &&
// Check multi-enum
(!isMultiEnum(dim) || multiEnumHasValidOptions(dim)) &&
// Only show groups if they have at least one SelectableDimension
(!isGroup(dim) || dim.selectableDimensions.length > 0)
);
Expand All @@ -245,6 +272,14 @@ export const findSelectedValueName = (
return dim.options?.find((opt) => opt.id === dim.selectedId)?.name;
}

if (isMultiEnum(dim)) {
// return names as CSV
return dim.options
?.filter((opt) => dim.selectedIds?.some((id) => opt.id === id))
?.map((option) => option.name)
?.join(", ");
}

if (isNumeric(dim)) {
return dim.value?.toString();
}
Expand Down
55 changes: 54 additions & 1 deletion lib/ReactViews/SelectableDimensions/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import React from "react";
import ReactSelect from "react-select";
import ReactSelectCreatable from "react-select/creatable";
import { useTheme } from "styled-components";
import isDefined from "../../Core/isDefined";
import CommonStrata from "../../Models/Definition/CommonStrata";
import { SelectableDimensionEnum as SelectableDimensionEnumModel } from "../../Models/SelectableDimensions/SelectableDimensions";
import {
SelectableDimensionEnum as SelectableDimensionEnumModel,
SelectableDimensionMultiEnum as SelectableDimensionEnumMultiModel
} from "../../Models/SelectableDimensions/SelectableDimensions";

export const SelectableDimensionEnum: React.FC<{
id: string;
Expand Down Expand Up @@ -88,3 +92,52 @@ export const SelectableDimensionEnum: React.FC<{
/>
);
});

export const SelectableDimensionEnumMulti: React.FC<{
id: string;
dim: SelectableDimensionEnumMultiModel;
}> = observer(({ id, dim }) => {
const theme = useTheme();

let options = dim.options?.map((option) => ({
value: option.id,
label: option.name ?? option.id
}));

if (!options) return null;

const selectedOptions = options.filter((option) =>
dim.selectedIds?.some((id) => option.value === id)
);

return (
<ReactSelect
css={`
color: ${theme.dark};
`}
options={options}
value={selectedOptions}
onChange={(evt) => {
runInAction(() =>
dim.setDimensionValue(
CommonStrata.user,
evt?.map((selected) => selected.value).filter(isDefined) ?? []
)
);
}}
isClearable={dim.allowUndefined}
formatOptionLabel={dim.optionRenderer}
theme={(selectTheme) => ({
...selectTheme,
colors: {
...selectTheme.colors,
primary25: theme.greyLighter,
primary50: theme.colorPrimary,
primary75: theme.colorPrimary,
primary: theme.colorPrimary
}
})}
isMulti={true}
/>
);
});