From 9508477e1946644087cd79716c09bc197d295065 Mon Sep 17 00:00:00 2001 From: Peter Todd Date: Wed, 8 Apr 2026 16:28:01 +0100 Subject: [PATCH] Fix packed multitext chart coloring --- src/datastore/DataStore.js | 38 +++++++++++-- src/tests/datastore-color.multitext.spec.ts | 62 +++++++++++++++++++++ 2 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 src/tests/datastore-color.multitext.spec.ts diff --git a/src/datastore/DataStore.js b/src/datastore/DataStore.js index e3b7b0d08..ae44e0134 100644 --- a/src/datastore/DataStore.js +++ b/src/datastore/DataStore.js @@ -18,6 +18,7 @@ import { getMultitextCapacity, getMultitextDelimiter, getMultitextJoinDelimiter, + inferMultitextEmptyItem, } from "@/lib/multitext"; import { DataSourceSchema } from "../charts/schemas/DataSourceSchema"; import { logValidationError } from "@/lib/validationLogging"; @@ -1494,13 +1495,40 @@ class DataStore { return !Number.isFinite(v) || (ov.fallbackOnZero && v === 0); } //simply return the color associated with the value - if ( - c.datatype === "text" || - c.datatype === "text16" || - c.datatype === "multitext" - ) { + if (c.datatype === "text" || c.datatype === "text16") { return (x) => colors[data[x]]; } + if (c.datatype === "multitext") { + const capacity = getMultitextCapacity(c); + if (capacity <= 1) { + return (x) => colors[data[x]]; + } + + const emptyItem = inferMultitextEmptyItem(c); + const emptyIndex = emptyItem == null ? -1 : c.values.indexOf(emptyItem); + + return (rowIndex) => { + const start = rowIndex * capacity; + let fallbackColor; + + for (let offset = 0; offset < capacity; offset++) { + const valueIndex = data[start + offset]; + if (valueIndex == null || valueIndex === 65535) { + break; + } + if (valueIndex === emptyIndex) { + fallbackColor = colors[valueIndex]; + continue; + } + // Packed multitext rows can contain multiple values. + // Until charts support multi-value categorical coloring directly, + // use the first semantic row item deterministically. + return colors[valueIndex]; + } + + return fallbackColor; + }; + } if ( c.datatype === "integer" || c.datatype === "double" || diff --git a/src/tests/datastore-color.multitext.spec.ts b/src/tests/datastore-color.multitext.spec.ts new file mode 100644 index 000000000..f8ed77b5c --- /dev/null +++ b/src/tests/datastore-color.multitext.spec.ts @@ -0,0 +1,62 @@ +import DataStore from "@/datastore/DataStore"; +import { describe, expect, test } from "vitest"; + +describe("DataStore.getColorFunction", () => { + test("keeps legacy single-slot multitext coloring", () => { + const column = { + field: "__tags__", + name: "tags", + datatype: "multitext" as const, + delimiter: ";", + stringLength: 1, + values: ["a", "b", "a; b"], + data: new Uint16Array([0, 1, 2]), + }; + + const store = { + columnIndex: { __tags__: column }, + getColumnColors: () => ["#ff0000", "#00ff00", "#0000ff"], + }; + + const colorFn = DataStore.prototype.getColorFunction.call( + store, + "__tags__", + {}, + ); + + expect(colorFn(0)).toBe("#ff0000"); + expect(colorFn(1)).toBe("#00ff00"); + expect(colorFn(2)).toBe("#0000ff"); + }); + + test("reads packed multitext colors using row stride", () => { + const column = { + field: "__gates__", + name: "gates", + datatype: "multitext" as const, + delimiter: ",", + stringLength: 3, + values: ["N/A", "a", "b"], + data: new Uint16Array([ + 0, 65535, 65535, + 2, 65535, 65535, + 1, 2, 65535, + ]), + }; + + const store = { + columnIndex: { __gates__: column }, + getColumnColors: () => ["#999999", "#ff0000", "#00ff00"], + }; + + const colorFn = DataStore.prototype.getColorFunction.call( + store, + "__gates__", + {}, + ); + + expect(colorFn(0)).toBe("#999999"); + expect(colorFn(1)).toBe("#00ff00"); + expect(colorFn(2)).toBe("#ff0000"); + }); +});