From a8595249599fc177ef1c14fcb1c32f304ebc918f Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 10 May 2024 14:16:46 +0200 Subject: [PATCH 01/26] introduces new user color bar prop "categorical" --- .../ColorBarLegend/UserColorBarEditor.tsx | 30 +++++++++++++++++++ src/model/userColorBar.ts | 4 +++ 2 files changed, 34 insertions(+) diff --git a/src/components/ColorBarLegend/UserColorBarEditor.tsx b/src/components/ColorBarLegend/UserColorBarEditor.tsx index 7419da53..d312f32c 100644 --- a/src/components/ColorBarLegend/UserColorBarEditor.tsx +++ b/src/components/ColorBarLegend/UserColorBarEditor.tsx @@ -24,8 +24,10 @@ import { ChangeEvent } from "react"; import Box from "@mui/material/Box"; +import Checkbox from "@mui/material/Checkbox"; import TextField from "@mui/material/TextField"; +import i18n from "@/i18n"; import { USER_COLOR_BAR_CODE_EXAMPLE, UserColorBar, @@ -54,6 +56,13 @@ export default function UserColorBarEditor({ updateUserColorBar({ ...userColorBar, code: event.currentTarget.value }); }; + const handleCategoricalChange = () => { + updateUserColorBar({ + ...userColorBar, + categorical: !userColorBar.categorical, + }); + }; + return ( + + + {i18n.get("Categorical")} + Date: Fri, 10 May 2024 15:39:56 +0200 Subject: [PATCH 02/26] changed color bar models to support categories incl. labels --- .../ColorBarLegend/ColorBarLegend.tsx | 169 +----------------- .../ColorBarLegendCategorical.tsx | 143 +++++++++++++++ .../ColorBarLegendContinuous.tsx | 143 +++++++++++++++ src/components/ColorBarLegend/common.ts | 71 ++++++++ src/model/colorBar.ts | 13 ++ src/model/userColorBar.ts | 44 +++-- src/selectors/controlSelectors.tsx | 23 ++- 7 files changed, 424 insertions(+), 182 deletions(-) create mode 100644 src/components/ColorBarLegend/ColorBarLegendCategorical.tsx create mode 100644 src/components/ColorBarLegend/ColorBarLegendContinuous.tsx create mode 100644 src/components/ColorBarLegend/common.ts diff --git a/src/components/ColorBarLegend/ColorBarLegend.tsx b/src/components/ColorBarLegend/ColorBarLegend.tsx index b55adc62..1328dbb6 100644 --- a/src/components/ColorBarLegend/ColorBarLegend.tsx +++ b/src/components/ColorBarLegend/ColorBarLegend.tsx @@ -22,165 +22,14 @@ * SOFTWARE. */ -import React, { useState } from "react"; -import makeStyles from "@mui/styles/makeStyles"; -import { Theme } from "@mui/material"; -import Popover from "@mui/material/Popover"; - -import { ColorBar, ColorBars } from "@/model/colorBar"; -import { UserColorBar } from "@/model/userColorBar"; -import ColorBarCanvas from "./ColorBarCanvas"; -import ColorBarColorEditor from "./ColorBarColorEditor"; -import ColorBarRangeEditor from "./ColorBarRangeEditor"; -import ColorBarLabels from "./ColorBarLabels"; - -const useStyles = makeStyles((theme: Theme) => ({ - title: { - fontSize: "x-small", - fontWeight: "bold", - width: "100%", - display: "flex", - flexWrap: "nowrap", - justifyContent: "center", - paddingBottom: theme.spacing(0.5), - }, - container: { - paddingLeft: theme.spacing(1.5), - paddingRight: theme.spacing(1.5), - paddingBottom: theme.spacing(0.5), - paddingTop: theme.spacing(0.5), - color: "black", - }, -})); - -interface ColorBarLegendProps { - variableName: string | null; - variableUnits: string; - variableColorBarMinMax: [number, number]; - variableColorBarName: string; - variableColorBar: ColorBar; - variableOpacity: number; - updateVariableColorBar: ( - colorBarMinMax: [number, number], - colorBarName: string, - opacity: number, - ) => void; - colorBars: ColorBars; - userColorBars: UserColorBar[]; - addUserColorBar: (userColorBarId: string) => void; - removeUserColorBar: (userColorBarId: string) => void; - updateUserColorBar: (userColorBar: UserColorBar) => void; - updateUserColorBars: (userColorBars: UserColorBar[]) => void; - width?: number | string; - height?: number | string; - numTicks?: number; -} - -export default function ColorBarLegend({ - variableName, - variableUnits, - variableColorBarMinMax, - variableColorBarName, - variableColorBar, - variableOpacity, - updateVariableColorBar, - colorBars, - userColorBars, - addUserColorBar, - removeUserColorBar, - updateUserColorBar, - updateUserColorBars, - width, - height, - numTicks, -}: ColorBarLegendProps) { - const classes = useStyles(); - - const [colorBarRangeEditorAnchor, setColorBarRangeEditorAnchor] = - useState(null); - const [colorBarSelectAnchor, setColorBarSelectAnchor] = - useState(null); - - if (!variableName) { - return null; - } - - const handleOpenColorBarRangeEditor = ( - event: React.MouseEvent, - ) => { - setColorBarRangeEditorAnchor(event.currentTarget); - }; - - const handleCloseColorBarRangeEditor = () => { - setColorBarRangeEditorAnchor(null); - }; - - const handleOpenColorBarSelect = ( - event: React.MouseEvent, - ) => { - setColorBarSelectAnchor(event.currentTarget); - }; - - const handleCloseColorBarSelect = () => { - setColorBarSelectAnchor(null); - }; - - const variableTitle = `${variableName} (${variableUnits || "-"})`; - - return ( -
-
- {variableTitle} -
- - - - - - - - -
+import { ColorBarLegendProps } from "./common"; +import ColorBarLegendCategorical from "./ColorBarLegendCategorical"; +import ColorBarLegendContinuous from "./ColorBarLegendContinuous"; + +export default function ColorBarLegend(props: ColorBarLegendProps) { + return props.variableColorBar.categories ? ( + + ) : ( + ); } diff --git a/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx b/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx new file mode 100644 index 00000000..e64db86f --- /dev/null +++ b/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx @@ -0,0 +1,143 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019-2024 by the xcube development team and contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { useState } from "react"; +import Popover from "@mui/material/Popover"; + +import ColorBarCanvas from "./ColorBarCanvas"; +import ColorBarColorEditor from "./ColorBarColorEditor"; +import ColorBarRangeEditor from "./ColorBarRangeEditor"; +import ColorBarLabels from "./ColorBarLabels"; +import { useColorBarLegendStyles, ColorBarLegendProps } from "./common"; + +export default function ColorBarLegendCategorical({ + variableName, + variableUnits, + variableColorBarMinMax, + variableColorBarName, + variableColorBar, + variableOpacity, + updateVariableColorBar, + colorBars, + userColorBars, + addUserColorBar, + removeUserColorBar, + updateUserColorBar, + updateUserColorBars, + width, + height, + numTicks, +}: ColorBarLegendProps) { + const classes = useColorBarLegendStyles(); + + const [colorBarRangeEditorAnchor, setColorBarRangeEditorAnchor] = + useState(null); + const [colorBarSelectAnchor, setColorBarSelectAnchor] = + useState(null); + + console.log("ColorBarLegendCategorical: ", variableColorBar); + + if (!variableName) { + return null; + } + + const handleOpenColorBarRangeEditor = ( + event: React.MouseEvent, + ) => { + setColorBarRangeEditorAnchor(event.currentTarget); + }; + + const handleCloseColorBarRangeEditor = () => { + setColorBarRangeEditorAnchor(null); + }; + + const handleOpenColorBarSelect = ( + event: React.MouseEvent, + ) => { + setColorBarSelectAnchor(event.currentTarget); + }; + + const handleCloseColorBarSelect = () => { + setColorBarSelectAnchor(null); + }; + + const variableTitle = `${variableName} (${variableUnits || "-"})`; + + return ( +
+
+ {variableTitle} +
+ + + + + + + + +
+ ); +} diff --git a/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx b/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx new file mode 100644 index 00000000..463894bc --- /dev/null +++ b/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx @@ -0,0 +1,143 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019-2024 by the xcube development team and contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { useState } from "react"; +import Popover from "@mui/material/Popover"; + +import ColorBarCanvas from "./ColorBarCanvas"; +import ColorBarColorEditor from "./ColorBarColorEditor"; +import ColorBarRangeEditor from "./ColorBarRangeEditor"; +import ColorBarLabels from "./ColorBarLabels"; +import { useColorBarLegendStyles, ColorBarLegendProps } from "./common"; + +export default function ColorBarLegendContinuous({ + variableName, + variableUnits, + variableColorBarMinMax, + variableColorBarName, + variableColorBar, + variableOpacity, + updateVariableColorBar, + colorBars, + userColorBars, + addUserColorBar, + removeUserColorBar, + updateUserColorBar, + updateUserColorBars, + width, + height, + numTicks, +}: ColorBarLegendProps) { + const classes = useColorBarLegendStyles(); + + const [colorBarRangeEditorAnchor, setColorBarRangeEditorAnchor] = + useState(null); + const [colorBarSelectAnchor, setColorBarSelectAnchor] = + useState(null); + + console.log("ColorBarLegendContinuous: ", variableColorBar); + + if (!variableName) { + return null; + } + + const handleOpenColorBarRangeEditor = ( + event: React.MouseEvent, + ) => { + setColorBarRangeEditorAnchor(event.currentTarget); + }; + + const handleCloseColorBarRangeEditor = () => { + setColorBarRangeEditorAnchor(null); + }; + + const handleOpenColorBarSelect = ( + event: React.MouseEvent, + ) => { + setColorBarSelectAnchor(event.currentTarget); + }; + + const handleCloseColorBarSelect = () => { + setColorBarSelectAnchor(null); + }; + + const variableTitle = `${variableName} (${variableUnits || "-"})`; + + return ( +
+
+ {variableTitle} +
+ + + + + + + + +
+ ); +} diff --git a/src/components/ColorBarLegend/common.ts b/src/components/ColorBarLegend/common.ts new file mode 100644 index 00000000..541bf212 --- /dev/null +++ b/src/components/ColorBarLegend/common.ts @@ -0,0 +1,71 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019-2024 by the xcube development team and contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import makeStyles from "@mui/styles/makeStyles"; +import { Theme } from "@mui/material"; + +import { ColorBar, ColorBars } from "@/model/colorBar"; +import { UserColorBar } from "@/model/userColorBar"; + +export const useColorBarLegendStyles = makeStyles((theme: Theme) => ({ + title: { + fontSize: "x-small", + fontWeight: "bold", + width: "100%", + display: "flex", + flexWrap: "nowrap", + justifyContent: "center", + paddingBottom: theme.spacing(0.5), + }, + container: { + paddingLeft: theme.spacing(1.5), + paddingRight: theme.spacing(1.5), + paddingBottom: theme.spacing(0.5), + paddingTop: theme.spacing(0.5), + color: "black", + }, +})); + +export interface ColorBarLegendProps { + variableName: string | null; + variableUnits: string; + variableColorBarMinMax: [number, number]; + variableColorBarName: string; + variableColorBar: ColorBar; + variableOpacity: number; + updateVariableColorBar: ( + colorBarMinMax: [number, number], + colorBarName: string, + opacity: number, + ) => void; + colorBars: ColorBars; + userColorBars: UserColorBar[]; + addUserColorBar: (userColorBarId: string) => void; + removeUserColorBar: (userColorBarId: string) => void; + updateUserColorBar: (userColorBar: UserColorBar) => void; + updateUserColorBars: (userColorBars: UserColorBar[]) => void; + width?: number | string; + height?: number | string; + numTicks?: number; +} diff --git a/src/model/colorBar.ts b/src/model/colorBar.ts index 5280685b..873a6c94 100644 --- a/src/model/colorBar.ts +++ b/src/model/colorBar.ts @@ -23,10 +23,19 @@ */ import bgImageData from "./bg.png"; +import { RGBA } from "@/util/color"; const BG_IMAGE = new Image(); BG_IMAGE.src = bgImageData; +export interface ColorRecord { + value: number; + color: RGBA; + label?: string; +} + +export type HexColorRecord = Omit & { color: string }; + export interface ColorBars { groups: ColorBarGroup[]; images: Record; @@ -64,6 +73,10 @@ export interface ColorBar { * renderUserColorBarAsBase64() from user color bar code. */ imageData?: string; + /** + * Defined, if this color bar is a user-defined categorical color bar. + */ + categories?: HexColorRecord[]; } export function parseColorBar(name: string): ColorBar { diff --git a/src/model/userColorBar.ts b/src/model/userColorBar.ts index 69a28954..8dca3a00 100644 --- a/src/model/userColorBar.ts +++ b/src/model/userColorBar.ts @@ -22,7 +22,8 @@ * SOFTWARE. */ -import { parseColor, RGBA, rgbToHex } from "@/util/color"; +import { parseColor, rgbToHex } from "@/util/color"; +import { ColorRecord, HexColorRecord } from "@/model/colorBar"; export const USER_COLOR_BAR_GROUP_TITLE = "User"; export const USER_COLOR_BAR_CODE_EXAMPLE = @@ -30,8 +31,6 @@ export const USER_COLOR_BAR_CODE_EXAMPLE = "0.5: red\n" + // tie point 2 "1.0: 120,30,255"; // tie point 3 -export type ColorRecord = [number, RGBA]; - export interface UserColorBar { /** * Unique ID. @@ -63,11 +62,14 @@ export interface UserColorBar { errorMessage?: string; } -export function getUserColorBarRgbaArray(records: ColorRecord[], size: number) { +export function getUserColorBarRgbaArray( + records: ColorRecord[], + size: number, +): Uint8ClampedArray { const n = records.length; - const min = records[0][0]; - const max = records[n - 1][0]; - const values = records.map((record) => (record[0] - min) / (max - min)); + const min = records[0].value; + const max = records[n - 1].value; + const values = records.map((record) => (record.value - min) / (max - min)); const rgbaArray = new Uint8ClampedArray(4 * size); let recordIndex = 0; let v1 = values[0]; @@ -80,8 +82,8 @@ export function getUserColorBarRgbaArray(records: ColorRecord[], size: number) { v2 = values[recordIndex + 1]; } const w = (v - v1) / (v2 - v1); - const [r1, g1, b1, a1] = records[recordIndex][1]; - const [r2, g2, b2, a2] = records[recordIndex + 1][1]; + const [r1, g1, b1, a1] = records[recordIndex].color; + const [r2, g2, b2, a2] = records[recordIndex + 1].color; rgbaArray[j] = r1 + w * (r2 - r1); rgbaArray[j + 1] = g1 + w * (g2 - g1); rgbaArray[j + 2] = b1 + w * (b2 - b1); @@ -122,10 +124,12 @@ export function renderUserColorBarAsBase64( }); } -export function getUserColorBarColorArray(code: string) { +export function getUserColorBarHexRecords( + code: string, +): HexColorRecord[] | undefined { const { colorRecords } = getUserColorBarColorRecords(code); if (colorRecords) { - return colorRecords.map(([value, rgba]) => [value, rgbToHex(rgba)]); + return colorRecords.map((r) => ({ ...r, color: rgbToHex(r.color) })); } } @@ -160,19 +164,23 @@ export function parseUserColorBarCode(code: string): ColorRecord[] { .map((comp) => comp.trim()), ) .forEach((recordParts, index) => { - if (recordParts.length == 2) { + if (recordParts.length == 2 || recordParts.length == 3) { const [valueText, rgbText] = recordParts; const value = parseFloat(valueText); - const rgba = parseColor(rgbText); + const color = parseColor(rgbText); if (!Number.isFinite(value)) { throw new SyntaxError( `Line ${index + 1}: invalid value: ${valueText}`, ); } - if (!rgba) { + if (!color) { throw new SyntaxError(`Line ${index + 1}: invalid color: ${rgbText}`); } - points.push([value, rgba]); + if (recordParts.length == 3) { + points.push({ value, color, label: recordParts[2] }); + } else { + points.push({ value, color }); + } } else if (recordParts.length === 1) { if (recordParts[0] !== "") { throw new SyntaxError( @@ -185,9 +193,9 @@ export function parseUserColorBarCode(code: string): ColorRecord[] { if (n < 2) { throw new SyntaxError(`At least two color records must be given`); } - points.sort((r1: ColorRecord, r2: ColorRecord) => r1[0] - r2[0]); - const v1 = points[0][0]; - const v2 = points[n - 1][0]; + points.sort((r1: ColorRecord, r2: ColorRecord) => r1.value - r2.value); + const v1 = points[0].value; + const v2 = points[n - 1].value; if (v1 === v2) { throw new SyntaxError(`Values must form a range`); } diff --git a/src/selectors/controlSelectors.tsx b/src/selectors/controlSelectors.tsx index c469bd87..82e4607d 100644 --- a/src/selectors/controlSelectors.tsx +++ b/src/selectors/controlSelectors.tsx @@ -86,10 +86,11 @@ import { ColorBar, ColorBarGroup, ColorBars, + HexColorRecord, parseColorBar, } from "@/model/colorBar"; import { - getUserColorBarColorArray, + getUserColorBarHexRecords, USER_COLOR_BAR_GROUP_TITLE, UserColorBar, } from "@/model/userColorBar"; @@ -279,21 +280,32 @@ export const colorBarsSelector = createSelector( const getVariableColorBar = ( colorBarName: string, colorBars: ColorBars, + userColorBars: UserColorBar[], ): ColorBar => { const colorBar: ColorBar = parseColorBar(colorBarName); const imageData = colorBars.images[colorBar.baseName]; - return { ...colorBar, imageData }; + const { baseName } = colorBar; + const userColorBar = userColorBars.find( + (userColorBar) => userColorBar.id === baseName, + ); + let categories: HexColorRecord[] | undefined = undefined; + if (userColorBar) { + categories = getUserColorBarHexRecords(userColorBar.code); + } + return { ...colorBar, imageData, categories }; }; export const selectedVariableColorBarSelector = createSelector( selectedVariableColorBarNameSelector, colorBarsSelector, + userColorBarsSelector, getVariableColorBar, ); export const selectedVariable2ColorBarSelector = createSelector( selectedVariable2ColorBarNameSelector, colorBarsSelector, + userColorBarsSelector, getVariableColorBar, ); @@ -307,9 +319,12 @@ const getVariableUserColorBarJson = ( (userColorBar) => userColorBar.id === baseName, ); if (userColorBar) { - const colors = getUserColorBarColorArray(userColorBar.code); + const colors = getUserColorBarHexRecords(userColorBar.code); if (colors) { - return JSON.stringify({ name: colorBarName, colors }); + return JSON.stringify({ + name: colorBarName, + colors: colors.map((c) => [c.value, c.color]), + }); } } return null; From 1eb734545b7582a7c8bb8be6e2eb0e6f8fdf1e7b Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 10 May 2024 15:46:38 +0200 Subject: [PATCH 03/26] fixed tests --- src/model/userColorBar.test.ts | 38 ++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/model/userColorBar.test.ts b/src/model/userColorBar.test.ts index ae645f9c..59cf343a 100644 --- a/src/model/userColorBar.test.ts +++ b/src/model/userColorBar.test.ts @@ -29,13 +29,13 @@ import { USER_COLOR_BAR_CODE_EXAMPLE, } from "./userColorBar"; -describe("Assert that colorBar.getUserColorBarData()", () => { +describe("Assert that colorBar.getUserColorBarRgbaArray()", () => { it("works as expected", () => { const data = getUserColorBarRgbaArray( [ - [0.0, [35, 255, 82, 255]], - [0.5, [255, 0, 0, 255]], - [1.0, [120, 30, 255, 255]], + { value: 0.0, color: [35, 255, 82, 255] }, + { value: 0.5, color: [255, 0, 0, 255] }, + { value: 1.0, color: [120, 30, 255, 255] }, ], 10, ); @@ -48,26 +48,38 @@ describe("Assert that colorBar.getUserColorBarData()", () => { }); }); -describe("Assert that colorBar.parseUserColorCode()", () => { +describe("Assert that colorBar.getUserColorBarColorRecords()", () => { it("parses the example code as expected", () => { expect(getUserColorBarColorRecords(USER_COLOR_BAR_CODE_EXAMPLE)).toEqual({ colorRecords: [ - [0.0, [35, 255, 82, 255]], - [0.5, [255, 0, 0, 255]], - [1.0, [120, 30, 255, 255]], + { value: 0.0, color: [35, 255, 82, 255] }, + { value: 0.5, color: [255, 0, 0, 255] }, + { value: 1.0, color: [120, 30, 255, 255] }, ], }); }); - it("parses the non-unique values", () => { + it("parses non-unique values", () => { expect( getUserColorBarColorRecords("0:blue\n1:red\n1:green\n2:blue"), ).toEqual({ colorRecords: [ - [0, [0, 0, 255, 255]], - [1, [255, 0, 0, 255]], - [1, [0, 128, 0, 255]], - [2, [0, 0, 255, 255]], + { value: 0, color: [0, 0, 255, 255] }, + { value: 1, color: [255, 0, 0, 255] }, + { value: 1, color: [0, 128, 0, 255] }, + { value: 2, color: [0, 0, 255, 255] }, + ], + }); + }); + + it("parses categories", () => { + expect( + getUserColorBarColorRecords("0:blue:cat-1\n1:red:cat-2\n2:blue:cat-3"), + ).toEqual({ + colorRecords: [ + { value: 0, color: [0, 0, 255, 255], label: "cat-1" }, + { value: 1, color: [255, 0, 0, 255], label: "cat-2" }, + { value: 2, color: [0, 0, 255, 255], label: "cat-3" }, ], }); }); From 66fd33c3b58cf61924d8a1d5be9824787b45c3d1 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 10 May 2024 15:51:09 +0200 Subject: [PATCH 04/26] fix: only if categorical --- src/selectors/controlSelectors.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/selectors/controlSelectors.tsx b/src/selectors/controlSelectors.tsx index 82e4607d..de162e6d 100644 --- a/src/selectors/controlSelectors.tsx +++ b/src/selectors/controlSelectors.tsx @@ -289,7 +289,7 @@ const getVariableColorBar = ( (userColorBar) => userColorBar.id === baseName, ); let categories: HexColorRecord[] | undefined = undefined; - if (userColorBar) { + if (userColorBar && userColorBar.categorical) { categories = getUserColorBarHexRecords(userColorBar.code); } return { ...colorBar, imageData, categories }; From 34e9eb2369cd7a5eae67b8abb6f37f3d941634b3 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 10 May 2024 16:42:46 +0200 Subject: [PATCH 05/26] remove unneeded components --- .../ColorBarLegendCategorical.tsx | 41 +------------------ 1 file changed, 1 insertion(+), 40 deletions(-) diff --git a/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx b/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx index e64db86f..8a70d267 100644 --- a/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx +++ b/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx @@ -27,13 +27,10 @@ import Popover from "@mui/material/Popover"; import ColorBarCanvas from "./ColorBarCanvas"; import ColorBarColorEditor from "./ColorBarColorEditor"; -import ColorBarRangeEditor from "./ColorBarRangeEditor"; -import ColorBarLabels from "./ColorBarLabels"; import { useColorBarLegendStyles, ColorBarLegendProps } from "./common"; export default function ColorBarLegendCategorical({ variableName, - variableUnits, variableColorBarMinMax, variableColorBarName, variableColorBar, @@ -47,12 +44,9 @@ export default function ColorBarLegendCategorical({ updateUserColorBars, width, height, - numTicks, }: ColorBarLegendProps) { const classes = useColorBarLegendStyles(); - const [colorBarRangeEditorAnchor, setColorBarRangeEditorAnchor] = - useState(null); const [colorBarSelectAnchor, setColorBarSelectAnchor] = useState(null); @@ -62,16 +56,6 @@ export default function ColorBarLegendCategorical({ return null; } - const handleOpenColorBarRangeEditor = ( - event: React.MouseEvent, - ) => { - setColorBarRangeEditorAnchor(event.currentTarget); - }; - - const handleCloseColorBarRangeEditor = () => { - setColorBarRangeEditorAnchor(null); - }; - const handleOpenColorBarSelect = ( event: React.MouseEvent, ) => { @@ -82,12 +66,10 @@ export default function ColorBarLegendCategorical({ setColorBarSelectAnchor(null); }; - const variableTitle = `${variableName} (${variableUnits || "-"})`; - return (
- {variableTitle} + {variableName}
- - - - Date: Fri, 10 May 2024 17:07:52 +0200 Subject: [PATCH 06/26] avoid closing color bar selector if making color bar categorical --- .../ColorBarLegend/ColorBarLegend.tsx | 65 +++++++++++++++-- .../ColorBarLegendCategorical.tsx | 71 +++---------------- .../ColorBarLegendContinuous.tsx | 66 +++-------------- src/components/ColorBarLegend/common.ts | 2 + 4 files changed, 81 insertions(+), 123 deletions(-) diff --git a/src/components/ColorBarLegend/ColorBarLegend.tsx b/src/components/ColorBarLegend/ColorBarLegend.tsx index 1328dbb6..a2d98e87 100644 --- a/src/components/ColorBarLegend/ColorBarLegend.tsx +++ b/src/components/ColorBarLegend/ColorBarLegend.tsx @@ -22,14 +22,67 @@ * SOFTWARE. */ -import { ColorBarLegendProps } from "./common"; +import { useRef, useState } from "react"; +import Popover from "@mui/material/Popover"; + +import { ColorBarLegendProps, useColorBarLegendStyles } from "./common"; import ColorBarLegendCategorical from "./ColorBarLegendCategorical"; import ColorBarLegendContinuous from "./ColorBarLegendContinuous"; +import ColorBarColorEditor from "./ColorBarColorEditor"; + +export default function ColorBarLegend( + props: Omit, +) { + const classes = useColorBarLegendStyles(); + + const { variableName, variableUnits, variableColorBar } = props; + + const colorBarSelectAnchorRef = useRef(null); + const [colorBarSelectAnchorEl, setColorBarSelectAnchorEl] = + useState(null); + + console.log("ColorBarLegendCategorical: ", variableColorBar); + + const handleOpenColorBarSelect = () => { + setColorBarSelectAnchorEl(colorBarSelectAnchorRef.current); + }; + + const handleCloseColorBarSelect = () => { + setColorBarSelectAnchorEl(null); + }; -export default function ColorBarLegend(props: ColorBarLegendProps) { - return props.variableColorBar.categories ? ( - - ) : ( - + return ( +
+
+ + {variableColorBar.categories + ? variableName + : `${variableName} (${variableUnits || "-"})`} + +
+ {variableColorBar.categories ? ( + + ) : ( + + )} + + + +
); } diff --git a/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx b/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx index 8a70d267..60a468e6 100644 --- a/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx +++ b/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx @@ -22,83 +22,30 @@ * SOFTWARE. */ -import React, { useState } from "react"; -import Popover from "@mui/material/Popover"; - import ColorBarCanvas from "./ColorBarCanvas"; -import ColorBarColorEditor from "./ColorBarColorEditor"; -import { useColorBarLegendStyles, ColorBarLegendProps } from "./common"; +import { ColorBarLegendProps } from "./common"; export default function ColorBarLegendCategorical({ variableName, - variableColorBarMinMax, - variableColorBarName, variableColorBar, variableOpacity, - updateVariableColorBar, - colorBars, - userColorBars, - addUserColorBar, - removeUserColorBar, - updateUserColorBar, - updateUserColorBars, width, height, + onOpenColorBarEditor, }: ColorBarLegendProps) { - const classes = useColorBarLegendStyles(); - - const [colorBarSelectAnchor, setColorBarSelectAnchor] = - useState(null); - console.log("ColorBarLegendCategorical: ", variableColorBar); if (!variableName) { return null; } - const handleOpenColorBarSelect = ( - event: React.MouseEvent, - ) => { - setColorBarSelectAnchor(event.currentTarget); - }; - - const handleCloseColorBarSelect = () => { - setColorBarSelectAnchor(null); - }; - return ( -
-
- {variableName} -
- - - - -
+ ); } diff --git a/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx b/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx index 463894bc..eb8ea30e 100644 --- a/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx +++ b/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx @@ -26,10 +26,9 @@ import React, { useState } from "react"; import Popover from "@mui/material/Popover"; import ColorBarCanvas from "./ColorBarCanvas"; -import ColorBarColorEditor from "./ColorBarColorEditor"; import ColorBarRangeEditor from "./ColorBarRangeEditor"; import ColorBarLabels from "./ColorBarLabels"; -import { useColorBarLegendStyles, ColorBarLegendProps } from "./common"; +import { ColorBarLegendProps } from "./common"; export default function ColorBarLegendContinuous({ variableName, @@ -39,24 +38,15 @@ export default function ColorBarLegendContinuous({ variableColorBar, variableOpacity, updateVariableColorBar, - colorBars, - userColorBars, - addUserColorBar, - removeUserColorBar, - updateUserColorBar, - updateUserColorBars, width, height, numTicks, + onOpenColorBarEditor, }: ColorBarLegendProps) { - const classes = useColorBarLegendStyles(); - - const [colorBarRangeEditorAnchor, setColorBarRangeEditorAnchor] = + const [colorBarRangeEditorAnchorEl, setColorBarRangeEditorAnchorEl] = useState(null); - const [colorBarSelectAnchor, setColorBarSelectAnchor] = - useState(null); - console.log("ColorBarLegendContinuous: ", variableColorBar); + console.log("ColorBarLegendContinuous", variableColorBar); if (!variableName) { return null; @@ -65,36 +55,23 @@ export default function ColorBarLegendContinuous({ const handleOpenColorBarRangeEditor = ( event: React.MouseEvent, ) => { - setColorBarRangeEditorAnchor(event.currentTarget); + setColorBarRangeEditorAnchorEl(event.currentTarget); }; const handleCloseColorBarRangeEditor = () => { - setColorBarRangeEditorAnchor(null); - }; - - const handleOpenColorBarSelect = ( - event: React.MouseEvent, - ) => { - setColorBarSelectAnchor(event.currentTarget); - }; - - const handleCloseColorBarSelect = () => { - setColorBarSelectAnchor(null); + setColorBarRangeEditorAnchorEl(null); }; const variableTitle = `${variableName} (${variableUnits || "-"})`; return ( -
-
- {variableTitle} -
+ <> - - - -
+ ); } diff --git a/src/components/ColorBarLegend/common.ts b/src/components/ColorBarLegend/common.ts index 541bf212..9eab0219 100644 --- a/src/components/ColorBarLegend/common.ts +++ b/src/components/ColorBarLegend/common.ts @@ -22,6 +22,7 @@ * SOFTWARE. */ +import { MouseEvent } from "react"; import makeStyles from "@mui/styles/makeStyles"; import { Theme } from "@mui/material"; @@ -68,4 +69,5 @@ export interface ColorBarLegendProps { width?: number | string; height?: number | string; numTicks?: number; + onOpenColorBarEditor: (event: MouseEvent) => void; } From 7482e29a95f578468f3bf0fc422959131792eb67 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 10 May 2024 17:41:31 +0200 Subject: [PATCH 07/26] works --- .../ColorBarLegend/ColorBarCanvas.tsx | 4 +- .../ColorBarLegend/ColorBarLegend.tsx | 65 +++++++++++++++-- .../ColorBarLegendCategorical.tsx | 57 +++++++++------ .../ColorBarLegendContinuous.tsx | 40 +++++----- src/components/ColorBarLegend/common.ts | 73 ------------------- 5 files changed, 115 insertions(+), 124 deletions(-) delete mode 100644 src/components/ColorBarLegend/common.ts diff --git a/src/components/ColorBarLegend/ColorBarCanvas.tsx b/src/components/ColorBarLegend/ColorBarCanvas.tsx index e2bddfbb..0d23e252 100644 --- a/src/components/ColorBarLegend/ColorBarCanvas.tsx +++ b/src/components/ColorBarLegend/ColorBarCanvas.tsx @@ -35,8 +35,8 @@ import { interface ColorBarCanvasProps { colorBar: ColorBar; opacity: number; - width: number | string | undefined; - height: number | string | undefined; + width?: number | string | undefined; + height?: number | string | undefined; onClick: (event: MouseEvent) => void; } diff --git a/src/components/ColorBarLegend/ColorBarLegend.tsx b/src/components/ColorBarLegend/ColorBarLegend.tsx index a2d98e87..de58aa60 100644 --- a/src/components/ColorBarLegend/ColorBarLegend.tsx +++ b/src/components/ColorBarLegend/ColorBarLegend.tsx @@ -22,18 +22,61 @@ * SOFTWARE. */ -import { useRef, useState } from "react"; +import { MouseEvent, useRef, useState } from "react"; import Popover from "@mui/material/Popover"; -import { ColorBarLegendProps, useColorBarLegendStyles } from "./common"; import ColorBarLegendCategorical from "./ColorBarLegendCategorical"; import ColorBarLegendContinuous from "./ColorBarLegendContinuous"; import ColorBarColorEditor from "./ColorBarColorEditor"; +import { ColorBar, ColorBars } from "@/model/colorBar"; +import { UserColorBar } from "@/model/userColorBar"; +import makeStyles from "@mui/styles/makeStyles"; +import { Theme } from "@mui/material"; + +const useStyles = makeStyles((theme: Theme) => ({ + title: { + fontSize: "small", + fontWeight: "bold", + width: "100%", + display: "flex", + flexWrap: "nowrap", + justifyContent: "center", + paddingBottom: theme.spacing(0.5), + }, + container: { + paddingLeft: theme.spacing(1.5), + paddingRight: theme.spacing(1.5), + paddingBottom: theme.spacing(0.5), + paddingTop: theme.spacing(0.5), + color: "black", + }, +})); + +interface ColorBarLegendProps { + variableName: string | null; + variableUnits: string; + variableColorBarMinMax: [number, number]; + variableColorBarName: string; + variableColorBar: ColorBar; + variableOpacity: number; + updateVariableColorBar: ( + colorBarMinMax: [number, number], + colorBarName: string, + opacity: number, + ) => void; + colorBars: ColorBars; + userColorBars: UserColorBar[]; + addUserColorBar: (userColorBarId: string) => void; + removeUserColorBar: (userColorBarId: string) => void; + updateUserColorBar: (userColorBar: UserColorBar) => void; + updateUserColorBars: (userColorBars: UserColorBar[]) => void; + onOpenColorBarEditor: (event: MouseEvent) => void; +} export default function ColorBarLegend( props: Omit, ) { - const classes = useColorBarLegendStyles(); + const classes = useStyles(); const { variableName, variableUnits, variableColorBar } = props; @@ -51,25 +94,31 @@ export default function ColorBarLegend( setColorBarSelectAnchorEl(null); }; + if (!variableName) { + return null; + } + + const variableTitle = variableColorBar.categories + ? variableName + : `${variableName} (${variableUnits || "-"})`; + return (
- - {variableColorBar.categories - ? variableName - : `${variableName} (${variableUnits || "-"})`} - + {variableTitle}
{variableColorBar.categories ? ( ) : ( diff --git a/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx b/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx index 60a468e6..a5185b4b 100644 --- a/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx +++ b/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx @@ -22,30 +22,45 @@ * SOFTWARE. */ -import ColorBarCanvas from "./ColorBarCanvas"; -import { ColorBarLegendProps } from "./common"; +import Box from "@mui/material/Box"; -export default function ColorBarLegendCategorical({ - variableName, - variableColorBar, - variableOpacity, - width, - height, - onOpenColorBarEditor, -}: ColorBarLegendProps) { - console.log("ColorBarLegendCategorical: ", variableColorBar); +import { HexColorRecord } from "@/model/colorBar"; +import { COLOR_BAR_ITEM_WIDTH } from "@/components/ColorBarLegend/constants"; - if (!variableName) { - return null; - } +export interface ColorBarLegendCategoricalProps { + variableColorBarCategories: HexColorRecord[]; + onOpenColorBarEditor: () => void; +} +export default function ColorBarLegendCategorical({ + variableColorBarCategories, + onOpenColorBarEditor, +}: ColorBarLegendCategoricalProps) { return ( - + + {variableColorBarCategories.map((category, index) => ( + + + {`${category.label || `Category ${index + 1}`} (${category.value})`} + + ))} + ); } diff --git a/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx b/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx index eb8ea30e..a2e30253 100644 --- a/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx +++ b/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx @@ -22,39 +22,43 @@ * SOFTWARE. */ -import React, { useState } from "react"; +import { MouseEvent, useState } from "react"; import Popover from "@mui/material/Popover"; import ColorBarCanvas from "./ColorBarCanvas"; import ColorBarRangeEditor from "./ColorBarRangeEditor"; import ColorBarLabels from "./ColorBarLabels"; -import { ColorBarLegendProps } from "./common"; +import { ColorBar } from "@/model/colorBar"; + +export interface ColorBarLegendContinuousProps { + variableTitle: string; + variableColorBarMinMax: [number, number]; + variableColorBarName: string; + variableColorBar: ColorBar; + variableOpacity: number; + updateVariableColorBar: ( + colorBarMinMax: [number, number], + colorBarName: string, + opacity: number, + ) => void; + onOpenColorBarEditor: () => void; +} export default function ColorBarLegendContinuous({ - variableName, - variableUnits, + variableTitle, variableColorBarMinMax, variableColorBarName, variableColorBar, variableOpacity, updateVariableColorBar, - width, - height, - numTicks, onOpenColorBarEditor, -}: ColorBarLegendProps) { +}: ColorBarLegendContinuousProps) { const [colorBarRangeEditorAnchorEl, setColorBarRangeEditorAnchorEl] = useState(null); console.log("ColorBarLegendContinuous", variableColorBar); - if (!variableName) { - return null; - } - - const handleOpenColorBarRangeEditor = ( - event: React.MouseEvent, - ) => { + const handleOpenColorBarRangeEditor = (event: MouseEvent) => { setColorBarRangeEditorAnchorEl(event.currentTarget); }; @@ -62,21 +66,17 @@ export default function ColorBarLegendContinuous({ setColorBarRangeEditorAnchorEl(null); }; - const variableTitle = `${variableName} (${variableUnits || "-"})`; - return ( <> ({ - title: { - fontSize: "x-small", - fontWeight: "bold", - width: "100%", - display: "flex", - flexWrap: "nowrap", - justifyContent: "center", - paddingBottom: theme.spacing(0.5), - }, - container: { - paddingLeft: theme.spacing(1.5), - paddingRight: theme.spacing(1.5), - paddingBottom: theme.spacing(0.5), - paddingTop: theme.spacing(0.5), - color: "black", - }, -})); - -export interface ColorBarLegendProps { - variableName: string | null; - variableUnits: string; - variableColorBarMinMax: [number, number]; - variableColorBarName: string; - variableColorBar: ColorBar; - variableOpacity: number; - updateVariableColorBar: ( - colorBarMinMax: [number, number], - colorBarName: string, - opacity: number, - ) => void; - colorBars: ColorBars; - userColorBars: UserColorBar[]; - addUserColorBar: (userColorBarId: string) => void; - removeUserColorBar: (userColorBarId: string) => void; - updateUserColorBar: (userColorBar: UserColorBar) => void; - updateUserColorBars: (userColorBars: UserColorBar[]) => void; - width?: number | string; - height?: number | string; - numTicks?: number; - onOpenColorBarEditor: (event: MouseEvent) => void; -} From 219be7487c866668ced1ae2f803c21ca5b6abb3a Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Fri, 10 May 2024 18:07:04 +0200 Subject: [PATCH 08/26] now also rendering categorical color bars --- src/model/userColorBar.ts | 65 ++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/src/model/userColorBar.ts b/src/model/userColorBar.ts index 8dca3a00..3943e2c8 100644 --- a/src/model/userColorBar.ts +++ b/src/model/userColorBar.ts @@ -65,38 +65,51 @@ export interface UserColorBar { export function getUserColorBarRgbaArray( records: ColorRecord[], size: number, + categorical?: boolean, ): Uint8ClampedArray { - const n = records.length; - const min = records[0].value; - const max = records[n - 1].value; - const values = records.map((record) => (record.value - min) / (max - min)); const rgbaArray = new Uint8ClampedArray(4 * size); - let recordIndex = 0; - let v1 = values[0]; - let v2 = values[1]; - for (let i = 0, j = 0; i < size; i++, j += 4) { - const v = i / (size - 1); - if (v > v2) { - recordIndex++; - v1 = values[recordIndex]; - v2 = values[recordIndex + 1]; + const n = records.length; + if (categorical) { + for (let i = 0, j = 0; i < size; i++, j += 4) { + const recordIndex = Math.floor((n * i) / size); + const [r, g, b, a] = records[recordIndex].color; + rgbaArray[j] = r; + rgbaArray[j + 1] = g; + rgbaArray[j + 2] = b; + rgbaArray[j + 3] = a; + } + } else { + const min = records[0].value; + const max = records[n - 1].value; + const values = records.map((record) => (record.value - min) / (max - min)); + let recordIndex = 0; + let v1 = values[0]; + let v2 = values[1]; + for (let i = 0, j = 0; i < size; i++, j += 4) { + const v = i / (size - 1); + if (v > v2) { + recordIndex++; + v1 = values[recordIndex]; + v2 = values[recordIndex + 1]; + } + const w = categorical ? 0 : (v - v1) / (v2 - v1); + const [r1, g1, b1, a1] = records[recordIndex].color; + const [r2, g2, b2, a2] = records[recordIndex + 1].color; + rgbaArray[j] = r1 + w * (r2 - r1); + rgbaArray[j + 1] = g1 + w * (g2 - g1); + rgbaArray[j + 2] = b1 + w * (b2 - b1); + rgbaArray[j + 3] = a1 + w * (a2 - a1); } - const w = (v - v1) / (v2 - v1); - const [r1, g1, b1, a1] = records[recordIndex].color; - const [r2, g2, b2, a2] = records[recordIndex + 1].color; - rgbaArray[j] = r1 + w * (r2 - r1); - rgbaArray[j + 1] = g1 + w * (g2 - g1); - rgbaArray[j + 2] = b1 + w * (b2 - b1); - rgbaArray[j + 3] = a1 + w * (a2 - a1); } return rgbaArray; } export function renderUserColorBar( records: ColorRecord[], + categorical: boolean, canvas: HTMLCanvasElement, ): Promise { - const data = getUserColorBarRgbaArray(records, canvas.width); + const data = getUserColorBarRgbaArray(records, canvas.width, categorical); const imageData = new ImageData(data, data.length / 4, 1); return createImageBitmap(imageData).then((bitMap) => { const ctx = canvas.getContext("2d"); @@ -118,10 +131,12 @@ export function renderUserColorBarAsBase64( const canvas = document.createElement("canvas"); canvas.width = 256; canvas.height = 1; - return renderUserColorBar(colorRecords, canvas).then(() => { - const dataURL = canvas.toDataURL("image/png"); - return { imageData: dataURL.split(",")[1] }; - }); + return renderUserColorBar(colorRecords, !!colorBar.categorical, canvas).then( + () => { + const dataURL = canvas.toDataURL("image/png"); + return { imageData: dataURL.split(",")[1] }; + }, + ); } export function getUserColorBarHexRecords( From 81532202447f9a339fad1f47076c470c920c47bc Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sun, 12 May 2024 07:49:46 +0200 Subject: [PATCH 09/26] better color item border colors --- src/components/ColorBarLegend/ColorBarItem.tsx | 2 +- .../ColorBarLegend/ColorBarLegendCategorical.tsx | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/ColorBarLegend/ColorBarItem.tsx b/src/components/ColorBarLegend/ColorBarItem.tsx index 21f0be96..a48b8684 100644 --- a/src/components/ColorBarLegend/ColorBarItem.tsx +++ b/src/components/ColorBarLegend/ColorBarItem.tsx @@ -39,7 +39,7 @@ const colorBarItemStyle = (theme: Theme) => ({ const useItemStyles = makeStyles((theme: Theme) => ({ colorBarItem: { ...colorBarItemStyle(theme), - borderColor: theme.palette.mode === "dark" ? "white" : "black", + borderColor: theme.palette.mode === "dark" ? "lightgray" : "darkgray", }, colorBarItemSelected: { ...colorBarItemStyle(theme), diff --git a/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx b/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx index a5185b4b..5ba8656c 100644 --- a/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx +++ b/src/components/ColorBarLegend/ColorBarLegendCategorical.tsx @@ -49,11 +49,15 @@ export default function ColorBarLegendCategorical({ }} > ({ width: "48px", height: "16px", backgroundColor: category.color, - }} + borderStyle: "solid", + borderColor: + theme.palette.mode === "dark" ? "lightgray" : "darkgray", + borderWidth: 1, + })} /> Date: Sun, 12 May 2024 15:39:00 +0200 Subject: [PATCH 10/26] introduced property Variable.colorBarNorm and set tile query param "norm" accordingly --- .../ColorBarLegend/ColorBarSelect.tsx | 7 +- .../ColorBarLegend/ColorBarStyleEditor.tsx | 9 ++- src/model/colorBar.test.ts | 34 ++++++--- src/model/colorBar.ts | 4 +- src/model/userColorBar.ts | 14 ++-- src/model/variable.ts | 3 + src/selectors/controlSelectors.tsx | 72 +++++++++++++------ src/volume/ColorBarTextures.ts | 4 +- 8 files changed, 100 insertions(+), 47 deletions(-) diff --git a/src/components/ColorBarLegend/ColorBarSelect.tsx b/src/components/ColorBarLegend/ColorBarSelect.tsx index a188bedf..61fc65bc 100644 --- a/src/components/ColorBarLegend/ColorBarSelect.tsx +++ b/src/components/ColorBarLegend/ColorBarSelect.tsx @@ -22,7 +22,7 @@ * SOFTWARE. */ -import { ColorBar, ColorBars, formatColorBar } from "@/model/colorBar"; +import { ColorBar, ColorBars, formatColorBarName } from "@/model/colorBar"; import { USER_COLOR_BAR_GROUP_TITLE, UserColorBar } from "@/model/userColorBar"; import ColorBarGroupComponent from "./ColorBarGroupComponent"; import UserColorBarGroup from "./UserColorBarGroup"; @@ -59,7 +59,10 @@ export default function ColorBarSelect({ updateUserColorBars, }: ColorBarSelectProps) { const handleSelectColorBar = (baseName: string) => { - variableColorBarName = formatColorBar({ ...variableColorBar, baseName }); + variableColorBarName = formatColorBarName({ + ...variableColorBar, + baseName, + }); updateVariableColorBar( variableColorBarMinMax, variableColorBarName, diff --git a/src/components/ColorBarLegend/ColorBarStyleEditor.tsx b/src/components/ColorBarLegend/ColorBarStyleEditor.tsx index 25ca04f6..2c723aef 100644 --- a/src/components/ColorBarLegend/ColorBarStyleEditor.tsx +++ b/src/components/ColorBarLegend/ColorBarStyleEditor.tsx @@ -28,7 +28,7 @@ import Checkbox from "@mui/material/Checkbox"; import Slider from "@mui/material/Slider"; import i18n from "@/i18n"; -import { ColorBar, formatColorBar } from "@/model/colorBar"; +import { ColorBar, formatColorBarName } from "@/model/colorBar"; interface ColorBarSelectProps { variableColorBarMinMax: [number, number]; @@ -51,7 +51,7 @@ export default function ColorBarStyleEditor({ }: ColorBarSelectProps) { const handleColorBarAlpha = (event: React.ChangeEvent) => { const isAlpha = event.currentTarget.checked; - variableColorBarName = formatColorBar({ ...variableColorBar, isAlpha }); + variableColorBarName = formatColorBarName({ ...variableColorBar, isAlpha }); updateVariableColorBar( variableColorBarMinMax, variableColorBarName, @@ -63,7 +63,10 @@ export default function ColorBarStyleEditor({ event: React.ChangeEvent, ) => { const isReversed = event.currentTarget.checked; - variableColorBarName = formatColorBar({ ...variableColorBar, isReversed }); + variableColorBarName = formatColorBarName({ + ...variableColorBar, + isReversed, + }); updateVariableColorBar( variableColorBarMinMax, variableColorBarName, diff --git a/src/model/colorBar.test.ts b/src/model/colorBar.test.ts index 6f394e56..5f79875d 100644 --- a/src/model/colorBar.test.ts +++ b/src/model/colorBar.test.ts @@ -23,26 +23,26 @@ */ import { describe, expect, it } from "vitest"; -import { parseColorBar, formatColorBar } from "./colorBar"; +import { parseColorBarName, formatColorBarName } from "./colorBar"; -describe("Assert that colorBar.parseColorBar()", () => { +describe("Assert that colorBar.parseColorBarName()", () => { it("works as expected", () => { - expect(parseColorBar("magma")).toEqual({ + expect(parseColorBarName("magma")).toEqual({ baseName: "magma", isAlpha: false, isReversed: false, }); - expect(parseColorBar("magma_r")).toEqual({ + expect(parseColorBarName("magma_r")).toEqual({ baseName: "magma", isAlpha: false, isReversed: true, }); - expect(parseColorBar("magma_alpha")).toEqual({ + expect(parseColorBarName("magma_alpha")).toEqual({ baseName: "magma", isAlpha: true, isReversed: false, }); - expect(parseColorBar("magma_r_alpha")).toEqual({ + expect(parseColorBarName("magma_r_alpha")).toEqual({ baseName: "magma", isAlpha: true, isReversed: true, @@ -50,23 +50,35 @@ describe("Assert that colorBar.parseColorBar()", () => { }); }); -describe("Assert that colorBar.formatColorBar()", () => { +describe("Assert that colorBar.formatColorBarName()", () => { it("works as expected", () => { expect( - formatColorBar({ + formatColorBarName({ baseName: "viridis", isAlpha: false, isReversed: false, }), ).toEqual("viridis"); expect( - formatColorBar({ baseName: "viridis", isAlpha: false, isReversed: true }), + formatColorBarName({ + baseName: "viridis", + isAlpha: false, + isReversed: true, + }), ).toEqual("viridis_r"); expect( - formatColorBar({ baseName: "viridis", isAlpha: true, isReversed: false }), + formatColorBarName({ + baseName: "viridis", + isAlpha: true, + isReversed: false, + }), ).toEqual("viridis_alpha"); expect( - formatColorBar({ baseName: "viridis", isAlpha: true, isReversed: true }), + formatColorBarName({ + baseName: "viridis", + isAlpha: true, + isReversed: true, + }), ).toEqual("viridis_r_alpha"); }); }); diff --git a/src/model/colorBar.ts b/src/model/colorBar.ts index 873a6c94..5f5e13cc 100644 --- a/src/model/colorBar.ts +++ b/src/model/colorBar.ts @@ -79,7 +79,7 @@ export interface ColorBar { categories?: HexColorRecord[]; } -export function parseColorBar(name: string): ColorBar { +export function parseColorBarName(name: string): ColorBar { let baseName = name; const isAlpha = baseName.endsWith(CB_ALPHA_SUFFIX); @@ -95,7 +95,7 @@ export function parseColorBar(name: string): ColorBar { return { baseName, isAlpha, isReversed }; } -export function formatColorBar(colorBar: ColorBar): string { +export function formatColorBarName(colorBar: ColorBar): string { let name = colorBar.baseName; if (colorBar.isReversed) { name += CB_REVERSE_SUFFIX; diff --git a/src/model/userColorBar.ts b/src/model/userColorBar.ts index 3943e2c8..2b69c785 100644 --- a/src/model/userColorBar.ts +++ b/src/model/userColorBar.ts @@ -37,12 +37,12 @@ export interface UserColorBar { */ id: string; /** - * Format of the code value: + * Format of the `code` value: * * code := record {"\n" record} - * record := value ":" (rgb | rgba) - * rgba := rgb ["," a] - * rgb := name | "#"hex | (r "," g "," b) + * record := value ":" (rgb | rgba) [":" label] + * rgb := name | (r "," g "," b) | "#"hex3 | "#"hex6 + * rgba := rgb ["," a] | "#"hex8 * * r, g, b in range 0 to 255, a in range 0 to 1 */ @@ -52,12 +52,12 @@ export interface UserColorBar { */ categorical?: boolean; /** - * base64-encoded image/png - * rendered by renderUserColorBarAsBase64() from code. + * base64-encoded `image/png` + * rendered by renderUserColorBarAsBase64() from `code`. */ imageData?: string; /** - * If imageData is undefined, errorMessage should say why. + * If `imageData` is undefined, errorMessage should say why. */ errorMessage?: string; } diff --git a/src/model/variable.ts b/src/model/variable.ts index c2f963b9..74ce6b8d 100644 --- a/src/model/variable.ts +++ b/src/model/variable.ts @@ -25,6 +25,8 @@ import { VolumeRenderMode } from "@/states/controlState"; import { TileSourceOptions } from "./tile"; +export type ColorBarNorm = "lin" | "log" | "cat"; + export interface Variable { id: string; name: string; @@ -44,6 +46,7 @@ export interface Variable { colorBarName: string; colorBarMin: number; colorBarMax: number; + colorBarNorm?: ColorBarNorm; opacity?: number; volumeRenderMode?: VolumeRenderMode; volumeIsoThreshold?: number; diff --git a/src/selectors/controlSelectors.tsx b/src/selectors/controlSelectors.tsx index de162e6d..cc5d510b 100644 --- a/src/selectors/controlSelectors.tsx +++ b/src/selectors/controlSelectors.tsx @@ -64,7 +64,7 @@ import { PlaceInfo, } from "@/model/place"; import { Time, TimeRange, TimeSeriesGroup } from "@/model/timeSeries"; -import { Variable } from "@/model/variable"; +import { ColorBarNorm, Variable } from "@/model/variable"; import { AppState } from "@/states/appState"; import { findIndexCloseTo } from "@/util/find"; @@ -87,7 +87,7 @@ import { ColorBarGroup, ColorBars, HexColorRecord, - parseColorBar, + parseColorBarName, } from "@/model/colorBar"; import { getUserColorBarHexRecords, @@ -220,6 +220,20 @@ export const selectedVariableUnitsSelector = createSelector( }, ); +const getVariableColorBarName = (variable: Variable | null): string => { + return (variable && variable.colorBarName) || "viridis"; +}; + +export const selectedVariableColorBarNameSelector = createSelector( + selectedVariableSelector, + getVariableColorBarName, +); + +export const selectedVariable2ColorBarNameSelector = createSelector( + selectedVariable2Selector, + getVariableColorBarName, +); + const getVariableColorBarMinMax = ( variable: Variable | null, ): [number, number] => { @@ -236,18 +250,18 @@ export const selectedVariable2ColorBarMinMaxSelector = createSelector( getVariableColorBarMinMax, ); -const getVariableColorBarName = (variable: Variable | null): string => { - return (variable && variable.colorBarName) || "viridis"; +const getVariableColorBarNorm = (variable: Variable | null): ColorBarNorm => { + return (variable && variable.colorBarNorm) || "lin"; }; -export const selectedVariableColorBarNameSelector = createSelector( +export const selectedVariableColorBarNormSelector = createSelector( selectedVariableSelector, - getVariableColorBarName, + getVariableColorBarNorm, ); -export const selectedVariable2ColorBarNameSelector = createSelector( +export const selectedVariable2ColorBarNormSelector = createSelector( selectedVariable2Selector, - getVariableColorBarName, + getVariableColorBarNorm, ); export const colorBarsSelector = createSelector( @@ -282,7 +296,7 @@ const getVariableColorBar = ( colorBars: ColorBars, userColorBars: UserColorBar[], ): ColorBar => { - const colorBar: ColorBar = parseColorBar(colorBarName); + const colorBar: ColorBar = parseColorBarName(colorBarName); const imageData = colorBars.images[colorBar.baseName]; const { baseName } = colorBar; const userColorBar = userColorBars.find( @@ -321,10 +335,22 @@ const getVariableUserColorBarJson = ( if (userColorBar) { const colors = getUserColorBarHexRecords(userColorBar.code); if (colors) { - return JSON.stringify({ - name: colorBarName, - colors: colors.map((c) => [c.value, c.color]), - }); + if (userColorBar.categorical) { + return JSON.stringify({ + name: colorBarName, + colors: colors.map((c) => [Math.round(c.value), c.color]), + norm: "cat", + }); + } else { + const vMin = colors[0].value; + const vMax = colors[colors.length - 1].value; + const vRange = vMax - vMin; + return JSON.stringify({ + name: colorBarName, + colors: colors.map((c) => [(c.value - vMin) / vRange, c.color]), + norm: "lin", // or later also "log" + }); + } } } return null; @@ -857,11 +883,12 @@ const getVariableTileLayer = ( datasetTimeDimension: TimeDimension | null, attributions: string[] | null, variable: Variable | null, - colorBarMinMax: [number, number], colorBarName: string, + colorBarMinMax: [number, number], + colorBarNorm: ColorBarNorm, colorBarJson: string | null, - visibility: boolean, opacity: number, + visibility: boolean, layerId: string, zIndex: number, time: Time | null, @@ -876,9 +903,12 @@ const getVariableTileLayer = ( ["crs", mapProjection], ["vmin", `${colorBarMinMax[0]}`], ["vmax", `${colorBarMinMax[1]}`], - ["cbar", colorBarJson ? colorBarJson : colorBarName], + ["cmap", colorBarJson ? colorBarJson : colorBarName], // ['retina', '1'], ]; + if (colorBarNorm !== "lin") { + queryParams.push(["norm", colorBarNorm]); + } return getTileLayer( layerId, getTileUrl(server.url, datasetId!, variable.name), @@ -902,11 +932,12 @@ export const selectedDatasetVariableLayerSelector = createSelector( selectedDatasetTimeDimensionSelector, selectedDatasetAttributionsSelector, selectedVariableSelector, - selectedVariableColorBarMinMaxSelector, selectedVariableColorBarNameSelector, + selectedVariableColorBarMinMaxSelector, + selectedVariableColorBarNormSelector, selectedVariableUserColorBarJsonSelector, - selectedVariableVisibilitySelector, selectedVariableOpacitySelector, + selectedVariableVisibilitySelector, variableLayerId, variableZIndexSelector, selectedTimeSelector, @@ -922,11 +953,12 @@ export const selectedDatasetVariable2LayerSelector = createSelector( selectedDataset2TimeDimensionSelector, selectedDataset2AttributionsSelector, selectedVariable2Selector, - selectedVariable2ColorBarMinMaxSelector, selectedVariable2ColorBarNameSelector, + selectedVariable2ColorBarMinMaxSelector, + selectedVariable2ColorBarNormSelector, selectedVariable2UserColorBarJsonSelector, - selectedVariable2VisibilitySelector, selectedVariable2OpacitySelector, + selectedVariable2VisibilitySelector, variable2LayerId, variable2ZIndexSelector, selectedTimeSelector, diff --git a/src/volume/ColorBarTextures.ts b/src/volume/ColorBarTextures.ts index 889316ad..9fa5f757 100644 --- a/src/volume/ColorBarTextures.ts +++ b/src/volume/ColorBarTextures.ts @@ -24,7 +24,7 @@ import * as THREE from "three"; -import { ColorBar, formatColorBar } from "@/model/colorBar"; +import { ColorBar, formatColorBarName } from "@/model/colorBar"; class ColorBarTextures { private readonly textures: { [cmName: string]: THREE.Texture }; @@ -34,7 +34,7 @@ class ColorBarTextures { } get(colorBar: ColorBar, onLoad?: () => void): THREE.Texture { - const key = formatColorBar(colorBar); + const key = formatColorBarName(colorBar); let texture = this.textures[key]; if (!texture) { // const image = new Image(); From 9e46a9978e77b25f6a0731f1eb6997c1ab928e85 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sun, 12 May 2024 16:02:00 +0200 Subject: [PATCH 11/26] UserColorBar.categorical --> UserColorBar.discrete --- .../ColorBarLegend/UserColorBarEditor.tsx | 10 +++---- src/model/userColorBar.test.ts | 21 ++++++++++++++- src/model/userColorBar.ts | 16 ++++++------ src/selectors/controlSelectors.tsx | 26 +++++++------------ 4 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/components/ColorBarLegend/UserColorBarEditor.tsx b/src/components/ColorBarLegend/UserColorBarEditor.tsx index d312f32c..aa2d0e10 100644 --- a/src/components/ColorBarLegend/UserColorBarEditor.tsx +++ b/src/components/ColorBarLegend/UserColorBarEditor.tsx @@ -56,10 +56,10 @@ export default function UserColorBarEditor({ updateUserColorBar({ ...userColorBar, code: event.currentTarget.value }); }; - const handleCategoricalChange = () => { + const handleDiscreteChange = () => { updateUserColorBar({ ...userColorBar, - categorical: !userColorBar.categorical, + discrete: !userColorBar.discrete, }); }; @@ -93,8 +93,8 @@ export default function UserColorBarEditor({ > - {i18n.get("Categorical")} + {i18n.get("Discrete")} { - it("works as expected", () => { + it("works as expected if continuous", () => { const data = getUserColorBarRgbaArray( [ { value: 0.0, color: [35, 255, 82, 255] }, { value: 0.5, color: [255, 0, 0, 255] }, { value: 1.0, color: [120, 30, 255, 255] }, ], + false, + 10, + ); + expect(data).toBeInstanceOf(Uint8ClampedArray); + expect([...data]).toEqual([ + 35, 255, 82, 255, 84, 198, 64, 255, 133, 142, 46, 255, 182, 85, 27, 255, + 231, 28, 9, 255, 240, 3, 28, 255, 210, 10, 85, 255, 180, 17, 142, 255, + 150, 23, 198, 255, 120, 30, 255, 255, + ]); + }); + + it("works as expected if discrete", () => { + const data = getUserColorBarRgbaArray( + [ + { value: 0.0, color: [35, 255, 82, 255] }, + { value: 0.5, color: [255, 0, 0, 255] }, + { value: 1.0, color: [120, 30, 255, 255] }, + ], + true, 10, ); expect(data).toBeInstanceOf(Uint8ClampedArray); diff --git a/src/model/userColorBar.ts b/src/model/userColorBar.ts index 2b69c785..41a35831 100644 --- a/src/model/userColorBar.ts +++ b/src/model/userColorBar.ts @@ -48,9 +48,9 @@ export interface UserColorBar { */ code: string; /** - * Whether the color bar is categorical. + * Whether the color mapping is discrete or continuous. */ - categorical?: boolean; + discrete?: boolean; /** * base64-encoded `image/png` * rendered by renderUserColorBarAsBase64() from `code`. @@ -64,12 +64,12 @@ export interface UserColorBar { export function getUserColorBarRgbaArray( records: ColorRecord[], + discrete: boolean, size: number, - categorical?: boolean, ): Uint8ClampedArray { const rgbaArray = new Uint8ClampedArray(4 * size); const n = records.length; - if (categorical) { + if (discrete) { for (let i = 0, j = 0; i < size; i++, j += 4) { const recordIndex = Math.floor((n * i) / size); const [r, g, b, a] = records[recordIndex].color; @@ -92,7 +92,7 @@ export function getUserColorBarRgbaArray( v1 = values[recordIndex]; v2 = values[recordIndex + 1]; } - const w = categorical ? 0 : (v - v1) / (v2 - v1); + const w = discrete ? 0 : (v - v1) / (v2 - v1); const [r1, g1, b1, a1] = records[recordIndex].color; const [r2, g2, b2, a2] = records[recordIndex + 1].color; rgbaArray[j] = r1 + w * (r2 - r1); @@ -106,10 +106,10 @@ export function getUserColorBarRgbaArray( export function renderUserColorBar( records: ColorRecord[], - categorical: boolean, + discrete: boolean, canvas: HTMLCanvasElement, ): Promise { - const data = getUserColorBarRgbaArray(records, canvas.width, categorical); + const data = getUserColorBarRgbaArray(records, discrete, canvas.width); const imageData = new ImageData(data, data.length / 4, 1); return createImageBitmap(imageData).then((bitMap) => { const ctx = canvas.getContext("2d"); @@ -131,7 +131,7 @@ export function renderUserColorBarAsBase64( const canvas = document.createElement("canvas"); canvas.width = 256; canvas.height = 1; - return renderUserColorBar(colorRecords, !!colorBar.categorical, canvas).then( + return renderUserColorBar(colorRecords, !!colorBar.discrete, canvas).then( () => { const dataURL = canvas.toDataURL("image/png"); return { imageData: dataURL.split(",")[1] }; diff --git a/src/selectors/controlSelectors.tsx b/src/selectors/controlSelectors.tsx index cc5d510b..f2e39d30 100644 --- a/src/selectors/controlSelectors.tsx +++ b/src/selectors/controlSelectors.tsx @@ -293,6 +293,7 @@ export const colorBarsSelector = createSelector( const getVariableColorBar = ( colorBarName: string, + colorBarNorm: ColorBarNorm, colorBars: ColorBars, userColorBars: UserColorBar[], ): ColorBar => { @@ -303,7 +304,7 @@ const getVariableColorBar = ( (userColorBar) => userColorBar.id === baseName, ); let categories: HexColorRecord[] | undefined = undefined; - if (userColorBar && userColorBar.categorical) { + if (userColorBar && colorBarNorm === "cat") { categories = getUserColorBarHexRecords(userColorBar.code); } return { ...colorBar, imageData, categories }; @@ -311,6 +312,7 @@ const getVariableColorBar = ( export const selectedVariableColorBarSelector = createSelector( selectedVariableColorBarNameSelector, + selectedVariableColorBarNormSelector, colorBarsSelector, userColorBarsSelector, getVariableColorBar, @@ -318,6 +320,7 @@ export const selectedVariableColorBarSelector = createSelector( export const selectedVariable2ColorBarSelector = createSelector( selectedVariable2ColorBarNameSelector, + selectedVariable2ColorBarNormSelector, colorBarsSelector, userColorBarsSelector, getVariableColorBar, @@ -335,22 +338,11 @@ const getVariableUserColorBarJson = ( if (userColorBar) { const colors = getUserColorBarHexRecords(userColorBar.code); if (colors) { - if (userColorBar.categorical) { - return JSON.stringify({ - name: colorBarName, - colors: colors.map((c) => [Math.round(c.value), c.color]), - norm: "cat", - }); - } else { - const vMin = colors[0].value; - const vMax = colors[colors.length - 1].value; - const vRange = vMax - vMin; - return JSON.stringify({ - name: colorBarName, - colors: colors.map((c) => [(c.value - vMin) / vRange, c.color]), - norm: "lin", // or later also "log" - }); - } + return JSON.stringify({ + name: colorBarName, + colors: colors.map((c) => [c.value, c.color]), + ...(userColorBar.discrete ? { discrete: true } : {}), + }); } } return null; From ba1a3c82d6cea97354394a9dea6dedd7fe585f72 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sun, 12 May 2024 17:14:50 +0200 Subject: [PATCH 12/26] Can now switch color bar norm --- src/actions/dataActions.tsx | 16 ++- .../ColorBarLegend/ColorBarColorEditor.tsx | 7 +- .../ColorBarLegend/ColorBarGroupHeader.tsx | 3 +- .../ColorBarLegend/ColorBarLegend.tsx | 9 +- .../ColorBarLegendContinuous.tsx | 15 ++- .../ColorBarLegend/ColorBarRangeEditor.tsx | 19 ++- .../ColorBarLegend/ColorBarSelect.tsx | 13 +- .../ColorBarLegend/ColorBarStyleEditor.tsx | 126 ++++++++++++------ .../ColorBarLegend/UserColorBarEditor.tsx | 45 ++++--- src/connected/ColorBarLegend.tsx | 4 +- src/reducers/dataReducer.ts | 13 +- 11 files changed, 182 insertions(+), 88 deletions(-) diff --git a/src/actions/dataActions.tsx b/src/actions/dataActions.tsx index a1215203..900b6a1e 100644 --- a/src/actions/dataActions.tsx +++ b/src/actions/dataActions.tsx @@ -76,6 +76,7 @@ import { import { VolumeRenderMode } from "@/states/controlState"; import { MessageLogAction, postMessage } from "./messageLogActions"; import { renameUserPlaceInLayer } from "./mapActions"; +import { ColorBarNorm } from "@/model/variable"; //////////////////////////////////////////////////////////////////////////////// @@ -682,14 +683,16 @@ export interface UpdateVariableColorBar { type: typeof UPDATE_VARIABLE_COLOR_BAR; datasetId: string; variableName: string; - colorBarMinMax: [number, number]; colorBarName: string; + colorBarMinMax: [number, number]; + colorBarNorm: ColorBarNorm; opacity: number; } export function updateVariableColorBar( - colorBarMinMax: [number, number], colorBarName: string, + colorBarMinMax: [number, number], + colorBarNorm: ColorBarNorm, opacity: number, ) { return ( @@ -703,8 +706,9 @@ export function updateVariableColorBar( _updateVariableColorBar( selectedDatasetId, selectedVariableName, - colorBarMinMax, colorBarName, + colorBarMinMax, + colorBarNorm, opacity, ), ); @@ -715,16 +719,18 @@ export function updateVariableColorBar( export function _updateVariableColorBar( datasetId: string, variableName: string, - colorBarMinMax: [number, number], colorBarName: string, + colorBarMinMax: [number, number], + colorBarNorm: ColorBarNorm, opacity: number, ): UpdateVariableColorBar { return { type: UPDATE_VARIABLE_COLOR_BAR, datasetId, variableName, - colorBarMinMax, colorBarName, + colorBarMinMax, + colorBarNorm, opacity, }; } diff --git a/src/components/ColorBarLegend/ColorBarColorEditor.tsx b/src/components/ColorBarLegend/ColorBarColorEditor.tsx index bcf79f56..b3d8cd67 100644 --- a/src/components/ColorBarLegend/ColorBarColorEditor.tsx +++ b/src/components/ColorBarLegend/ColorBarColorEditor.tsx @@ -31,6 +31,7 @@ import { UserColorBar } from "@/model/userColorBar"; import ColorBarStyleEditor from "./ColorBarStyleEditor"; import ColorBarSelect from "./ColorBarSelect"; import { COLOR_BAR_ITEM_GAP, COLOR_BAR_BOX_MARGIN } from "./constants"; +import { ColorBarNorm } from "@/model/variable"; const useStyles = makeStyles((theme: Theme) => ({ colorBarBox: { @@ -42,13 +43,15 @@ const useStyles = makeStyles((theme: Theme) => ({ })); interface ColorBarColorEditorProps { - variableColorBarMinMax: [number, number]; variableColorBarName: string; + variableColorBarMinMax: [number, number]; + variableColorBarNorm: ColorBarNorm; variableColorBar: ColorBar; variableOpacity: number; updateVariableColorBar: ( - colorBarMinMax: [number, number], colorBarName: string, + colorBarMinMax: [number, number], + colorBarNorm: ColorBarNorm, opacity: number, ) => void; colorBars: ColorBars; diff --git a/src/components/ColorBarLegend/ColorBarGroupHeader.tsx b/src/components/ColorBarLegend/ColorBarGroupHeader.tsx index 446af931..201e774e 100644 --- a/src/components/ColorBarLegend/ColorBarGroupHeader.tsx +++ b/src/components/ColorBarLegend/ColorBarGroupHeader.tsx @@ -32,7 +32,8 @@ import { COLOR_BAR_ITEM_GAP } from "./constants"; const useStyles = makeStyles((theme: Theme) => ({ colorBarGroupTitle: { marginTop: theme.spacing(2 * COLOR_BAR_ITEM_GAP), - color: theme.palette.grey[400], + fontSize: "small", + color: theme.palette.text.secondary, }, })); diff --git a/src/components/ColorBarLegend/ColorBarLegend.tsx b/src/components/ColorBarLegend/ColorBarLegend.tsx index de58aa60..dd5bf700 100644 --- a/src/components/ColorBarLegend/ColorBarLegend.tsx +++ b/src/components/ColorBarLegend/ColorBarLegend.tsx @@ -32,6 +32,7 @@ import { ColorBar, ColorBars } from "@/model/colorBar"; import { UserColorBar } from "@/model/userColorBar"; import makeStyles from "@mui/styles/makeStyles"; import { Theme } from "@mui/material"; +import { ColorBarNorm } from "@/model/variable"; const useStyles = makeStyles((theme: Theme) => ({ title: { @@ -55,13 +56,15 @@ const useStyles = makeStyles((theme: Theme) => ({ interface ColorBarLegendProps { variableName: string | null; variableUnits: string; - variableColorBarMinMax: [number, number]; variableColorBarName: string; + variableColorBarMinMax: [number, number]; + variableColorBarNorm: ColorBarNorm; variableColorBar: ColorBar; variableOpacity: number; updateVariableColorBar: ( - colorBarMinMax: [number, number], colorBarName: string, + colorBarMinMax: [number, number], + colorBarNorm: ColorBarNorm, opacity: number, ) => void; colorBars: ColorBars; @@ -84,8 +87,6 @@ export default function ColorBarLegend( const [colorBarSelectAnchorEl, setColorBarSelectAnchorEl] = useState(null); - console.log("ColorBarLegendCategorical: ", variableColorBar); - const handleOpenColorBarSelect = () => { setColorBarSelectAnchorEl(colorBarSelectAnchorRef.current); }; diff --git a/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx b/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx index a2e30253..5c74d74d 100644 --- a/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx +++ b/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx @@ -29,16 +29,19 @@ import ColorBarCanvas from "./ColorBarCanvas"; import ColorBarRangeEditor from "./ColorBarRangeEditor"; import ColorBarLabels from "./ColorBarLabels"; import { ColorBar } from "@/model/colorBar"; +import { ColorBarNorm } from "@/model/variable"; export interface ColorBarLegendContinuousProps { variableTitle: string; - variableColorBarMinMax: [number, number]; variableColorBarName: string; + variableColorBarMinMax: [number, number]; + variableColorBarNorm: ColorBarNorm; variableColorBar: ColorBar; variableOpacity: number; updateVariableColorBar: ( - colorBarMinMax: [number, number], colorBarName: string, + colorBarMinMax: [number, number], + colorBarNorm: ColorBarNorm, opacity: number, ) => void; onOpenColorBarEditor: () => void; @@ -46,8 +49,9 @@ export interface ColorBarLegendContinuousProps { export default function ColorBarLegendContinuous({ variableTitle, - variableColorBarMinMax, variableColorBarName, + variableColorBarMinMax, + variableColorBarNorm, variableColorBar, variableOpacity, updateVariableColorBar, @@ -56,8 +60,6 @@ export default function ColorBarLegendContinuous({ const [colorBarRangeEditorAnchorEl, setColorBarRangeEditorAnchorEl] = useState(null); - console.log("ColorBarLegendContinuous", variableColorBar); - const handleOpenColorBarRangeEditor = (event: MouseEvent) => { setColorBarRangeEditorAnchorEl(event.currentTarget); }; @@ -88,8 +90,9 @@ export default function ColorBarLegendContinuous({ > diff --git a/src/components/ColorBarLegend/ColorBarRangeEditor.tsx b/src/components/ColorBarLegend/ColorBarRangeEditor.tsx index f16fc373..566c6049 100644 --- a/src/components/ColorBarLegend/ColorBarRangeEditor.tsx +++ b/src/components/ColorBarLegend/ColorBarRangeEditor.tsx @@ -31,6 +31,7 @@ import Slider from "@mui/material/Slider"; import { Mark } from "@mui/base/useSlider"; import { getLabelsFromArray } from "@/util/label"; +import { ColorBarNorm } from "@/model/variable"; const HOR_SLIDER_MARGIN = 5; @@ -62,20 +63,23 @@ const useStyles = makeStyles((theme: Theme) => ({ interface ColorBarRangeEditorProps { variableTitle: string; - variableColorBarMinMax: [number, number]; variableColorBarName: string; + variableColorBarMinMax: [number, number]; + variableColorBarNorm: ColorBarNorm; variableOpacity: number; updateVariableColorBar: ( - colorBarMinMax: [number, number], colorBarName: string, + colorBarMinMax: [number, number], + colorBarNorm: ColorBarNorm, opacity: number, ) => void; } export default function ColorBarRangeEditor({ variableTitle, - variableColorBarMinMax, variableColorBarName, + variableColorBarMinMax, + variableColorBarNorm, variableOpacity, updateVariableColorBar, }: ColorBarRangeEditorProps) { @@ -113,8 +117,9 @@ export default function ColorBarRangeEditor({ ) => { if (Array.isArray(value)) { updateVariableColorBar( - [value[0], value[1]], variableColorBarName, + [value[0], value[1]], + variableColorBarNorm, variableOpacity, ); } @@ -136,8 +141,9 @@ export default function ColorBarRangeEditor({ setCurrentColorBarMinMax(newMinMax); setOriginalColorBarMinMax(newMinMax); updateVariableColorBar( - newMinMax, variableColorBarName, + newMinMax, + variableColorBarNorm, variableOpacity, ); } @@ -163,8 +169,9 @@ export default function ColorBarRangeEditor({ setCurrentColorBarMinMax(newMinMax); setOriginalColorBarMinMax(newMinMax); updateVariableColorBar( - newMinMax, variableColorBarName, + newMinMax, + variableColorBarNorm, variableOpacity, ); } diff --git a/src/components/ColorBarLegend/ColorBarSelect.tsx b/src/components/ColorBarLegend/ColorBarSelect.tsx index 61fc65bc..1491dcf9 100644 --- a/src/components/ColorBarLegend/ColorBarSelect.tsx +++ b/src/components/ColorBarLegend/ColorBarSelect.tsx @@ -26,15 +26,18 @@ import { ColorBar, ColorBars, formatColorBarName } from "@/model/colorBar"; import { USER_COLOR_BAR_GROUP_TITLE, UserColorBar } from "@/model/userColorBar"; import ColorBarGroupComponent from "./ColorBarGroupComponent"; import UserColorBarGroup from "./UserColorBarGroup"; +import { ColorBarNorm } from "@/model/variable"; interface ColorBarSelectProps { - variableColorBarMinMax: [number, number]; variableColorBarName: string; + variableColorBarMinMax: [number, number]; + variableColorBarNorm: ColorBarNorm; variableColorBar: ColorBar; variableOpacity: number; updateVariableColorBar: ( - colorBarMinMax: [number, number], colorBarName: string, + colorBarMinMax: [number, number], + colorBarNorm: ColorBarNorm, opacity: number, ) => void; colorBars: ColorBars; @@ -46,8 +49,9 @@ interface ColorBarSelectProps { } export default function ColorBarSelect({ - variableColorBarMinMax, variableColorBarName, + variableColorBarMinMax, + variableColorBarNorm, variableColorBar, variableOpacity, updateVariableColorBar, @@ -64,8 +68,9 @@ export default function ColorBarSelect({ baseName, }); updateVariableColorBar( - variableColorBarMinMax, variableColorBarName, + variableColorBarMinMax, + variableColorBarNorm, variableOpacity, ); }; diff --git a/src/components/ColorBarLegend/ColorBarStyleEditor.tsx b/src/components/ColorBarLegend/ColorBarStyleEditor.tsx index 2c723aef..dda671d6 100644 --- a/src/components/ColorBarLegend/ColorBarStyleEditor.tsx +++ b/src/components/ColorBarLegend/ColorBarStyleEditor.tsx @@ -22,96 +22,146 @@ * SOFTWARE. */ -import React from "react"; +import { MouseEvent } from "react"; import Box from "@mui/material/Box"; -import Checkbox from "@mui/material/Checkbox"; import Slider from "@mui/material/Slider"; +import ToggleButton from "@mui/material/ToggleButton"; +import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; +import Tooltip from "@mui/material/Tooltip"; +import InvertColorsIcon from "@mui/icons-material/InvertColors"; +import OpacityIcon from "@mui/icons-material/Opacity"; import i18n from "@/i18n"; import { ColorBar, formatColorBarName } from "@/model/colorBar"; +import { ColorBarNorm } from "@/model/variable"; + +const TOGGLE_BUTTON_GROUP_BUTTON_STYLE = { paddingTop: 2, paddingBottom: 2 }; interface ColorBarSelectProps { - variableColorBarMinMax: [number, number]; variableColorBarName: string; + variableColorBarMinMax: [number, number]; + variableColorBarNorm: ColorBarNorm; variableColorBar: ColorBar; variableOpacity: number; updateVariableColorBar: ( - colorBarMinMax: [number, number], colorBarName: string, + colorBarMinMax: [number, number], + colorBarNorm: ColorBarNorm, opacity: number, ) => void; } export default function ColorBarStyleEditor({ - variableColorBarMinMax, variableColorBarName, + variableColorBarMinMax, + variableColorBarNorm, variableColorBar, variableOpacity, updateVariableColorBar, }: ColorBarSelectProps) { - const handleColorBarAlpha = (event: React.ChangeEvent) => { - const isAlpha = event.currentTarget.checked; + const handleColorBarAlpha = () => { + const isAlpha = !variableColorBar.isAlpha; variableColorBarName = formatColorBarName({ ...variableColorBar, isAlpha }); updateVariableColorBar( - variableColorBarMinMax, variableColorBarName, + variableColorBarMinMax, + variableColorBarNorm, variableOpacity, ); }; - const handleColorBarReversed = ( - event: React.ChangeEvent, - ) => { - const isReversed = event.currentTarget.checked; + const handleColorBarReversed = () => { + const isReversed = !variableColorBar.isReversed; variableColorBarName = formatColorBarName({ ...variableColorBar, isReversed, }); updateVariableColorBar( + variableColorBarName, variableColorBarMinMax, + variableColorBarNorm, + variableOpacity, + ); + }; + + const handleColorBarNorm = ( + _event: MouseEvent, + value: ColorBarNorm, + ) => { + updateVariableColorBar( variableColorBarName, + variableColorBarMinMax, + value, variableOpacity, ); }; const handleVariableOpacity = (_event: Event, value: number | number[]) => { updateVariableColorBar( - variableColorBarMinMax, variableColorBarName, + variableColorBarMinMax, + variableColorBarNorm, value as number, ); }; return ( <> - - + + + + + + + + + + + + + - {i18n.get("Hide small values")} - - {i18n.get("Reverse")} + > + + Lin + + + Log + + + Cat + + - {i18n.get("Opacity")} + ({ color: theme.palette.text.secondary })} + > + {i18n.get("Opacity")} + - - {i18n.get("Discrete")} + ({ + color: theme.palette.text.secondary, + fontSize: "small", + })} + > + {i18n.get("Discrete")} + + { return { variableName: selectedVariableNameSelector(state), variableUnits: selectedVariableUnitsSelector(state), - variableColorBarMinMax: selectedVariableColorBarMinMaxSelector(state), variableColorBarName: selectedVariableColorBarNameSelector(state), + variableColorBarMinMax: selectedVariableColorBarMinMaxSelector(state), + variableColorBarNorm: selectedVariableColorBarNormSelector(state), variableColorBar: selectedVariableColorBarSelector(state), variableOpacity: selectedVariableOpacitySelector(state), userColorBars: userColorBarsSelector(state), diff --git a/src/reducers/dataReducer.ts b/src/reducers/dataReducer.ts index 4c26a00b..876d8278 100644 --- a/src/reducers/dataReducer.ts +++ b/src/reducers/dataReducer.ts @@ -67,12 +67,19 @@ export function dataReducer( return { ...state, datasets: action.datasets }; } case UPDATE_VARIABLE_COLOR_BAR: { - const { datasetId, variableName, colorBarMinMax, colorBarName, opacity } = - action; + const { + datasetId, + variableName, + colorBarName, + colorBarMinMax, + colorBarNorm, + opacity, + } = action; const variableProps = { + colorBarName, colorBarMin: colorBarMinMax[0], colorBarMax: colorBarMinMax[1], - colorBarName, + colorBarNorm, opacity, }; return updateVariableProps(state, datasetId, variableName, variableProps); From 4038a042ac2af22a0fc0d60d6a32aaa5ce58458e Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Mon, 13 May 2024 07:27:24 +0200 Subject: [PATCH 13/26] Update --- CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 592d3a71..f1091910 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,12 @@ definition. Existing custom color bars can be edited and deleted. This feature requires xcube server >= 1.6. (#334) +* Users can now select the data normalisation function to be applied before + the actual color mapping takes place. Three functions are available: + - `Lin`: linear mapping of data values between `min` and `max` to colors. + - `Log`: logarithmic mapping of data values between `vmin` and `vmax` to colors. + - `Cat`: direct mapping of categorical data values into colors. + * Users can now zoom into arbitrary regions of a time-series chart by pressing the `CTRL` key of the keyboard. (#285) From 414650af35a284dad47f2929b9e8ead5bc2e5672 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Mon, 13 May 2024 17:09:08 +0200 Subject: [PATCH 14/26] introduced ColorMapType --- .../ColorBarLegend/ColorMapTypeEditor.tsx | 81 +++++++++++++++++++ .../ColorBarLegend/UserColorBarEditor.tsx | 42 ++-------- src/model/userColorBar.test.ts | 8 +- src/model/userColorBar.ts | 25 +++--- src/reducers/controlReducer.ts | 1 + src/selectors/controlSelectors.tsx | 2 +- 6 files changed, 109 insertions(+), 50 deletions(-) create mode 100644 src/components/ColorBarLegend/ColorMapTypeEditor.tsx diff --git a/src/components/ColorBarLegend/ColorMapTypeEditor.tsx b/src/components/ColorBarLegend/ColorMapTypeEditor.tsx new file mode 100644 index 00000000..6c2e979c --- /dev/null +++ b/src/components/ColorBarLegend/ColorMapTypeEditor.tsx @@ -0,0 +1,81 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019-2024 by the xcube development team and contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import Box from "@mui/material/Box"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Radio from "@mui/material/Radio"; +import RadioGroup from "@mui/material/RadioGroup"; +import Tooltip from "@mui/material/Tooltip"; + +import i18n from "@/i18n"; +import { ColorMapType } from "@/model/userColorBar"; + +const RADIO_GROUP_SX = { marginLeft: 1 }; +const RADIO_STYLE = { padding: 4 }; +const LABEL_BOX_SX = { fontSize: "small" }; + +const tooltipTitles: [ColorMapType, string][] = [ + ["node", "Values are nodes (continuous colors)"], + ["bound", "Values are bounds (discrete colors)"], + ["index", "Values are indices (discrete colors)"], +]; + +interface ColorMapTypeEditorProps { + colorMapType: ColorMapType; + setColorMapType: (colorMapType: ColorMapType) => void; +} + +export default function ColorMapTypeEditor({ + colorMapType, + setColorMapType, +}: ColorMapTypeEditorProps) { + return ( + { + setColorMapType(value as ColorMapType); + }} + sx={RADIO_GROUP_SX} + > + {tooltipTitles.map(([type, tooltipTitle]) => ( + + } + label={ + + {i18n.get(toLabel(type))} + + } + /> + + ))} + + ); +} + +function toLabel(type: string) { + return type[0].toUpperCase() + type.substring(1); +} diff --git a/src/components/ColorBarLegend/UserColorBarEditor.tsx b/src/components/ColorBarLegend/UserColorBarEditor.tsx index 27a20f71..a3994129 100644 --- a/src/components/ColorBarLegend/UserColorBarEditor.tsx +++ b/src/components/ColorBarLegend/UserColorBarEditor.tsx @@ -24,16 +24,16 @@ import { ChangeEvent } from "react"; import Box from "@mui/material/Box"; -import Checkbox from "@mui/material/Checkbox"; import TextField from "@mui/material/TextField"; -import i18n from "@/i18n"; import { USER_COLOR_BAR_CODE_EXAMPLE, UserColorBar, + ColorMapType, } from "@/model/userColorBar"; import DoneCancel from "@/components/DoneCancel"; import ColorBarItem from "./ColorBarItem"; +import ColorMapTypeEditor from "./ColorMapTypeEditor"; interface UserColorBarEditorProps { userColorBar: UserColorBar; @@ -56,10 +56,10 @@ export default function UserColorBarEditor({ updateUserColorBar({ ...userColorBar, code: event.currentTarget.value }); }; - const handleDiscreteChange = () => { + const handleTypeChange = (colorMapType: ColorMapType) => { updateUserColorBar({ ...userColorBar, - discrete: !userColorBar.discrete, + type: colorMapType, }); }; @@ -71,36 +71,10 @@ export default function UserColorBarEditor({ selected={selected} onSelect={onSelect} /> - - - ({ - color: theme.palette.text.secondary, - fontSize: "small", - })} - > - {i18n.get("Discrete")} - - + { - it("works as expected if continuous", () => { + it("works as expected if type='node'", () => { const data = getUserColorBarRgbaArray( [ { value: 0.0, color: [35, 255, 82, 255] }, { value: 0.5, color: [255, 0, 0, 255] }, { value: 1.0, color: [120, 30, 255, 255] }, ], - false, + "node", 10, ); expect(data).toBeInstanceOf(Uint8ClampedArray); @@ -48,14 +48,14 @@ describe("Assert that colorBar.getUserColorBarRgbaArray()", () => { ]); }); - it("works as expected if discrete", () => { + it("works as expected if type='index'", () => { const data = getUserColorBarRgbaArray( [ { value: 0.0, color: [35, 255, 82, 255] }, { value: 0.5, color: [255, 0, 0, 255] }, { value: 1.0, color: [120, 30, 255, 255] }, ], - true, + "index", 10, ); expect(data).toBeInstanceOf(Uint8ClampedArray); diff --git a/src/model/userColorBar.ts b/src/model/userColorBar.ts index 41a35831..1da92cb2 100644 --- a/src/model/userColorBar.ts +++ b/src/model/userColorBar.ts @@ -31,6 +31,8 @@ export const USER_COLOR_BAR_CODE_EXAMPLE = "0.5: red\n" + // tie point 2 "1.0: 120,30,255"; // tie point 3 +export type ColorMapType = "index" | "bound" | "node"; + export interface UserColorBar { /** * Unique ID. @@ -48,9 +50,9 @@ export interface UserColorBar { */ code: string; /** - * Whether the color mapping is discrete or continuous. + * Type of color mapping, discrete (= index or bounds) or continuous (=node). */ - discrete?: boolean; + type: ColorMapType; /** * base64-encoded `image/png` * rendered by renderUserColorBarAsBase64() from `code`. @@ -64,14 +66,15 @@ export interface UserColorBar { export function getUserColorBarRgbaArray( records: ColorRecord[], - discrete: boolean, + type: ColorMapType, size: number, ): Uint8ClampedArray { const rgbaArray = new Uint8ClampedArray(4 * size); const n = records.length; - if (discrete) { + if (type === "index" || type === "bound") { + const m = type === "index" ? n : n - 1; for (let i = 0, j = 0; i < size; i++, j += 4) { - const recordIndex = Math.floor((n * i) / size); + const recordIndex = Math.floor((m * i) / size); const [r, g, b, a] = records[recordIndex].color; rgbaArray[j] = r; rgbaArray[j + 1] = g; @@ -92,7 +95,7 @@ export function getUserColorBarRgbaArray( v1 = values[recordIndex]; v2 = values[recordIndex + 1]; } - const w = discrete ? 0 : (v - v1) / (v2 - v1); + const w = (v - v1) / (v2 - v1); const [r1, g1, b1, a1] = records[recordIndex].color; const [r2, g2, b2, a2] = records[recordIndex + 1].color; rgbaArray[j] = r1 + w * (r2 - r1); @@ -106,10 +109,10 @@ export function getUserColorBarRgbaArray( export function renderUserColorBar( records: ColorRecord[], - discrete: boolean, + type: ColorMapType, canvas: HTMLCanvasElement, ): Promise { - const data = getUserColorBarRgbaArray(records, discrete, canvas.width); + const data = getUserColorBarRgbaArray(records, type, canvas.width); const imageData = new ImageData(data, data.length / 4, 1); return createImageBitmap(imageData).then((bitMap) => { const ctx = canvas.getContext("2d"); @@ -120,10 +123,10 @@ export function renderUserColorBar( } export function renderUserColorBarAsBase64( - colorBar: UserColorBar, + userColorBar: UserColorBar, ): Promise<{ imageData?: string; errorMessage?: string }> { const { colorRecords, errorMessage } = getUserColorBarColorRecords( - colorBar.code, + userColorBar.code, ); if (!colorRecords) { return Promise.resolve({ errorMessage }); @@ -131,7 +134,7 @@ export function renderUserColorBarAsBase64( const canvas = document.createElement("canvas"); canvas.width = 256; canvas.height = 1; - return renderUserColorBar(colorRecords, !!colorBar.discrete, canvas).then( + return renderUserColorBar(colorRecords, userColorBar.type, canvas).then( () => { const dataURL = canvas.toDataURL("image/png"); return { imageData: dataURL.split(",")[1] }; diff --git a/src/reducers/controlReducer.ts b/src/reducers/controlReducer.ts index 7791166e..dd689c7f 100644 --- a/src/reducers/controlReducer.ts +++ b/src/reducers/controlReducer.ts @@ -353,6 +353,7 @@ export function controlReducer( userColorBars: [ { id: id, + type: "node", code: USER_COLOR_BAR_CODE_EXAMPLE, }, ...state.userColorBars, diff --git a/src/selectors/controlSelectors.tsx b/src/selectors/controlSelectors.tsx index f2e39d30..87410dc6 100644 --- a/src/selectors/controlSelectors.tsx +++ b/src/selectors/controlSelectors.tsx @@ -340,8 +340,8 @@ const getVariableUserColorBarJson = ( if (colors) { return JSON.stringify({ name: colorBarName, + type: userColorBar.type, colors: colors.map((c) => [c.value, c.color]), - ...(userColorBar.discrete ? { discrete: true } : {}), }); } } From 661da4916833d1ccf047e5d1965c7b8924d14499 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Tue, 14 May 2024 11:02:04 +0200 Subject: [PATCH 15/26] Disable normalisation "cat" if we do not have categories --- src/components/ColorBarLegend/ColorBarStyleEditor.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/ColorBarLegend/ColorBarStyleEditor.tsx b/src/components/ColorBarLegend/ColorBarStyleEditor.tsx index dda671d6..7df6479b 100644 --- a/src/components/ColorBarLegend/ColorBarStyleEditor.tsx +++ b/src/components/ColorBarLegend/ColorBarStyleEditor.tsx @@ -149,7 +149,11 @@ export default function ColorBarStyleEditor({ Log - + Cat From 1ee72f101764d8fcda451156d080072a5b256ec4 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Tue, 14 May 2024 15:06:29 +0200 Subject: [PATCH 16/26] Normalisation "cat" now works --- src/components/ColorBarLegend/ColorBarLegend.tsx | 9 +++++++-- src/model/colorBar.ts | 3 ++- src/selectors/controlSelectors.tsx | 8 ++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/ColorBarLegend/ColorBarLegend.tsx b/src/components/ColorBarLegend/ColorBarLegend.tsx index dd5bf700..c53c9975 100644 --- a/src/components/ColorBarLegend/ColorBarLegend.tsx +++ b/src/components/ColorBarLegend/ColorBarLegend.tsx @@ -81,7 +81,12 @@ export default function ColorBarLegend( ) { const classes = useStyles(); - const { variableName, variableUnits, variableColorBar } = props; + const { + variableName, + variableUnits, + variableColorBar, + variableColorBarNorm, + } = props; const colorBarSelectAnchorRef = useRef(null); const [colorBarSelectAnchorEl, setColorBarSelectAnchorEl] = @@ -111,7 +116,7 @@ export default function ColorBarLegend(
{variableTitle}
- {variableColorBar.categories ? ( + {variableColorBarNorm === "cat" && variableColorBar.categories ? ( { @@ -304,7 +303,10 @@ const getVariableColorBar = ( (userColorBar) => userColorBar.id === baseName, ); let categories: HexColorRecord[] | undefined = undefined; - if (userColorBar && colorBarNorm === "cat") { + if ( + userColorBar && + (userColorBar.type == "index" || userColorBar.type == "bound") + ) { categories = getUserColorBarHexRecords(userColorBar.code); } return { ...colorBar, imageData, categories }; @@ -312,7 +314,6 @@ const getVariableColorBar = ( export const selectedVariableColorBarSelector = createSelector( selectedVariableColorBarNameSelector, - selectedVariableColorBarNormSelector, colorBarsSelector, userColorBarsSelector, getVariableColorBar, @@ -320,7 +321,6 @@ export const selectedVariableColorBarSelector = createSelector( export const selectedVariable2ColorBarSelector = createSelector( selectedVariable2ColorBarNameSelector, - selectedVariable2ColorBarNormSelector, colorBarsSelector, userColorBarsSelector, getVariableColorBar, From e2cc2a7a9d617ee25916bad3569c3bc43517cf73 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Tue, 14 May 2024 16:48:33 +0200 Subject: [PATCH 17/26] Adjust min/max range for log normalisation --- src/actions/dataActions.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/actions/dataActions.tsx b/src/actions/dataActions.tsx index 900b6a1e..1ca51c6a 100644 --- a/src/actions/dataActions.tsx +++ b/src/actions/dataActions.tsx @@ -702,6 +702,18 @@ export function updateVariableColorBar( const selectedDatasetId = getState().controlState.selectedDatasetId; const selectedVariableName = getState().controlState.selectedVariableName; if (selectedDatasetId && selectedVariableName) { + if (colorBarNorm === "log") { + // Adjust range in case of log norm: Make sure xcube server can use + // matplotlib.colors.LogNorm(vmin, vmax) without errors + let [vMin, vMax] = colorBarMinMax; + if (vMin <= 0) { + vMin = 1e-3; + } + if (vMax <= vMin) { + vMax = 1; + } + colorBarMinMax = [vMin, vMax]; + } dispatch( _updateVariableColorBar( selectedDatasetId, From e3768103c7df5a134896dab8cc30bef9a736e0e3 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Tue, 14 May 2024 17:40:50 +0200 Subject: [PATCH 18/26] Fixed test --- src/model/userColorBar.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/model/userColorBar.test.ts b/src/model/userColorBar.test.ts index 8d6b0483..5e251ecb 100644 --- a/src/model/userColorBar.test.ts +++ b/src/model/userColorBar.test.ts @@ -59,10 +59,11 @@ describe("Assert that colorBar.getUserColorBarRgbaArray()", () => { 10, ); expect(data).toBeInstanceOf(Uint8ClampedArray); + console.log("data:", data); expect([...data]).toEqual([ - 35, 255, 82, 255, 84, 198, 64, 255, 133, 142, 46, 255, 182, 85, 27, 255, - 231, 28, 9, 255, 240, 3, 28, 255, 210, 10, 85, 255, 180, 17, 142, 255, - 150, 23, 198, 255, 120, 30, 255, 255, + 35, 255, 82, 255, 35, 255, 82, 255, 35, 255, 82, 255, 35, 255, 82, 255, + 255, 0, 0, 255, 255, 0, 0, 255, 255, 0, 0, 255, 120, 30, 255, 255, 120, + 30, 255, 255, 120, 30, 255, 255, ]); }); }); From f071bdfed9b52447ca6b2ed1b8ba2cbbf1adcc46 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Tue, 14 May 2024 17:49:14 +0200 Subject: [PATCH 19/26] renamed user color bar type "index" into "key" --- src/components/ColorBarLegend/ColorMapTypeEditor.tsx | 6 +++--- src/model/colorBar.ts | 2 +- src/model/userColorBar.test.ts | 4 ++-- src/model/userColorBar.ts | 6 +++--- src/selectors/controlSelectors.tsx | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/ColorBarLegend/ColorMapTypeEditor.tsx b/src/components/ColorBarLegend/ColorMapTypeEditor.tsx index 6c2e979c..69fc34e3 100644 --- a/src/components/ColorBarLegend/ColorMapTypeEditor.tsx +++ b/src/components/ColorBarLegend/ColorMapTypeEditor.tsx @@ -36,9 +36,9 @@ const RADIO_STYLE = { padding: 4 }; const LABEL_BOX_SX = { fontSize: "small" }; const tooltipTitles: [ColorMapType, string][] = [ - ["node", "Values are nodes (continuous colors)"], - ["bound", "Values are bounds (discrete colors)"], - ["index", "Values are indices (discrete colors)"], + ["node", "Values are nodes of a continuous color gradient"], + ["bound", "Values are bounds identifying individual colors"], + ["key", "Values are integer keys identifying individual colors"], ]; interface ColorMapTypeEditorProps { diff --git a/src/model/colorBar.ts b/src/model/colorBar.ts index e37b95e5..39379889 100644 --- a/src/model/colorBar.ts +++ b/src/model/colorBar.ts @@ -75,7 +75,7 @@ export interface ColorBar { imageData?: string; /** * Defined, if this color bar is a user-defined categorical color bar, - * that its `userColorBar.type` is "bound" or "index". + * that its `userColorBar.type` is "bound" or "key". */ categories?: HexColorRecord[]; } diff --git a/src/model/userColorBar.test.ts b/src/model/userColorBar.test.ts index 5e251ecb..dc0ba3a8 100644 --- a/src/model/userColorBar.test.ts +++ b/src/model/userColorBar.test.ts @@ -48,14 +48,14 @@ describe("Assert that colorBar.getUserColorBarRgbaArray()", () => { ]); }); - it("works as expected if type='index'", () => { + it("works as expected if type='key'", () => { const data = getUserColorBarRgbaArray( [ { value: 0.0, color: [35, 255, 82, 255] }, { value: 0.5, color: [255, 0, 0, 255] }, { value: 1.0, color: [120, 30, 255, 255] }, ], - "index", + "key", 10, ); expect(data).toBeInstanceOf(Uint8ClampedArray); diff --git a/src/model/userColorBar.ts b/src/model/userColorBar.ts index 1da92cb2..9c60dd95 100644 --- a/src/model/userColorBar.ts +++ b/src/model/userColorBar.ts @@ -31,7 +31,7 @@ export const USER_COLOR_BAR_CODE_EXAMPLE = "0.5: red\n" + // tie point 2 "1.0: 120,30,255"; // tie point 3 -export type ColorMapType = "index" | "bound" | "node"; +export type ColorMapType = "key" | "bound" | "node"; export interface UserColorBar { /** @@ -71,8 +71,8 @@ export function getUserColorBarRgbaArray( ): Uint8ClampedArray { const rgbaArray = new Uint8ClampedArray(4 * size); const n = records.length; - if (type === "index" || type === "bound") { - const m = type === "index" ? n : n - 1; + if (type === "key" || type === "bound") { + const m = type === "key" ? n : n - 1; for (let i = 0, j = 0; i < size; i++, j += 4) { const recordIndex = Math.floor((m * i) / size); const [r, g, b, a] = records[recordIndex].color; diff --git a/src/selectors/controlSelectors.tsx b/src/selectors/controlSelectors.tsx index 4240fedd..a07bc709 100644 --- a/src/selectors/controlSelectors.tsx +++ b/src/selectors/controlSelectors.tsx @@ -305,7 +305,7 @@ const getVariableColorBar = ( let categories: HexColorRecord[] | undefined = undefined; if ( userColorBar && - (userColorBar.type == "index" || userColorBar.type == "bound") + (userColorBar.type == "key" || userColorBar.type == "bound") ) { categories = getUserColorBarHexRecords(userColorBar.code); } From 432c92cc190a5d81afbc58956eb995424a84bad7 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 15 May 2024 13:00:42 +0200 Subject: [PATCH 20/26] labels now recognise log-scaling --- .../ColorBarLegend/ColorBarLabels.tsx | 12 +- .../ColorBarLegendContinuous.tsx | 1 + .../ColorBarLegend/ColorBarRangeEditor.tsx | 4 +- src/util/label.test.ts | 115 ++++++++++++++++++ src/util/label.ts | 88 ++++++++++---- 5 files changed, 193 insertions(+), 27 deletions(-) create mode 100644 src/util/label.test.ts diff --git a/src/components/ColorBarLegend/ColorBarLabels.tsx b/src/components/ColorBarLegend/ColorBarLabels.tsx index 14165886..5bd286d1 100644 --- a/src/components/ColorBarLegend/ColorBarLabels.tsx +++ b/src/components/ColorBarLegend/ColorBarLabels.tsx @@ -22,10 +22,10 @@ * SOFTWARE. */ -import React from "react"; +import React, { useMemo } from "react"; import makeStyles from "@mui/styles/makeStyles"; -import { getLabelsFromRange } from "@/util/label"; +import { getLabelsForRange } from "@/util/label"; const useStyles = makeStyles(() => ({ label: { @@ -42,6 +42,7 @@ interface ColorBarLabelsProps { minValue: number; maxValue: number; numTicks: number; + logScaled?: boolean; onClick: (event: React.MouseEvent) => void; } @@ -49,12 +50,17 @@ export default function ColorBarLabels({ minValue, maxValue, numTicks, + logScaled, onClick, }: ColorBarLabelsProps) { const classes = useStyles(); + const labels = useMemo( + () => getLabelsForRange(minValue, maxValue, numTicks, logScaled), + [minValue, maxValue, numTicks, logScaled], + ); return (
- {getLabelsFromRange(minValue, maxValue, numTicks).map((label, i) => ( + {labels.map((label, i) => ( {label} ))}
diff --git a/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx b/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx index 5c74d74d..fc2a32fe 100644 --- a/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx +++ b/src/components/ColorBarLegend/ColorBarLegendContinuous.tsx @@ -79,6 +79,7 @@ export default function ColorBarLegendContinuous({ minValue={variableColorBarMinMax[0]} maxValue={variableColorBarMinMax[1]} numTicks={5} + logScaled={variableColorBarNorm === "log"} onClick={handleOpenColorBarRangeEditor} /> { + const marks: Mark[] = getLabelsForArray(values).map((label, i) => { return { value: values[i], label }; }); diff --git a/src/util/label.test.ts b/src/util/label.test.ts new file mode 100644 index 00000000..fb046f1b --- /dev/null +++ b/src/util/label.test.ts @@ -0,0 +1,115 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019-2024 by the xcube development team and contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { expect, it, describe } from "vitest"; +import { getLabelsForRange, getLabelsForArray } from "./label"; + +describe("Assert that label.getLabelsForRange()", () => { + it("works for linear range", () => { + expect(getLabelsForRange(0.0, 0.25, 5)).toEqual([ + "0", + "0.06", + "0.13", + "0.19", + "0.25", + ]); + }); + it("works for log-scale range", () => { + expect(getLabelsForRange(0.001, 0.25, 5, true, false)).toEqual([ + "0.001", + "0.004", + "0.0158", + "0.0629", + "0.25", + ]); + }); + it("works for linear range, exponential", () => { + expect(getLabelsForRange(0.0, 0.25, 5, false, true)).toEqual([ + "0.00e+0", + "6.25e-2", + "1.25e-1", + "1.88e-1", + "2.50e-1", + ]); + }); + it("works for log-scale range, exponential", () => { + expect(getLabelsForRange(0.001, 0.25, 5, true, true)).toEqual([ + "1.00e-3", + "3.98e-3", + "1.58e-2", + "6.29e-2", + "2.50e-1", + ]); + }); +}); + +describe("Assert that label.getLabelsForArray()", () => { + it("works for integers", () => { + expect(getLabelsForArray([-1, 0, 1])).toEqual(["-1", "0", "1"]); + }); + it("works for floats", () => { + expect(getLabelsForArray([0.01, 0.02, 0.03])).toEqual([ + "0.01", + "0.02", + "0.03", + ]); + }); + it("works for 1e-2", () => { + expect(getLabelsForArray([0.0123, 0.0234, 0.0345])).toEqual([ + "0.012", + "0.023", + "0.035", + ]); + }); + it("works for 1e-4", () => { + expect(getLabelsForArray([0.000123, 0.000234, 0.000345])).toEqual([ + "0.00012", + "0.00023", + "0.00034", + ]); + }); + it("works for 1e+2", () => { + expect(getLabelsForArray([123.456, 234.567, 345.678])).toEqual([ + "123.46", + "234.57", + "345.68", + ]); + }); + + it("works for 1e-4 exponential", () => { + expect(getLabelsForArray([0.000123, 0.000234, 0.000345], true)).toEqual([ + "1.23e-4", + "2.34e-4", + "3.45e-4", + ]); + }); + + it("works for 1e+2 exponential", () => { + expect(getLabelsForArray([123.456, 234.567, 345.678], true)).toEqual([ + "1.23e+2", + "2.35e+2", + "3.46e+2", + ]); + }); +}); diff --git a/src/util/label.ts b/src/util/label.ts index bc4cdec2..7ecb409c 100644 --- a/src/util/label.ts +++ b/src/util/label.ts @@ -22,45 +22,89 @@ * SOFTWARE. */ -export function getLabelsFromArray(values: number[]): string[] { - const min = values[0]; - const max = values[values.length - 1]; - const fractionDigits = Math.min( - 10, - Math.max(2, Math.round(-Math.log10(max - min))), - ); - function _formatValue(value: number): string { - return formatValue(value, fractionDigits); - } - return values.map(_formatValue); -} - -export function getLabelsFromRange( +export function getLabelsForRange( minValue: number, maxValue: number, count: number = 5, + logScaled: boolean = false, + exponential: boolean = false, +): string[] { + return getLabelsForArray( + getRange(minValue, maxValue, count, logScaled), + exponential, + ); +} + +export function getLabelsForArray( + values: number[], + exponential: boolean = false, ): string[] { - return getLabelsFromArray(arange(minValue, maxValue, count)); + const fractionDigits = exponential ? 2 : getFractionDigits(values); + return values.map((v) => getLabelForValue(v, fractionDigits, exponential)); } -function formatValue(value: number, fractionDigits: number): string { +function getLabelForValue( + value: number, + fractionDigits: number, + exponential?: boolean, +): string { + if (exponential) { + return value.toExponential(fractionDigits); + } const valueRounded = Math.round(value); - if (Math.abs(valueRounded - value) < 1e-8) { + if (valueRounded === value || Math.abs(valueRounded - value) < 1e-8) { return valueRounded + ""; } else { - return value.toFixed(fractionDigits); + let label = value.toFixed(fractionDigits); + // Strip trailing "0"s + if (label.includes(".")) { + while (label.endsWith("0")) { + label = label.substring(0, label.length - 1); + } + } + return label; } } -export function arange( +export function getFractionDigits(values: number[]): number { + const min = values[0]; + const max = values[values.length - 1]; + const v1 = getFractionDigitsForScalar(min); + const v2 = getFractionDigitsForScalar(max); + const n = Math.max(v1, v2); + console.info("getFractionDigits", v1, v2, n); + return Math.min(10, Math.max(2, n + 1)); +} + +function getFractionDigitsForScalar(x: number): number { + if (x === 0) { + return 0; + } + const n = Math.floor(Math.log10(Math.abs(x))); + return n < 0 ? -n : 0; +} + +function getRange( minValue: number, maxValue: number, count: number, + logScaled?: boolean, ): number[] { - const delta = (maxValue - minValue) / (count - 1); const ticks = new Array(count); - for (let i = 0; i < count; i++) { - ticks[i] = minValue + i * delta; + if (logScaled) { + const logMin = Math.log10(minValue); + const logMax = Math.log10(maxValue); + const logDelta = (logMax - logMin) / (count - 1); + for (let i = 1; i < count - 1; i++) { + ticks[i] = Math.pow(10, logMin + i * logDelta); + } + } else { + const delta = (maxValue - minValue) / (count - 1); + for (let i = 1; i < count - 1; i++) { + ticks[i] = minValue + i * delta; + } } + ticks[0] = minValue; + ticks[count - 1] = maxValue; return ticks; } From 2e7b93ba6f5f89caf2d6e32b706e65ee8f103fdd Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 15 May 2024 19:58:58 +0200 Subject: [PATCH 21/26] log-scaled range slider --- .../ColorBarLegend/ColorBarRangeEditor.tsx | 153 +++++----------- .../ColorBarLegend/ColorBarRangeSlider.tsx | 168 ++++++++++++++++++ src/util/label.ts | 1 - 3 files changed, 210 insertions(+), 112 deletions(-) create mode 100644 src/components/ColorBarLegend/ColorBarRangeSlider.tsx diff --git a/src/components/ColorBarLegend/ColorBarRangeEditor.tsx b/src/components/ColorBarLegend/ColorBarRangeEditor.tsx index b9e7b5cb..c17a96d7 100644 --- a/src/components/ColorBarLegend/ColorBarRangeEditor.tsx +++ b/src/components/ColorBarLegend/ColorBarRangeEditor.tsx @@ -22,16 +22,14 @@ * SOFTWARE. */ -import React, { useEffect, useState, SyntheticEvent } from "react"; +import React, { useEffect, useState } from "react"; import makeStyles from "@mui/styles/makeStyles"; import { Theme } from "@mui/material"; import TextField from "@mui/material/TextField"; import Box from "@mui/material/Box"; -import Slider from "@mui/material/Slider"; -import { Mark } from "@mui/base/useSlider"; -import { getLabelsForArray } from "@/util/label"; import { ColorBarNorm } from "@/model/variable"; +import ColorBarRangeSlider from "./ColorBarRangeSlider"; const HOR_SLIDER_MARGIN = 5; @@ -85,61 +83,35 @@ export default function ColorBarRangeEditor({ }: ColorBarRangeEditorProps) { const classes = useStyles(); - const [currentColorBarMinMax, setCurrentColorBarMinMax] = useState< - [number, number] - >(variableColorBarMinMax); - const [originalColorBarMinMax, setOriginalColorBarMinMax] = useState< - [number, number] - >(variableColorBarMinMax); - const [enteredColorBarMinMax, setEnteredColorBarMinMax] = useState< - [string, string] - >(minMaxToText(variableColorBarMinMax)); - const [enteredColorBarMinMaxError, setEnteredColorBarMinMaxError] = useState< + const [currentMinMax, setCurrentMinMax] = useState<[number, number]>( + variableColorBarMinMax, + ); + const [originalMinMax, setOriginalMinMax] = useState<[number, number]>( + variableColorBarMinMax, + ); + const [enteredMinMax, setEnteredMinMax] = useState<[string, string]>( + minMaxToText(variableColorBarMinMax), + ); + const [enteredMinMaxError, setEnteredMinMaxError] = useState< [boolean, boolean] >([false, false]); useEffect(() => { - setEnteredColorBarMinMax(minMaxToText(variableColorBarMinMax)); + setEnteredMinMax(minMaxToText(variableColorBarMinMax)); }, [variableColorBarMinMax]); - const handleColorBarMinMaxChange = ( - _event: Event, - value: number | number[], - ) => { - if (Array.isArray(value)) { - setCurrentColorBarMinMax([value[0], value[1]]); - } - }; - - const handleColorBarMinMaxChangeCommitted = ( - _event: Event | SyntheticEvent, - value: number | number[], - ) => { - if (Array.isArray(value)) { - updateVariableColorBar( - variableColorBarName, - [value[0], value[1]], - variableColorBarNorm, - variableOpacity, - ); - } - }; - - const handleEnteredColorBarMinChange = ( + const handleEnteredMinChange = ( event: React.ChangeEvent, ) => { const enteredValue = event.target.value; - setEnteredColorBarMinMax([enteredValue, enteredColorBarMinMax[1]]); + setEnteredMinMax([enteredValue, enteredMinMax[1]]); const minValue = Number.parseFloat(enteredValue); let error = false; - if (!Number.isNaN(minValue) && minValue < currentColorBarMinMax[1]) { - if (minValue !== currentColorBarMinMax[0]) { - const newMinMax: [number, number] = [ - minValue, - currentColorBarMinMax[1], - ]; - setCurrentColorBarMinMax(newMinMax); - setOriginalColorBarMinMax(newMinMax); + if (!Number.isNaN(minValue) && minValue < currentMinMax[1]) { + if (minValue !== currentMinMax[0]) { + const newMinMax: [number, number] = [minValue, currentMinMax[1]]; + setCurrentMinMax(newMinMax); + setOriginalMinMax(newMinMax); updateVariableColorBar( variableColorBarName, newMinMax, @@ -150,24 +122,21 @@ export default function ColorBarRangeEditor({ } else { error = true; } - setEnteredColorBarMinMaxError([error, enteredColorBarMinMaxError[1]]); + setEnteredMinMaxError([error, enteredMinMaxError[1]]); }; - const handleEnteredColorBarMaxChange = ( + const handleEnteredMaxChange = ( event: React.ChangeEvent, ) => { const enteredValue = event.target.value; - setEnteredColorBarMinMax([enteredColorBarMinMax[0], enteredValue]); + setEnteredMinMax([enteredMinMax[0], enteredValue]); const maxValue = Number.parseFloat(enteredValue); let error = false; - if (!Number.isNaN(maxValue) && maxValue > currentColorBarMinMax[0]) { - if (maxValue !== currentColorBarMinMax[1]) { - const newMinMax: [number, number] = [ - currentColorBarMinMax[0], - maxValue, - ]; - setCurrentColorBarMinMax(newMinMax); - setOriginalColorBarMinMax(newMinMax); + if (!Number.isNaN(maxValue) && maxValue > currentMinMax[0]) { + if (maxValue !== currentMinMax[1]) { + const newMinMax: [number, number] = [currentMinMax[0], maxValue]; + setCurrentMinMax(newMinMax); + setOriginalMinMax(newMinMax); updateVariableColorBar( variableColorBarName, newMinMax, @@ -178,58 +147,20 @@ export default function ColorBarRangeEditor({ } else { error = true; } - setEnteredColorBarMinMaxError([enteredColorBarMinMaxError[0], error]); + setEnteredMinMaxError([enteredMinMaxError[0], error]); }; - const [original1, original2] = originalColorBarMinMax; - const dist = original1 < original2 ? original2 - original1 : 1; - const distExp = Math.floor(Math.log10(dist)); - const distNorm = dist * Math.pow(10, -distExp); - - let numStepsInner = null; - for (const delta of [0.25, 0.2, 0.15, 0.125, 0.1]) { - const numStepsFloat = distNorm / delta; - const numStepsInt = Math.floor(numStepsFloat); - if (Math.abs(numStepsInt - numStepsFloat) < 1e-10) { - numStepsInner = numStepsInt; - break; - } - } - - let numStepsOuter; - if (numStepsInner !== null && numStepsInner >= 2) { - numStepsOuter = Math.max(2, Math.round(numStepsInner / 2)); - } else { - numStepsOuter = 4; - numStepsInner = 8; - } - - const delta = original1 < original2 ? dist / numStepsInner : 0.5; - const numSteps = numStepsInner + 2 * numStepsOuter; - const total1 = original1 - numStepsOuter * delta; - const total2 = original2 + numStepsOuter * delta; - const step = (total2 - total1) / numSteps; - - const values = [total1, original1, original2, total2]; - - const marks: Mark[] = getLabelsForArray(values).map((label, i) => { - return { value: values[i], label }; - }); - return (
{variableTitle} - @@ -238,18 +169,18 @@ export default function ColorBarRangeEditor({ label="Minimum" variant="filled" size="small" - value={enteredColorBarMinMax[0]} - error={enteredColorBarMinMaxError[0]} - onChange={(evt) => handleEnteredColorBarMinChange(evt)} + value={enteredMinMax[0]} + error={enteredMinMaxError[0]} + onChange={(evt) => handleEnteredMinChange(evt)} /> handleEnteredColorBarMaxChange(evt)} + value={enteredMinMax[1]} + error={enteredMinMaxError[1]} + onChange={(evt) => handleEnteredMaxChange(evt)} />
diff --git a/src/components/ColorBarLegend/ColorBarRangeSlider.tsx b/src/components/ColorBarLegend/ColorBarRangeSlider.tsx new file mode 100644 index 00000000..5492c2af --- /dev/null +++ b/src/components/ColorBarLegend/ColorBarRangeSlider.tsx @@ -0,0 +1,168 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019-2024 by the xcube development team and contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining x copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { useState, SyntheticEvent } from "react"; +import Slider from "@mui/material/Slider"; +import { Mark } from "@mui/base/useSlider"; + +import { getLabelsForArray } from "@/util/label"; +import { ColorBarNorm } from "@/model/variable"; + +interface ColorBarRangeSliderProps { + variableColorBarName: string; + variableColorBarMinMax: [number, number]; + variableColorBarNorm: ColorBarNorm; + variableOpacity: number; + updateVariableColorBar: ( + colorBarName: string, + colorBarMinMax: [number, number], + colorBarNorm: ColorBarNorm, + opacity: number, + ) => void; + originalColorBarMinMax: [number, number]; +} + +export default function ColorBarRangeSlider({ + variableColorBarName, + variableColorBarMinMax, + variableColorBarNorm, + variableOpacity, + updateVariableColorBar, + originalColorBarMinMax, +}: ColorBarRangeSliderProps) { + const norm = new Norm(variableColorBarNorm === "log"); + + const [currentMinMax, setCurrentMinMax] = useState<[number, number]>(() => + norm.scale(variableColorBarMinMax), + ); + + const handleMinMaxChange = (_event: Event, value: number | number[]) => { + if (Array.isArray(value)) { + setCurrentMinMax(value as [number, number]); + } + }; + + const handleMinMaxChangeCommitted = ( + _event: Event | SyntheticEvent, + value: number | number[], + ) => { + if (Array.isArray(value)) { + updateVariableColorBar( + variableColorBarName, + norm.scaleInv(value as [number, number]), + variableColorBarNorm, + variableOpacity, + ); + } + }; + + const [original1, original2] = norm.scale(originalColorBarMinMax); + const dist = original1 < original2 ? original2 - original1 : 1; + const distExp = Math.floor(Math.log10(dist)); + const distNorm = dist * Math.pow(10, -distExp); + + let numStepsInner = null; + for (const delta of [0.25, 0.2, 0.15, 0.125, 0.1]) { + const numStepsFloat = distNorm / delta; + const numStepsInt = Math.floor(numStepsFloat); + if (Math.abs(numStepsInt - numStepsFloat) < 1e-10) { + numStepsInner = numStepsInt; + break; + } + } + + let numStepsOuter; + if (numStepsInner !== null && numStepsInner >= 2) { + numStepsOuter = Math.max(2, Math.round(numStepsInner / 2)); + } else { + numStepsOuter = 4; + numStepsInner = 8; + } + + const delta = original1 < original2 ? dist / numStepsInner : 0.5; + const numSteps = numStepsInner + 2 * numStepsOuter; + const total1 = original1 - numStepsOuter * delta; + const total2 = original2 + numStepsOuter * delta; + const step = (total2 - total1) / numSteps; + + const values = [total1, original1, original2, total2]; + + console.log("values:", values, norm.scaleInv(values)); + + const marks: Mark[] = getLabelsForArray(norm.scaleInv(values)).map( + (label, i) => { + return { value: values[i], label }; + }, + ); + + return ( + norm.scaleInv(v)} + onChange={handleMinMaxChange} + onChangeCommitted={handleMinMaxChangeCommitted} + valueLabelDisplay="on" + size="small" + /> + ); +} + +// noinspection JSUnusedGlobalSymbols +class Norm { + readonly isLog: boolean; + + constructor(isLog: boolean) { + this.isLog = isLog; + } + + scale(x: number): number; + scale(x: [number, number]): [number, number]; + scale(x: number[]): number[]; + scale( + x: number | [number, number] | number[], + ): number | [number, number] | number[] { + if (!this.isLog) { + return x; + } + return typeof x === "number" ? Math.log10(x) : x.map((v) => Math.log10(v)); + } + + scaleInv(x: number): number; + scaleInv(x: [number, number]): [number, number]; + scaleInv(x: number[]): number[]; + scaleInv( + x: number | [number, number] | number[], + ): number | [number, number] | number[] { + if (!this.isLog) { + return x; + } + return typeof x === "number" + ? Math.pow(10, x) + : x.map((v) => Math.pow(10, v)); + } +} diff --git a/src/util/label.ts b/src/util/label.ts index 7ecb409c..e92b52a3 100644 --- a/src/util/label.ts +++ b/src/util/label.ts @@ -72,7 +72,6 @@ export function getFractionDigits(values: number[]): number { const v1 = getFractionDigitsForScalar(min); const v2 = getFractionDigitsForScalar(max); const n = Math.max(v1, v2); - console.info("getFractionDigits", v1, v2, n); return Math.min(10, Math.max(2, n + 1)); } From aa5160c05db1b355953e5cce2da9858a6f85640e Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 15 May 2024 20:16:51 +0200 Subject: [PATCH 22/26] Seems to work now, but value formatting is not optimal --- src/components/ColorBarLegend/ColorBarRangeSlider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ColorBarLegend/ColorBarRangeSlider.tsx b/src/components/ColorBarLegend/ColorBarRangeSlider.tsx index 5492c2af..06ecfedf 100644 --- a/src/components/ColorBarLegend/ColorBarRangeSlider.tsx +++ b/src/components/ColorBarLegend/ColorBarRangeSlider.tsx @@ -123,7 +123,7 @@ export default function ColorBarRangeSlider({ value={currentMinMax} marks={marks} step={step} - // scale={(v) => norm.scaleInv(v)} + valueLabelFormat={(v) => getLabelsForArray([norm.scaleInv(v)])[0]} onChange={handleMinMaxChange} onChangeCommitted={handleMinMaxChangeCommitted} valueLabelDisplay="on" From 94aff879c470206f5b2e496116539f213c2fe80c Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 15 May 2024 20:45:54 +0200 Subject: [PATCH 23/26] It works now --- .../ColorBarLegend/ColorBarRangeSlider.tsx | 18 ++++++---- src/util/label.test.ts | 24 ++++++------- src/util/label.ts | 34 ++++++++----------- 3 files changed, 37 insertions(+), 39 deletions(-) diff --git a/src/components/ColorBarLegend/ColorBarRangeSlider.tsx b/src/components/ColorBarLegend/ColorBarRangeSlider.tsx index 06ecfedf..06d06bad 100644 --- a/src/components/ColorBarLegend/ColorBarRangeSlider.tsx +++ b/src/components/ColorBarLegend/ColorBarRangeSlider.tsx @@ -26,7 +26,7 @@ import { useState, SyntheticEvent } from "react"; import Slider from "@mui/material/Slider"; import { Mark } from "@mui/base/useSlider"; -import { getLabelsForArray } from "@/util/label"; +import { getLabelsForValues, getLabelForValue } from "@/util/label"; import { ColorBarNorm } from "@/model/variable"; interface ColorBarRangeSliderProps { @@ -68,9 +68,15 @@ export default function ColorBarRangeSlider({ value: number | number[], ) => { if (Array.isArray(value)) { + // Here we convert to the precision of the displayed labels. + // Otherwise, we'd get a lot of confusing digits if log-scaled. + const minMaxLabels = getLabelsForValues(norm.scaleInv(value)); + const minMaxValues = minMaxLabels.map((label) => + Number.parseFloat(label), + ); updateVariableColorBar( variableColorBarName, - norm.scaleInv(value as [number, number]), + minMaxValues as [number, number], variableColorBarNorm, variableOpacity, ); @@ -108,9 +114,7 @@ export default function ColorBarRangeSlider({ const values = [total1, original1, original2, total2]; - console.log("values:", values, norm.scaleInv(values)); - - const marks: Mark[] = getLabelsForArray(norm.scaleInv(values)).map( + const marks: Mark[] = getLabelsForValues(norm.scaleInv(values)).map( (label, i) => { return { value: values[i], label }; }, @@ -123,10 +127,10 @@ export default function ColorBarRangeSlider({ value={currentMinMax} marks={marks} step={step} - valueLabelFormat={(v) => getLabelsForArray([norm.scaleInv(v)])[0]} + valueLabelFormat={(v) => getLabelForValue(norm.scaleInv(v))} onChange={handleMinMaxChange} onChangeCommitted={handleMinMaxChangeCommitted} - valueLabelDisplay="on" + valueLabelDisplay="auto" size="small" /> ); diff --git a/src/util/label.test.ts b/src/util/label.test.ts index fb046f1b..15ca82cc 100644 --- a/src/util/label.test.ts +++ b/src/util/label.test.ts @@ -23,13 +23,13 @@ */ import { expect, it, describe } from "vitest"; -import { getLabelsForRange, getLabelsForArray } from "./label"; +import { getLabelsForRange, getLabelsForValues } from "./label"; describe("Assert that label.getLabelsForRange()", () => { it("works for linear range", () => { expect(getLabelsForRange(0.0, 0.25, 5)).toEqual([ "0", - "0.06", + "0.063", "0.13", "0.19", "0.25", @@ -39,8 +39,8 @@ describe("Assert that label.getLabelsForRange()", () => { expect(getLabelsForRange(0.001, 0.25, 5, true, false)).toEqual([ "0.001", "0.004", - "0.0158", - "0.0629", + "0.016", + "0.063", "0.25", ]); }); @@ -64,33 +64,33 @@ describe("Assert that label.getLabelsForRange()", () => { }); }); -describe("Assert that label.getLabelsForArray()", () => { +describe("Assert that label.getLabelsForValues()", () => { it("works for integers", () => { - expect(getLabelsForArray([-1, 0, 1])).toEqual(["-1", "0", "1"]); + expect(getLabelsForValues([-1, 0, 1])).toEqual(["-1", "0", "1"]); }); it("works for floats", () => { - expect(getLabelsForArray([0.01, 0.02, 0.03])).toEqual([ + expect(getLabelsForValues([0.01, 0.02, 0.03])).toEqual([ "0.01", "0.02", "0.03", ]); }); it("works for 1e-2", () => { - expect(getLabelsForArray([0.0123, 0.0234, 0.0345])).toEqual([ + expect(getLabelsForValues([0.0123, 0.0234, 0.0345])).toEqual([ "0.012", "0.023", "0.035", ]); }); it("works for 1e-4", () => { - expect(getLabelsForArray([0.000123, 0.000234, 0.000345])).toEqual([ + expect(getLabelsForValues([0.000123, 0.000234, 0.000345])).toEqual([ "0.00012", "0.00023", "0.00034", ]); }); it("works for 1e+2", () => { - expect(getLabelsForArray([123.456, 234.567, 345.678])).toEqual([ + expect(getLabelsForValues([123.456, 234.567, 345.678])).toEqual([ "123.46", "234.57", "345.68", @@ -98,7 +98,7 @@ describe("Assert that label.getLabelsForArray()", () => { }); it("works for 1e-4 exponential", () => { - expect(getLabelsForArray([0.000123, 0.000234, 0.000345], true)).toEqual([ + expect(getLabelsForValues([0.000123, 0.000234, 0.000345], true)).toEqual([ "1.23e-4", "2.34e-4", "3.45e-4", @@ -106,7 +106,7 @@ describe("Assert that label.getLabelsForArray()", () => { }); it("works for 1e+2 exponential", () => { - expect(getLabelsForArray([123.456, 234.567, 345.678], true)).toEqual([ + expect(getLabelsForValues([123.456, 234.567, 345.678], true)).toEqual([ "1.23e+2", "2.35e+2", "3.46e+2", diff --git a/src/util/label.ts b/src/util/label.ts index e92b52a3..223d7000 100644 --- a/src/util/label.ts +++ b/src/util/label.ts @@ -29,25 +29,28 @@ export function getLabelsForRange( logScaled: boolean = false, exponential: boolean = false, ): string[] { - return getLabelsForArray( + return getLabelsForValues( getRange(minValue, maxValue, count, logScaled), exponential, ); } -export function getLabelsForArray( +export function getLabelsForValues( values: number[], exponential: boolean = false, ): string[] { - const fractionDigits = exponential ? 2 : getFractionDigits(values); - return values.map((v) => getLabelForValue(v, fractionDigits, exponential)); + // const fractionDigits = exponential ? 2 : getFractionDigits(values); + return values.map((v) => getLabelForValue(v, undefined, exponential)); } -function getLabelForValue( +export function getLabelForValue( value: number, - fractionDigits: number, + fractionDigits?: number, exponential?: boolean, ): string { + if (fractionDigits === undefined) { + fractionDigits = exponential ? 2 : getSignificantDigits(value); + } if (exponential) { return value.toExponential(fractionDigits); } @@ -58,7 +61,7 @@ function getLabelForValue( let label = value.toFixed(fractionDigits); // Strip trailing "0"s if (label.includes(".")) { - while (label.endsWith("0")) { + while (label.endsWith("0") && !label.endsWith(".0")) { label = label.substring(0, label.length - 1); } } @@ -66,21 +69,12 @@ function getLabelForValue( } } -export function getFractionDigits(values: number[]): number { - const min = values[0]; - const max = values[values.length - 1]; - const v1 = getFractionDigitsForScalar(min); - const v2 = getFractionDigitsForScalar(max); - const n = Math.max(v1, v2); - return Math.min(10, Math.max(2, n + 1)); -} - -function getFractionDigitsForScalar(x: number): number { - if (x === 0) { +function getSignificantDigits(x: number): number { + if (x === 0 || x === Math.floor(x)) { return 0; } - const n = Math.floor(Math.log10(Math.abs(x))); - return n < 0 ? -n : 0; + const exp = Math.floor(Math.log10(Math.abs(x))); + return Math.min(16, Math.max(2, exp < 0 ? 1 - exp : 0)); } function getRange( From f729a66e02254cd9c1d7fdbd97e20a4d98456710 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 16 May 2024 08:36:13 +0200 Subject: [PATCH 24/26] refactorings --- .../ColorBarLegend/ColorBarRangeSlider.tsx | 50 ++----------- src/components/ColorBarLegend/scaler.ts | 72 +++++++++++++++++++ 2 files changed, 79 insertions(+), 43 deletions(-) create mode 100644 src/components/ColorBarLegend/scaler.ts diff --git a/src/components/ColorBarLegend/ColorBarRangeSlider.tsx b/src/components/ColorBarLegend/ColorBarRangeSlider.tsx index 06d06bad..fe6a73f7 100644 --- a/src/components/ColorBarLegend/ColorBarRangeSlider.tsx +++ b/src/components/ColorBarLegend/ColorBarRangeSlider.tsx @@ -28,6 +28,7 @@ import { Mark } from "@mui/base/useSlider"; import { getLabelsForValues, getLabelForValue } from "@/util/label"; import { ColorBarNorm } from "@/model/variable"; +import Scaler from "./scaler"; interface ColorBarRangeSliderProps { variableColorBarName: string; @@ -51,10 +52,10 @@ export default function ColorBarRangeSlider({ updateVariableColorBar, originalColorBarMinMax, }: ColorBarRangeSliderProps) { - const norm = new Norm(variableColorBarNorm === "log"); + const scaler = new Scaler(variableColorBarNorm === "log"); const [currentMinMax, setCurrentMinMax] = useState<[number, number]>(() => - norm.scale(variableColorBarMinMax), + scaler.scale(variableColorBarMinMax), ); const handleMinMaxChange = (_event: Event, value: number | number[]) => { @@ -70,7 +71,7 @@ export default function ColorBarRangeSlider({ if (Array.isArray(value)) { // Here we convert to the precision of the displayed labels. // Otherwise, we'd get a lot of confusing digits if log-scaled. - const minMaxLabels = getLabelsForValues(norm.scaleInv(value)); + const minMaxLabels = getLabelsForValues(scaler.scaleInv(value)); const minMaxValues = minMaxLabels.map((label) => Number.parseFloat(label), ); @@ -83,7 +84,7 @@ export default function ColorBarRangeSlider({ } }; - const [original1, original2] = norm.scale(originalColorBarMinMax); + const [original1, original2] = scaler.scale(originalColorBarMinMax); const dist = original1 < original2 ? original2 - original1 : 1; const distExp = Math.floor(Math.log10(dist)); const distNorm = dist * Math.pow(10, -distExp); @@ -111,10 +112,8 @@ export default function ColorBarRangeSlider({ const total1 = original1 - numStepsOuter * delta; const total2 = original2 + numStepsOuter * delta; const step = (total2 - total1) / numSteps; - const values = [total1, original1, original2, total2]; - - const marks: Mark[] = getLabelsForValues(norm.scaleInv(values)).map( + const marks: Mark[] = getLabelsForValues(scaler.scaleInv(values)).map( (label, i) => { return { value: values[i], label }; }, @@ -127,7 +126,7 @@ export default function ColorBarRangeSlider({ value={currentMinMax} marks={marks} step={step} - valueLabelFormat={(v) => getLabelForValue(norm.scaleInv(v))} + valueLabelFormat={(v) => getLabelForValue(scaler.scaleInv(v))} onChange={handleMinMaxChange} onChangeCommitted={handleMinMaxChangeCommitted} valueLabelDisplay="auto" @@ -135,38 +134,3 @@ export default function ColorBarRangeSlider({ /> ); } - -// noinspection JSUnusedGlobalSymbols -class Norm { - readonly isLog: boolean; - - constructor(isLog: boolean) { - this.isLog = isLog; - } - - scale(x: number): number; - scale(x: [number, number]): [number, number]; - scale(x: number[]): number[]; - scale( - x: number | [number, number] | number[], - ): number | [number, number] | number[] { - if (!this.isLog) { - return x; - } - return typeof x === "number" ? Math.log10(x) : x.map((v) => Math.log10(v)); - } - - scaleInv(x: number): number; - scaleInv(x: [number, number]): [number, number]; - scaleInv(x: number[]): number[]; - scaleInv( - x: number | [number, number] | number[], - ): number | [number, number] | number[] { - if (!this.isLog) { - return x; - } - return typeof x === "number" - ? Math.pow(10, x) - : x.map((v) => Math.pow(10, v)); - } -} diff --git a/src/components/ColorBarLegend/scaler.ts b/src/components/ColorBarLegend/scaler.ts new file mode 100644 index 00000000..4cccb3d0 --- /dev/null +++ b/src/components/ColorBarLegend/scaler.ts @@ -0,0 +1,72 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019-2024 by the xcube development team and contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining x copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +const ident = (x: number) => x; +const pow10 = (x: number) => Math.pow(10, x); +const log10 = Math.log10; + +function scale( + x: number | [number, number] | number[], + fn: (x: number) => number, +): number | [number, number] | number[] { + return typeof x === "number" ? fn(x) : x.map(fn); +} + +// noinspection JSUnusedGlobalSymbols +/** + * A class representing a scaling operation. + * @class + */ +export default class Scale { + private readonly _fn: (x: number) => number; + private readonly _invFn: (x: number) => number; + + constructor(isLog: boolean) { + if (isLog) { + this._fn = log10; + this._invFn = pow10; + } else { + this._fn = ident; + this._invFn = ident; + } + } + + scale(x: number): number; + scale(x: [number, number]): [number, number]; + scale(x: number[]): number[]; + scale( + x: number | [number, number] | number[], + ): number | [number, number] | number[] { + return scale(x, this._fn); + } + + scaleInv(x: number): number; + scaleInv(x: [number, number]): [number, number]; + scaleInv(x: number[]): number[]; + scaleInv( + x: number | [number, number] | number[], + ): number | [number, number] | number[] { + return scale(x, this._invFn); + } +} From 9eaa3bf6432b154950fdce280651a46862726143 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 16 May 2024 11:14:43 +0200 Subject: [PATCH 25/26] Changing range in text boxes now updates slider correctly --- .../ColorBarLegend/ColorBarRangeSlider.tsx | 23 +++++--- src/components/ColorBarLegend/scaling.test.ts | 52 +++++++++++++++++++ .../ColorBarLegend/{scaler.ts => scaling.ts} | 47 ++++++++--------- 3 files changed, 89 insertions(+), 33 deletions(-) create mode 100644 src/components/ColorBarLegend/scaling.test.ts rename src/components/ColorBarLegend/{scaler.ts => scaling.ts} (63%) diff --git a/src/components/ColorBarLegend/ColorBarRangeSlider.tsx b/src/components/ColorBarLegend/ColorBarRangeSlider.tsx index fe6a73f7..78a2c5e5 100644 --- a/src/components/ColorBarLegend/ColorBarRangeSlider.tsx +++ b/src/components/ColorBarLegend/ColorBarRangeSlider.tsx @@ -22,13 +22,13 @@ * SOFTWARE. */ -import { useState, SyntheticEvent } from "react"; +import { useState, SyntheticEvent, useEffect, useMemo } from "react"; import Slider from "@mui/material/Slider"; import { Mark } from "@mui/base/useSlider"; import { getLabelsForValues, getLabelForValue } from "@/util/label"; import { ColorBarNorm } from "@/model/variable"; -import Scaler from "./scaler"; +import Scaling from "./scaling"; interface ColorBarRangeSliderProps { variableColorBarName: string; @@ -52,12 +52,19 @@ export default function ColorBarRangeSlider({ updateVariableColorBar, originalColorBarMinMax, }: ColorBarRangeSliderProps) { - const scaler = new Scaler(variableColorBarNorm === "log"); + const scaling = useMemo( + () => new Scaling(variableColorBarNorm === "log"), + [variableColorBarNorm], + ); const [currentMinMax, setCurrentMinMax] = useState<[number, number]>(() => - scaler.scale(variableColorBarMinMax), + scaling.scale(variableColorBarMinMax), ); + useEffect(() => { + setCurrentMinMax(scaling.scale(variableColorBarMinMax)); + }, [scaling, variableColorBarMinMax]); + const handleMinMaxChange = (_event: Event, value: number | number[]) => { if (Array.isArray(value)) { setCurrentMinMax(value as [number, number]); @@ -71,7 +78,7 @@ export default function ColorBarRangeSlider({ if (Array.isArray(value)) { // Here we convert to the precision of the displayed labels. // Otherwise, we'd get a lot of confusing digits if log-scaled. - const minMaxLabels = getLabelsForValues(scaler.scaleInv(value)); + const minMaxLabels = getLabelsForValues(scaling.scaleInv(value)); const minMaxValues = minMaxLabels.map((label) => Number.parseFloat(label), ); @@ -84,7 +91,7 @@ export default function ColorBarRangeSlider({ } }; - const [original1, original2] = scaler.scale(originalColorBarMinMax); + const [original1, original2] = scaling.scale(originalColorBarMinMax); const dist = original1 < original2 ? original2 - original1 : 1; const distExp = Math.floor(Math.log10(dist)); const distNorm = dist * Math.pow(10, -distExp); @@ -113,7 +120,7 @@ export default function ColorBarRangeSlider({ const total2 = original2 + numStepsOuter * delta; const step = (total2 - total1) / numSteps; const values = [total1, original1, original2, total2]; - const marks: Mark[] = getLabelsForValues(scaler.scaleInv(values)).map( + const marks: Mark[] = getLabelsForValues(scaling.scaleInv(values)).map( (label, i) => { return { value: values[i], label }; }, @@ -126,7 +133,7 @@ export default function ColorBarRangeSlider({ value={currentMinMax} marks={marks} step={step} - valueLabelFormat={(v) => getLabelForValue(scaler.scaleInv(v))} + valueLabelFormat={(v) => getLabelForValue(scaling.scaleInv(v))} onChange={handleMinMaxChange} onChangeCommitted={handleMinMaxChangeCommitted} valueLabelDisplay="auto" diff --git a/src/components/ColorBarLegend/scaling.test.ts b/src/components/ColorBarLegend/scaling.test.ts new file mode 100644 index 00000000..853c77e7 --- /dev/null +++ b/src/components/ColorBarLegend/scaling.test.ts @@ -0,0 +1,52 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019-2024 by the xcube development team and contributors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { expect, it, describe } from "vitest"; +import Scaling from "./scaling"; + +describe("Assert that Scaling", () => { + it("works in the log case", () => { + const scaling = new Scaling(true); + + expect(scaling.scale(0.01)).toEqual(-2); + expect(scaling.scaleInv(2)).toEqual(100); + + expect(scaling.scale([0.001, 0.01, 0.1, 1, 10])).toEqual([ + -3, -2, -1, 0, 1, + ]); + expect(scaling.scaleInv([-3, -2, -1, 0, 1])).toEqual([ + 0.001, 0.01, 0.1, 1, 10, + ]); + }); + + it("works in the identity case", () => { + const scaling = new Scaling(false); + + expect(scaling.scale(0.01)).toEqual(0.01); + expect(scaling.scaleInv(100)).toEqual(100); + + expect(scaling.scale([0.1, 0.2, 0.3])).toEqual([0.1, 0.2, 0.3]); + expect(scaling.scaleInv([0.1, 0.2, 0.3])).toEqual([0.1, 0.2, 0.3]); + }); +}); diff --git a/src/components/ColorBarLegend/scaler.ts b/src/components/ColorBarLegend/scaling.ts similarity index 63% rename from src/components/ColorBarLegend/scaler.ts rename to src/components/ColorBarLegend/scaling.ts index 4cccb3d0..f0f04394 100644 --- a/src/components/ColorBarLegend/scaler.ts +++ b/src/components/ColorBarLegend/scaling.ts @@ -22,25 +22,26 @@ * SOFTWARE. */ -const ident = (x: number) => x; -const pow10 = (x: number) => Math.pow(10, x); +type Value = number; +type ValueRange = [number, number]; +type Values = number[]; +type Fn = (x: Value) => Value; + +const ident = (x: Value) => x; +const pow10 = (x: Value) => Math.pow(10, x); const log10 = Math.log10; -function scale( - x: number | [number, number] | number[], - fn: (x: number) => number, -): number | [number, number] | number[] { - return typeof x === "number" ? fn(x) : x.map(fn); -} +const applyFn = (x: Value | ValueRange | Values, fn: Fn) => + typeof x === "number" ? fn(x) : x.map(fn); // noinspection JSUnusedGlobalSymbols /** * A class representing a scaling operation. * @class */ -export default class Scale { - private readonly _fn: (x: number) => number; - private readonly _invFn: (x: number) => number; +export default class Scaling { + private readonly _fn: Fn; + private readonly _invFn: Fn; constructor(isLog: boolean) { if (isLog) { @@ -52,21 +53,17 @@ export default class Scale { } } - scale(x: number): number; - scale(x: [number, number]): [number, number]; - scale(x: number[]): number[]; - scale( - x: number | [number, number] | number[], - ): number | [number, number] | number[] { - return scale(x, this._fn); + scale(x: Value): Value; + scale(x: ValueRange): ValueRange; + scale(x: Values): Values; + scale(x: Value | ValueRange | Values) { + return applyFn(x, this._fn); } - scaleInv(x: number): number; - scaleInv(x: [number, number]): [number, number]; - scaleInv(x: number[]): number[]; - scaleInv( - x: number | [number, number] | number[], - ): number | [number, number] | number[] { - return scale(x, this._invFn); + scaleInv(x: Value): Value; + scaleInv(x: ValueRange): ValueRange; + scaleInv(x: Values): Values; + scaleInv(x: Value | ValueRange | Values) { + return applyFn(x, this._invFn); } } From 5b21335923e5ddeb73affb21736e6eaea8f001d2 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 16 May 2024 11:21:01 +0200 Subject: [PATCH 26/26] increased dev version --- package.json | 2 +- src/version.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c9d14bc4..afd1f9e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xcube-viewer", - "version": "1.2.0-dev.0", + "version": "1.2.0-dev.1", "private": true, "type": "module", "scripts": { diff --git a/src/version.ts b/src/version.ts index cc18c148..37874b33 100644 --- a/src/version.ts +++ b/src/version.ts @@ -22,6 +22,6 @@ * SOFTWARE. */ -const version = "1.2.0-dev.0"; +const version = "1.2.0-dev.1"; export default version;