terriajs
Version:
Geospatial data visualization platform.
1,441 lines (1,334 loc) • 102 kB
text/typescript
import i18next from "i18next";
import {
action,
computed,
IReactionDisposer,
makeObservable,
observable,
ObservableMap,
reaction,
runInAction
} from "mobx";
import filterOutUndefined from "../../Core/filterOutUndefined";
import isDefined from "../../Core/isDefined";
import TerriaError from "../../Core/TerriaError";
import ConstantColorMap from "../../Map/ColorMap/ConstantColorMap";
import ContinuousColorMap from "../../Map/ColorMap/ContinuousColorMap";
import DiscreteColorMap from "../../Map/ColorMap/DiscreteColorMap";
import EnumColorMap from "../../Map/ColorMap/EnumColorMap";
import { allIcons, getMakiIcon } from "../../Map/Icons/Maki/MakiIcons";
import ProtomapsImageryProvider from "../../Map/ImageryProvider/ProtomapsImageryProvider";
import { getName } from "../../ModelMixins/CatalogMemberMixin";
import { ImageryParts, isDataSource } from "../../ModelMixins/MappableMixin";
import TableMixin from "../../ModelMixins/TableMixin";
import {
QualitativeColorSchemeOptionRenderer,
QuantitativeColorSchemeOptionRenderer
} from "../../ReactViews/SelectableDimensions/ColorSchemeOptionRenderer";
import { MarkerOptionRenderer } from "../../ReactViews/SelectableDimensions/MarkerOptionRenderer";
import Icon from "../../Styled/Icon";
import {
DEFAULT_DIVERGING,
DEFAULT_QUALITATIVE,
DEFAULT_SEQUENTIAL,
DIVERGING_SCALES,
QUALITATIVE_SCALES,
SEQUENTIAL_CONTINUOUS_SCALES,
SEQUENTIAL_SCALES
} from "../../Table/TableColorMap";
import TableColumnType from "../../Table/TableColumnType";
import TableStyleMap from "../../Table/TableStyleMap";
import ModelTraits from "../../Traits/ModelTraits";
import { EnumColorTraits } from "../../Traits/TraitsClasses/Table/ColorStyleTraits";
import CommonStrata from "../Definition/CommonStrata";
import Model from "../Definition/Model";
import ModelPropertiesFromTraits from "../Definition/ModelPropertiesFromTraits";
import updateModelFromJson from "../Definition/updateModelFromJson";
import {
FlatSelectableDimension,
SelectableDimensionButton,
SelectableDimensionColor,
SelectableDimensionGroup,
SelectableDimensionNumeric,
SelectableDimensionText
} from "../SelectableDimensions/SelectableDimensions";
import ViewingControls from "../ViewingControls";
import SelectableDimensionWorkflow, {
SelectableDimensionWorkflowGroup
} from "../Workflows/SelectableDimensionWorkflow";
/** The ColorSchemeType is used to change which SelectableDimensions are shown.
* It is basically the "mode" of the TableStylingWorkflow
*
* For example - if we are using "sequential-continuous" - then the dimensions will be shown to configure the following:
* - Sequential color scales
* - Minimum/Maximum values
*/
type ColorSchemeType =
| "no-style"
| "sequential-continuous"
| "sequential-discrete"
| "diverging-continuous"
| "diverging-discrete"
| "custom-discrete"
| "qualitative"
| "custom-qualitative";
type StyleType =
| "fill"
| "point-size"
| "point"
| "outline"
| "label"
| "trail";
/** Columns/Styles with the following TableColumnTypes will be hidden unless we are showing "advanced" options */
export const ADVANCED_TABLE_COLUMN_TYPES = [
TableColumnType.latitude,
TableColumnType.longitude,
TableColumnType.region,
TableColumnType.time
];
/** SelectableDimensionWorkflow to set styling options for TableMixin models */
export default class TableStylingWorkflow implements SelectableDimensionWorkflow {
static type = "table-styling";
/** This is used to simplify SelectableDimensions available to the user.
* For example - if equal to `diverging-continuous` - then only Diverging continuous color scales will be presented as options
* See setColorSchemeTypeFromPalette and setColorSchemeType for how this is set. */
@observable colorSchemeType: ColorSchemeType | undefined;
@observable styleType: StyleType = "fill";
/** Which bin is currently open in `binMaximumsSelectableDims` or `enumColorsSelectableDim`.
* This is used in `SelectableDimensionGroup.onToggle` and `SelectableDimensionGroup.isOpen` to make the groups act like an accordion - so only one bin can be edited at any given time.
*/
@observable
private readonly openBinIndex = new ObservableMap<
StyleType,
number | undefined
>();
private activeStyleDisposer: IReactionDisposer;
constructor(readonly item: TableMixin.Instance) {
makeObservable(this);
// We need to reset colorSchemeType every time Table.activeStyle changes
this.activeStyleDisposer = reaction(
() => this.item.activeStyle,
() => {
// If the active style is of "advanced" TableColumnType, then set colorSchemeType to "no-style"
if (
isDefined(this.item.activeTableStyle.colorColumn) &&
ADVANCED_TABLE_COLUMN_TYPES.includes(
this.item.activeTableStyle.colorColumn.type
)
) {
runInAction(() => (this.colorSchemeType = "no-style"));
} else {
this.setColorSchemeTypeFromPalette();
}
}
);
this.setColorSchemeTypeFromPalette();
}
onClose() {
this.activeStyleDisposer();
}
@computed
get menu() {
return {
options: filterOutUndefined([
{
text: this.showAdvancedOptions
? i18next.t("models.tableStyling.hideAdvancedOptions")
: i18next.t("models.tableStyling.showAdvancedOptions"),
onSelect: action(() => {
this.showAdvancedOptions = !this.showAdvancedOptions;
})
},
this.showAdvancedOptions
? {
text: i18next.t("models.tableStyling.copyUserStratum"),
onSelect: () => {
const stratum = JSON.stringify(
this.item.strata.get(CommonStrata.user)
);
try {
navigator.clipboard.writeText(
JSON.stringify(this.item.strata.get(CommonStrata.user))
);
} catch (e) {
TerriaError.from(e).raiseError(
this.item.terria,
"Failed to copy to clipboard. User stratum has been printed to console"
);
console.log(stratum);
}
},
disable: !this.showAdvancedOptions
}
: undefined
])
};
}
get name() {
return i18next.t("models.tableStyling.name");
}
get icon() {
return Icon.GLYPHS.layers;
}
get footer() {
return {
buttonText: i18next.t("models.tableStyling.reset"),
/** Delete all user strata values for TableColumnTraits and TableStyleTraits for the current activeStyle */
onClick: action(() => {
this.getTableColumnTraits(CommonStrata.user)?.strata.delete(
CommonStrata.user
);
this.getTableStyleTraits(CommonStrata.user)?.strata.delete(
CommonStrata.user
);
this.setColorSchemeTypeFromPalette();
})
};
}
/** This will look at the current `colorMap` and `colorPalette` to guess which `colorSchemeType` is active.
* This is because `TableMixin` doesn't have an explicit `"colorSchemeType"` flag - it will choose the appropriate type based on `TableStyleTraits`
* `colorTraits.colorPalette` is also set here if we are only using `tableColorMap.defaultColorPaletteName`
*/
@action
setColorSchemeTypeFromPalette(): void {
const colorMap = this.tableStyle.colorMap;
const colorPalette = this.tableStyle.colorTraits.colorPalette;
const defaultColorPalette =
this.tableStyle.tableColorMap.defaultColorPaletteName;
const colorPaletteWithDefault = colorPalette ?? defaultColorPalette;
this.colorSchemeType = undefined;
if (colorMap instanceof ConstantColorMap) {
this.colorSchemeType = "no-style";
} else if (colorMap instanceof ContinuousColorMap) {
if (
SEQUENTIAL_SCALES.includes(colorPaletteWithDefault) ||
SEQUENTIAL_CONTINUOUS_SCALES.includes(colorPaletteWithDefault)
) {
this.colorSchemeType = "sequential-continuous";
if (!colorPalette) {
this.getTableStyleTraits(CommonStrata.user)?.color.setTrait(
CommonStrata.user,
"colorPalette",
DEFAULT_SEQUENTIAL
);
}
} else if (DIVERGING_SCALES.includes(colorPaletteWithDefault)) {
this.colorSchemeType = "diverging-continuous";
if (!colorPalette) {
this.getTableStyleTraits(CommonStrata.user)?.color.setTrait(
CommonStrata.user,
"colorPalette",
DEFAULT_DIVERGING
);
}
}
} else if (colorMap instanceof DiscreteColorMap) {
if (
this.tableStyle.colorTraits.binColors &&
this.tableStyle.colorTraits.binColors.length > 0
) {
this.colorSchemeType = "custom-discrete";
} else if (SEQUENTIAL_SCALES.includes(colorPaletteWithDefault)) {
this.colorSchemeType = "sequential-discrete";
if (!colorPalette) {
this.getTableStyleTraits(CommonStrata.user)?.color.setTrait(
CommonStrata.user,
"colorPalette",
DEFAULT_SEQUENTIAL
);
}
} else if (DIVERGING_SCALES.includes(colorPaletteWithDefault)) {
this.colorSchemeType = "diverging-discrete";
if (!colorPalette) {
this.getTableStyleTraits(CommonStrata.user)?.color.setTrait(
CommonStrata.user,
"colorPalette",
DEFAULT_DIVERGING
);
}
}
} else if (
colorMap instanceof EnumColorMap &&
QUALITATIVE_SCALES.includes(colorPaletteWithDefault)
) {
if (
this.tableStyle.colorTraits.enumColors &&
this.tableStyle.colorTraits.enumColors.length > 0
) {
this.colorSchemeType = "custom-qualitative";
} else {
this.colorSchemeType = "qualitative";
if (!colorPalette) {
this.getTableStyleTraits(CommonStrata.user)?.color.setTrait(
CommonStrata.user,
"colorPalette",
DEFAULT_QUALITATIVE
);
}
}
}
}
/** Handle change on colorType - this is called by the "Type" selectable dimension in `this.colorSchemeSelectableDim` */
@action setColorSchemeType(
stratumId: string,
id: ColorSchemeType | string | undefined
) {
if (!id) return;
// Set `activeStyle` trait so the value doesn't change
this.item.setTrait(stratumId, "activeStyle", this.tableStyle.id);
// Hide any open bin
this.openBinIndex.clear();
// Here we use item.activeTableStyle.colorTraits.colorPalette instead of this.colorPalette because we only want this to be defined, if the trait is defined - we don't care about defaultColorPaletteName
const colorPalette = this.tableStyle.colorTraits.colorPalette;
// **Discrete color maps**
// Reset bins
if (id === "sequential-discrete" || id === "diverging-discrete") {
this.colorSchemeType = id;
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"mapType",
"bin"
);
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"binColors",
[]
);
// Set numberOfBins according to limits of sequential and diverging color scales:
// - Sequential is [3,9]
// - Diverging is [3,11]
// If numberOfBins is 0 - set to sensible default (7)
if (this.tableStyle.colorTraits.numberOfBins === 0) {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"numberOfBins",
7
);
} else if (
id === "sequential-discrete" &&
this.tableStyle.tableColorMap.binColors.length > 9
) {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"numberOfBins",
9
);
} else if (
id === "diverging-discrete" &&
this.tableStyle.tableColorMap.binColors.length > 11
) {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"numberOfBins",
11
);
} else if (this.tableStyle.tableColorMap.binColors.length < 3) {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"numberOfBins",
3
);
}
// If no user stratum - reset bin maximums
if (!this.tableStyle.colorTraits.strata.get(stratumId)?.binMaximums) {
this.resetBinMaximums(stratumId);
}
}
// **Continuous color maps**
else if (id === "sequential-continuous" || id === "diverging-continuous") {
this.colorSchemeType = id;
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"mapType",
"continuous"
);
}
// **Qualitative (enum) color maps**
else if (id === "qualitative") {
this.colorSchemeType = id;
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"mapType",
"enum"
);
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"enumColors",
[]
);
}
// **No style (constant) color maps**
else if (id === "no-style") {
this.colorSchemeType = id;
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"mapType",
"constant"
);
}
// If the current colorPalette is incompatible with the selected type - change colorPalette to default for the selected type
if (
id === "sequential-continuous" &&
(!colorPalette ||
![...SEQUENTIAL_SCALES, ...SEQUENTIAL_CONTINUOUS_SCALES].includes(
colorPalette
))
) {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"colorPalette",
DEFAULT_SEQUENTIAL
);
}
if (
id === "sequential-discrete" &&
(!colorPalette || !SEQUENTIAL_SCALES.includes(colorPalette))
) {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"colorPalette",
DEFAULT_SEQUENTIAL
);
}
if (
(id === "diverging-continuous" || id === "diverging-discrete") &&
(!colorPalette || !DIVERGING_SCALES.includes(colorPalette))
) {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"colorPalette",
DEFAULT_DIVERGING
);
}
if (
id === "qualitative" &&
(!colorPalette || !QUALITATIVE_SCALES.includes(colorPalette))
) {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"colorPalette",
DEFAULT_QUALITATIVE
);
}
}
/** Show advances options
* - Show all column types in "Data" select
* - Show "Data type (advanced)" select. This allow user to change column type */
@observable showAdvancedOptions: boolean = false;
@computed get hasCesiumDataSource() {
return !!this.item.mapItems.find(
(d) => isDataSource(d) && d.entities.values.length > 0
);
}
@computed get hasProtomapsImageryProvider() {
return !!this.item.mapItems.find(
(d) =>
ImageryParts.is(d) &&
d.imageryProvider instanceof ProtomapsImageryProvider
);
}
/** Table Style dimensions:
* - Dataset (Table models in workbench)
* - Variable (Table style in model)
* - TableColumn type (advanced only)
*/
@computed get tableStyleSelectableDim(): SelectableDimensionWorkflowGroup {
// Show point style options if current catalog item has any points showing (or uses ProtomapsImageryProvider)
const showPointStyles =
this.hasCesiumDataSource || this.hasProtomapsImageryProvider;
// Show point size options if current catalog item has points and any scalar columns
const showPointSize =
this.hasCesiumDataSource &&
(this.tableStyle.pointSizeColumn ||
this.item.tableColumns.find((t) => t.type === TableColumnType.scalar));
// Show label style options if current catalog item has points
const showLabelStyles = this.hasCesiumDataSource;
// Show trail style options if current catalog item has time-series points
const showTrailStyles =
this.hasCesiumDataSource && this.tableStyle.isTimeVaryingPointsWithId();
return {
type: "group",
id: "data",
name: i18next.t("models.tableStyling.data.name"),
selectableDimensions: filterOutUndefined([
{
type: "select",
id: "dataset",
name: i18next.t(
"models.tableStyling.data.selectableDimensions.dataset.name"
),
selectedId: this.item.uniqueId,
// Find all workbench items which have TableStylingWorkflow
options: this.item.terria.workbench.items
.filter(
(item) =>
ViewingControls.is(item) &&
item.viewingControls.find(
(control) => control.id === TableStylingWorkflow.type
)
)
.map((item) => ({
id: item.uniqueId,
name: getName(item)
})),
setDimensionValue: (_stratumId, value) => {
const item = this.item.terria.workbench.items.find(
(i) => i.uniqueId === value
);
if (item && TableMixin.isMixedInto(item)) {
// Trigger new TableStylingWorkflow
if (
item.viewingControls.find(
(control) => control.id === TableStylingWorkflow.type
)
) {
item.terria.selectableDimensionWorkflow =
new TableStylingWorkflow(item);
}
}
}
},
{
type: "select",
id: "table-style",
name: i18next.t(
"models.tableStyling.data.selectableDimensions.tableStyle.name"
),
selectedId: this.tableStyle.id,
options: this.item.tableStyles.map((style) => ({
id: style.id,
name: style.title
})),
allowCustomInput: true,
setDimensionValue: (stratumId, value) => {
if (!this.item.tableStyles.find((style) => style.id === value)) {
this.getTableStyleTraits(stratumId, value);
}
this.item.setTrait(stratumId, "activeStyle", value);
// Note - the activeStyle reaction in TableStylingWorkflow.constructor handles all side effects
// The reaction will call this.setColorSchemeTypeFromPalette()
}
},
{
type: "select",
id: "table-style-type",
name: i18next.t(
"models.tableStyling.data.selectableDimensions.tableStyleType.name"
),
selectedId: this.styleType,
options: filterOutUndefined([
{
id: "fill",
name: i18next.t(
"models.tableStyling.data.selectableDimensions.tableStyleType.options.fill.name"
)
},
showPointSize
? {
id: "point-size",
name: i18next.t(
"models.tableStyling.data.selectableDimensions.tableStyleType.options.pointSize.name"
)
}
: undefined,
showPointStyles
? {
id: "point",
name: i18next.t(
"models.tableStyling.data.selectableDimensions.tableStyleType.options.point.name"
)
}
: undefined,
{
id: "outline",
name: i18next.t(
"models.tableStyling.data.selectableDimensions.tableStyleType.options.outline.name"
)
},
showLabelStyles
? {
id: "label",
name: i18next.t(
"models.tableStyling.data.selectableDimensions.tableStyleType.options.label.name"
)
}
: undefined,
showTrailStyles
? {
id: "trail",
name: i18next.t(
"models.tableStyling.data.selectableDimensions.tableStyleType.options.trail.name"
)
}
: undefined
]),
setDimensionValue: (_stratumId, value) => {
if (
value === "fill" ||
value === "point-size" ||
value === "point" ||
value === "outline" ||
value === "trail" ||
value === "label"
)
this.styleType = value;
}
}
])
};
}
/** List of color schemes available for given `colorSchemeType` */
@computed get colorSchemesForType() {
const type = this.colorSchemeType;
if (!isDefined(type)) return [];
if (type === "sequential-continuous")
return [...SEQUENTIAL_SCALES, ...SEQUENTIAL_CONTINUOUS_SCALES];
if (type === "sequential-discrete") return SEQUENTIAL_SCALES;
if (type === "diverging-discrete" || type === "diverging-continuous")
return DIVERGING_SCALES;
if (type === "qualitative") return QUALITATIVE_SCALES;
return [];
}
/** Color scheme dimensions:
* - Type (see `this.colorSchemeType`)
* - Color scheme (see `this.colorSchemesForType`)
* - Number of bins (for discrete)
*/
@computed get colorSchemeSelectableDim(): SelectableDimensionWorkflowGroup {
return {
type: "group",
id: "fill",
name: i18next.t("models.tableStyling.fill.name"),
selectableDimensions: filterOutUndefined([
// Show "Variable" selector to pick colorColumn if tableStyle ID is different from colorColumn ID
// OR if we are in advanced mode
this.tableStyle.id !== this.tableStyle.colorColumn?.name ||
this.showAdvancedOptions
? {
type: "select",
id: "table-color-column",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.tableColorColumn.name"
),
selectedId: this.tableStyle.colorColumn?.name,
options: this.item.tableColumns.map((col) => ({
id: col.name,
name: col.title
})),
setDimensionValue: (stratumId, value) => {
// Make sure `activeStyle` is set, otherwise it may change when we change `colorColumn`
this.item.setTrait(
stratumId,
"activeStyle",
this.tableStyle.id
);
const prevColumnType = this.tableStyle.colorColumn?.type;
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"colorColumn",
value
);
const newColumnType = this.tableStyle.colorColumn?.type;
// Reset color palette and color scheme type if color column type has changed
// For example, if the color column type changes from `scalar` to `enum` - we don't want to still have a `scalar` color palette
if (prevColumnType !== newColumnType) {
this.tableStyle.colorTraits.setTrait(
stratumId,
"colorPalette",
undefined
);
this.setColorSchemeTypeFromPalette();
}
}
}
: undefined,
this.showAdvancedOptions
? {
type: "select",
id: "data-type",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.dataType.name"
),
options: Object.keys(TableColumnType)
.filter((type) => type.length > 1)
.map((colType) => ({ id: colType })),
selectedId: isDefined(this.tableStyle.colorColumn?.type)
? TableColumnType[this.tableStyle.colorColumn!.type]
: undefined,
setDimensionValue: (stratumId, id) => {
this.getTableColumnTraits(stratumId)?.setTrait(
stratumId,
"type",
id
);
this.setColorSchemeTypeFromPalette();
}
}
: undefined,
this.tableStyle.colorColumn
? {
type: "select",
id: "type",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.type.name"
),
undefinedLabel: i18next.t(
"models.tableStyling.fill.selectableDimensions.type.undefinedLabel"
),
options: filterOutUndefined([
{
id: "no-style",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.type.options.noStyle.name"
)
},
...(this.tableStyle.colorColumn.type === TableColumnType.scalar
? [
{
id: "sequential-continuous",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.type.options.sequentialContinuous.name"
)
},
{
id: "sequential-discrete",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.type.options.sequentialDiscrete.name"
)
},
{
id: "diverging-continuous",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.type.options.divergingContinuous.name"
)
},
{
id: "diverging-discrete",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.type.options.divergingDiscrete.name"
)
}
]
: []),
{
id: "qualitative",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.type.options.qualitative.name"
)
},
// Add options for "custom" color palettes if we are in "custom-qualitative" or "custom-discrete" mode
this.colorSchemeType === "custom-qualitative"
? {
id: "custom-qualitative",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.type.options.customQualitative.name"
)
}
: undefined,
this.colorSchemeType === "custom-discrete"
? {
id: "custom-discrete",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.type.options.customDiscrete.name"
)
}
: undefined
]),
selectedId: this.colorSchemeType,
setDimensionValue: (stratumId, id) => {
this.setColorSchemeType(stratumId, id);
}
}
: undefined,
{
type: "select",
id: "scheme",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.scheme.name"
),
selectedId:
this.tableStyle.colorTraits.colorPalette ??
this.tableStyle.tableColorMap.defaultColorPaletteName,
options: this.colorSchemesForType.map((style) => ({
id: style
})),
optionRenderer:
this.colorSchemeType === "qualitative"
? QualitativeColorSchemeOptionRenderer
: QuantitativeColorSchemeOptionRenderer(
this.colorSchemeType === "sequential-discrete" ||
this.colorSchemeType === "diverging-discrete"
? this.tableStyle.tableColorMap.binColors.length
: undefined
),
setDimensionValue: (stratumId, id) => {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"colorPalette",
id
);
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"binColors",
[]
);
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"enumColors",
[]
);
}
},
// Show "Number of Bins" if in discrete mode
this.colorSchemeType === "sequential-discrete" ||
this.colorSchemeType === "diverging-discrete"
? {
type: "numeric",
id: "number-of-bins",
name: i18next.t(
"models.tableStyling.fill.selectableDimensions.numberOfBins.name"
),
allowUndefined: true,
min:
// Sequential and diverging color scales must have at least 3 bins
this.colorSchemeType === "sequential-discrete" ||
this.colorSchemeType === "diverging-discrete"
? 3
: // Custom color scales only need at least 1
1,
max:
// Sequential discrete color scales support up to 9 bins
this.colorSchemeType === "sequential-discrete"
? 9
: // Diverging discrete color scales support up to 11 bins
this.colorSchemeType === "diverging-discrete"
? 11
: // Custom discrete color scales can be any number of bins
undefined,
value: this.tableStyle.colorTraits.numberOfBins,
setDimensionValue: (stratumId, value) => {
if (!isDefined(value)) return;
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"numberOfBins",
value
);
this.resetBinMaximums(stratumId);
}
}
: undefined
])
};
}
@computed get minimumValueSelectableDim(): SelectableDimensionNumeric {
return {
type: "numeric",
id: "min",
name: i18next.t("models.tableStyling.min.name"),
max: this.tableStyle.tableColorMap.maximumValue,
value: this.tableStyle.tableColorMap.minimumValue,
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"minimumValue",
value
);
if (
this.colorSchemeType === "sequential-discrete" ||
this.colorSchemeType === "diverging-discrete" ||
this.colorSchemeType === "custom-discrete"
) {
this.setBinMaximums(stratumId);
}
}
};
}
/** Display range dimensions:
* - Minimum value
* - Maximum value
*/
@computed get displayRangeDim(): SelectableDimensionWorkflowGroup {
return {
type: "group",
id: "display-range",
name: i18next.t("models.tableStyling.displayRange.name"),
isOpen: false,
selectableDimensions: filterOutUndefined([
this.minimumValueSelectableDim,
{
type: "numeric",
id: "max",
name: i18next.t(
"models.tableStyling.displayRange.selectableDimensions.max.name"
),
min: this.tableStyle.tableColorMap.minimumValue,
value: this.tableStyle.tableColorMap.maximumValue,
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"maximumValue",
value
);
}
},
this.item.outlierFilterDimension
])
};
}
/** Group to show bins with color, start/stop numbers.
*/
@computed get binColorDims(): SelectableDimensionWorkflowGroup {
return {
type: "group",
id: "bins",
name: i18next.t("models.tableStyling.bins.name"),
isOpen: false,
selectableDimensions: [
...this.tableStyle.tableColorMap.binMaximums
.map(
(bin, idx) =>
({
type: "group",
id: `bin-${idx}-start`,
name: getColorPreview(
this.tableStyle.tableColorMap.binColors[idx] ?? "#aaa",
i18next.t(
"models.tableStyling.bins.selectableDimensions.start.name",
{
value1:
idx === 0
? this.minimumValueSelectableDim.value
: this.tableStyle.tableColorMap.binMaximums[idx - 1],
value2: bin
}
)
),
isOpen: this.openBinIndex.get("fill") === idx,
onToggle: (open) => {
if (open && this.openBinIndex.get("fill") !== idx) {
runInAction(() => this.openBinIndex.set("fill", idx));
return true;
}
},
selectableDimensions: [
{
type: "color",
id: `bin-${idx}-color`,
name: i18next.t(
"models.tableStyling.bins.selectableDimensions.start.selectableDimensions.color.name"
),
value: this.tableStyle.tableColorMap.binColors[idx],
setDimensionValue: (stratumId, value) => {
const binColors = [
...this.tableStyle.tableColorMap.binColors
];
if (isDefined(value)) binColors[idx] = value;
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"binColors",
binColors
);
this.colorSchemeType = "custom-discrete";
}
},
idx === 0
? this.minimumValueSelectableDim
: {
type: "numeric",
id: `bin-${idx}-start`,
name: i18next.t(
"models.tableStyling.bins.selectableDimensions.start.selectableDimensions.start.name"
),
value:
this.tableStyle.tableColorMap.binMaximums[idx - 1],
setDimensionValue: (stratumId, value) => {
const binMaximums = [
...this.tableStyle.tableColorMap.binMaximums
];
if (isDefined(value)) binMaximums[idx - 1] = value;
this.setBinMaximums(stratumId, binMaximums);
}
},
{
type: "numeric",
id: `bin-${idx}-stop`,
name: i18next.t(
"models.tableStyling.bins.selectableDimensions.start.selectableDimensions.stop.name"
),
value: bin,
setDimensionValue: (stratumId, value) => {
const binMaximums = [
...this.tableStyle.tableColorMap.binMaximums
];
if (isDefined(value)) binMaximums[idx] = value;
this.setBinMaximums(stratumId, binMaximums);
}
}
]
}) as SelectableDimensionGroup
)
.reverse() // Reverse array of bins to match Legend (descending order)
]
};
}
/** Groups to show enum "bins" with colors and value */
@computed get enumColorDims(): SelectableDimensionWorkflowGroup {
return {
type: "group",
id: "colors",
name: i18next.t("models.tableStyling.colors.name"),
isOpen: false,
selectableDimensions: filterOutUndefined([
...this.tableStyle.tableColorMap.enumColors.map((enumCol, idx) => {
if (!enumCol.value) return;
const dims: SelectableDimensionGroup = {
type: "group",
id: `enum-${idx}-start`,
name: getColorPreview(enumCol.color ?? "#aaa", enumCol.value),
isOpen: this.openBinIndex.get("fill") === idx,
onToggle: (open) => {
if (open && this.openBinIndex.get("fill") !== idx) {
runInAction(() => this.openBinIndex.set("fill", idx));
return true;
}
},
selectableDimensions: [
{
type: "color",
id: `enum-${idx}-color`,
name: i18next.t(
"models.tableStyling.colors.selectableDimensions.color.name"
),
value: enumCol.color,
setDimensionValue: (stratumId, value) => {
this.colorSchemeType = "custom-qualitative";
this.setEnumColorTrait(stratumId, idx, enumCol.value, value);
}
},
{
type: "select",
id: `enum-${idx}-value`,
name: i18next.t(
"models.tableStyling.colors.selectableDimensions.value.name"
),
selectedId: enumCol.value,
// Find unique column values which don't already have an enumCol
// We prepend the current enumCol.value
options: [
{ id: enumCol.value },
...(this.tableStyle.colorColumn?.uniqueValues.values
?.filter(
(value) =>
!this.tableStyle.tableColorMap.enumColors.find(
(enumCol) => enumCol.value === value
)
)
.map((id) => ({ id })) ?? [])
],
setDimensionValue: (stratumId, value) => {
this.colorSchemeType = "custom-qualitative";
this.setEnumColorTrait(stratumId, idx, value, enumCol.color);
}
},
{
type: "button",
id: `enum-${idx}-remove`,
value: i18next.t(
"models.tableStyling.colors.selectableDimensions.remove.value"
),
setDimensionValue: (stratumId) => {
this.colorSchemeType = "custom-qualitative";
// Remove element by clearing `value`
this.setEnumColorTrait(stratumId, idx, undefined, undefined);
}
}
]
};
return dims;
}),
// Are there more colors to add (are there more unique values in the column than enumCols)
// Create "Add" to user can add more
this.tableStyle.colorColumn &&
this.tableStyle.tableColorMap.enumColors.filter((col) => col.value)
.length < this.tableStyle.colorColumn?.uniqueValues.values.length
? {
type: "button",
id: `enum-add`,
value: i18next.t(
"models.tableStyling.colors.selectableDimensions.add.value"
),
setDimensionValue: (stratumId) => {
this.colorSchemeType = "custom-qualitative";
const firstValue =
this.tableStyle.colorColumn?.uniqueValues.values.find(
(value) =>
!this.tableStyle.tableColorMap.enumColors.find(
(col) => col.value === value
)
);
if (!isDefined(firstValue)) return;
// Can we find any unused colors in the colorPalette
const unusedColor = this.tableStyle.tableColorMap
.colorScaleCategorical(
this.tableStyle.tableColorMap.enumColors.length + 1
)
.find(
(col) =>
!this.tableStyle.tableColorMap.enumColors.find(
(enumColor) => enumColor.color === col
)
);
this.setEnumColorTrait(
stratumId,
this.tableStyle.tableColorMap.enumColors.length,
firstValue,
unusedColor ?? "#000000"
);
this.openBinIndex.set(
"fill",
this.tableStyle.tableColorMap.enumColors.length - 1
);
}
}
: undefined
])
};
}
@computed get nullColorDimension(): SelectableDimensionColor {
return {
type: "color",
id: `null-color`,
name: i18next.t("models.tableStyling.nullColor.name"),
value: this.tableStyle.colorTraits.nullColor,
allowUndefined: true,
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"nullColor",
value
);
}
};
}
@computed get outlierColorDimension(): SelectableDimensionColor {
return {
type: "color",
id: `outlier-color`,
name: i18next.t("models.tableStyling.outlierColor.name"),
allowUndefined: true,
value:
this.tableStyle.colorTraits.outlierColor ??
this.tableStyle.tableColorMap.outlierColor?.toCssHexString(),
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"outlierColor",
value
);
}
};
}
/** Misc table style color dimensions:
* - Region color
* - Null color
* - Outlier color
*/
@computed get additionalColorDimensions(): SelectableDimensionGroup {
return {
type: "group",
id: "additional-colors",
name: i18next.t("models.tableStyling.additionalColors.name"),
// Open group by default is no activeStyle is selected
isOpen: !this.item.activeStyle || this.colorSchemeType === "no-style",
selectableDimensions: filterOutUndefined([
this.tableStyle.colorColumn?.type === TableColumnType.region
? {
type: "color",
id: `region-color`,
name: i18next.t(
"models.tableStyling.additionalColors.selectableDimensions.regionColor.name"
),
value: this.tableStyle.colorTraits.regionColor,
allowUndefined: true,
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"regionColor",
value
);
}
}
: {
type: "color",
id: `null-color`,
name: i18next.t(
"models.tableStyling.additionalColors.selectableDimensions.nullColor.name"
),
value: this.tableStyle.colorTraits.nullColor,
allowUndefined: true,
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"nullColor",
value
);
}
},
this.tableStyle.colorColumn?.type === TableColumnType.scalar
? {
type: "color",
id: `outlier-color`,
name: i18next.t(
"models.tableStyling.additionalColors.selectableDimensions.outlierColor.name"
),
allowUndefined: true,
value:
this.tableStyle.colorTraits.outlierColor ??
this.tableStyle.tableColorMap.outlierColor?.toCssHexString(),
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.color.setTrait(
stratumId,
"outlierColor",
value
);
}
}
: undefined
])
};
}
@computed get pointSizeDimensions(): SelectableDimensionGroup {
return {
type: "group",
id: "point-size",
name: i18next.t("models.tableStyling.pointSize.name"),
isOpen: true,
selectableDimensions: [
{
type: "select",
id: `point-size-column`,
name: i18next.t(
"models.tableStyling.pointSize.selectableDimensions.pointSizeColumn.name"
),
selectedId: this.tableStyle.pointSizeTraits.pointSizeColumn,
options: this.item.tableColumns
.filter((col) => col.type === TableColumnType.scalar)
.map((col) => ({
id: col.name,
name: col.title
})),
allowUndefined: true,
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.pointSize.setTrait(
stratumId,
"pointSizeColumn",
value
);
}
},
...((this.tableStyle.pointSizeColumn
? [
{
type: "numeric",
id: "point-size-null",
name: i18next.t(
"models.tableStyling.pointSize.selectableDimensions.pointSizeNull.name"
),
min: 0,
value: this.tableStyle.pointSizeTraits.nullSize,
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.pointSize.setTrait(
stratumId,
"nullSize",
value
);
}
},
{
type: "numeric",
id: "point-sizes-factor",
name: i18next.t(
"models.tableStyling.pointSize.selectableDimensions.pointSizesFactor.name"
),
min: 0,
value: this.tableStyle.pointSizeTraits.sizeFactor,
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.pointSize.setTrait(
stratumId,
"sizeFactor",
value
);
}
},
{
type: "numeric",
id: "point-size-offset",
name: i18next.t(
"models.tableStyling.pointSize.selectableDimensions.pointSizeOffset.name"
),
min: 0,
value: this.tableStyle.pointSizeTraits.sizeOffset,
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.pointSize.setTrait(
stratumId,
"sizeOffset",
value
);
}
}
]
: []) as SelectableDimensionNumeric[])
]
};
}
/** Advanced region mapping - pulled from TableMixin.regionColumnDimensions and TableMixin.regionProviderDimensions
*/
@computed
get advancedRegionMappingDimensions(): SelectableDimensionWorkflowGroup {
return {
type: "group",
id: "region-mapping",
name: i18next.t("models.tableStyling.regionMapping.name"),
isOpen: false,
selectableDimensions: filterOutUndefined([
this.item.regionColumnDimensions,
this.item.regionProviderDimensions
])
};
}
/** Advanced table dimensions:
* - Legend (title, ticks, item titles...)
* - Style options (title, lat/lon columns)
* - Time options (column, id columns, spread start/finish time, ...)
* - Workbench options (show style selector, show disable style/time in workbench, ...)
* - Variable/Column options (title, units)
*/
@computed
get advancedTableDimensions(): SelectableDimensionWorkflowGroup[] {
return [
{
type: "group",
id: "legend",
name: i18next.t("models.tableStyling.legend.name"),
isOpen: false,
selectableDimensions: filterOutUndefined([
{
type: "text",
id: "legend-title",
name: i18next.t(
"models.tableStyling.legend.selectableDimensions.legendTitle.name"
),
value: this.tableStyle.colorTraits.legend.title,
setDimensionValue: (stratumId, value) => {
this.getTableStyleTraits(stratumId)?.color.legend.setTrait(
stratumId,
"title",
value
);
}
},
this.colorSchemeType === "diverging-continuous" ||
this.colorSchemeType === "sequential-continuous"
? {
type: "numeric",
i