Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions web/libs/editor/src/tags/object/TimeSeries.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1125,17 +1125,23 @@ const Overview = observer(({ item, data, series }) => {
const { margin, keyColumn: idX } = item;
const width = Math.max(fullWidth - margin.left - margin.right, 0);
// const data = store.task.dataObj;
let keys = Object.keys(item.channelsMap);

if (item.overviewchannels) {
const channels = item.overviewchannels
.toLowerCase()
.split(",")
.map((name) => (/^\d+$/.test(name) ? item.headers[name] : name))
.filter((ch) => keys.includes(ch));

if (channels.length) keys = channels;
}
const keys = React.useMemo(() => {
const allKeys = Object.keys(item.channelsMap);

if (item.overviewchannels) {
const channels = item.overviewchannels
.toLowerCase()
.split(",")
.map((name) => {
const trimmed = name.trim();
return /^\d+$/.test(trimmed) && item.headers ? item.headers[Number(trimmed)]?.toLowerCase() : trimmed;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is item.headers always guaranteed to be an array?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming the fix here is more to do with the normalization of the keys that were likely not matching up in the repro cases?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hlomzik you should know better. i'm really new to timeseries

})
.filter((ch) => ch && allKeys.includes(ch));

if (channels.length) return channels;
}
return allKeys;
}, [item.overviewchannels, item.channelsMap, item.headers]);
// const series = data[idX];
const minRegionWidth = 2;

Expand Down Expand Up @@ -1353,13 +1359,13 @@ const Overview = observer(({ item, data, series }) => {
.attr("viewBox", [0, 0, width + margin.left + margin.right, focusHeight + margin.bottom]);

gChannels.current.selectAll("path").remove();
for (const key of Object.keys(item.channelsMap)) drawPath(key);
for (const key of keys) drawPath(key);

drawAxis();
// gb.current.selectAll("*").remove();
gb.current.call(brush).call(brush.move, item.brushRange.map(x));
}
}, [width, node]);
}, [width, node, keys]);

// redraw overview on zoom
React.useEffect(() => {
Expand Down
303 changes: 303 additions & 0 deletions web/libs/editor/src/tags/object/__tests__/TimeSeries.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { types } from "mobx-state-tree";
import { mockFF } from "../../../../__mocks__/global";
import { FF_TIMESERIES_SYNC } from "../../../utils/feature-flags";
import { TimeSeriesModel } from "../TimeSeries";
// Import Channel to ensure Registry is initialized
import "../TimeSeries/Channel";

const ff = mockFF();

Expand Down Expand Up @@ -501,4 +503,305 @@ describe("TimeSeries playback", () => {
});
});

describe("TimeSeries overviewChannels filtering", () => {
// Helper function to replicate the filtering logic from Overview component
const filterOverviewChannels = (overviewchannels, channelsMap, headers) => {
const allKeys = Object.keys(channelsMap);

if (overviewchannels) {
const channels = overviewchannels
.toLowerCase()
.split(",")
.map((name) => {
const trimmed = name.trim();
return /^\d+$/.test(trimmed) && headers ? headers[Number(trimmed)]?.toLowerCase() : trimmed;
})
.filter((ch) => ch && allKeys.includes(ch));

if (channels.length) return channels;
}
return allKeys;
};

it("should return all channels when overviewChannels is not set", () => {
const model = TimeSeriesModel.create(
{
name: "timeseries",
value: "$timeseries",
valuetype: "json",
timecolumn: "time",
overviewchannels: "",
children: [
{ type: "channel", column: "channel1" },
{ type: "channel", column: "channel2" },
{ type: "channel", column: "channel3" },
],
},
mockEnv,
);

const store = MockStore.create({ timeseries: model }, mockEnv);
model.setColumnNames(["time", "channel1", "channel2", "channel3"]);

const channelsMap = model.channelsMap;
const filtered = filterOverviewChannels(model.overviewchannels, channelsMap, model.headers);

expect(filtered).toEqual(["channel1", "channel2", "channel3"]);
expect(filtered.length).toBe(3);
});

it("should filter channels by name when overviewChannels is set", () => {
const model = TimeSeriesModel.create(
{
name: "timeseries",
value: "$timeseries",
valuetype: "json",
timecolumn: "time",
overviewchannels: "channel1,channel3",
children: [
{ type: "channel", column: "channel1" },
{ type: "channel", column: "channel2" },
{ type: "channel", column: "channel3" },
],
},
mockEnv,
);

const store = MockStore.create({ timeseries: model }, mockEnv);
model.setColumnNames(["time", "channel1", "channel2", "channel3"]);

const channelsMap = model.channelsMap;
const filtered = filterOverviewChannels(model.overviewchannels, channelsMap, model.headers);

expect(filtered).toEqual(["channel1", "channel3"]);
expect(filtered.length).toBe(2);
expect(filtered).not.toContain("channel2");
});

it("should filter channels by numeric index when overviewChannels uses indices", () => {
const model = TimeSeriesModel.create(
{
name: "timeseries",
value: "$timeseries",
valuetype: "json",
timecolumn: "time",
overviewchannels: "1,3",
children: [
{ type: "channel", column: "velocity" },
{ type: "channel", column: "acceleration" },
{ type: "channel", column: "temperature" },
],
},
mockEnv,
);

const store = MockStore.create({ timeseries: model }, mockEnv);
// Headers array: [0: "time", 1: "velocity", 2: "acceleration", 3: "temperature"]
model.setColumnNames(["time", "velocity", "acceleration", "temperature"]);

const channelsMap = model.channelsMap;
const filtered = filterOverviewChannels(model.overviewchannels, channelsMap, model.headers);

// Should map index 1 -> "velocity", index 3 -> "temperature"
expect(filtered).toEqual(["velocity", "temperature"]);
expect(filtered.length).toBe(2);
expect(filtered).not.toContain("acceleration");
});

it("should handle case-insensitive channel names", () => {
const model = TimeSeriesModel.create(
{
name: "timeseries",
value: "$timeseries",
valuetype: "json",
timecolumn: "time",
overviewchannels: "VELOCITY,Acceleration",
children: [
{ type: "channel", column: "velocity" },
{ type: "channel", column: "acceleration" },
{ type: "channel", column: "temperature" },
],
},
mockEnv,
);

const store = MockStore.create({ timeseries: model }, mockEnv);
model.setColumnNames(["time", "velocity", "acceleration", "temperature"]);

const channelsMap = model.channelsMap;
const filtered = filterOverviewChannels(model.overviewchannels, channelsMap, model.headers);

expect(filtered).toEqual(["velocity", "acceleration"]);
expect(filtered.length).toBe(2);
});

it("should handle whitespace in overviewChannels", () => {
const model = TimeSeriesModel.create(
{
name: "timeseries",
value: "$timeseries",
valuetype: "json",
timecolumn: "time",
overviewchannels: " channel1 , channel2 ",
children: [
{ type: "channel", column: "channel1" },
{ type: "channel", column: "channel2" },
{ type: "channel", column: "channel3" },
],
},
mockEnv,
);

const store = MockStore.create({ timeseries: model }, mockEnv);
model.setColumnNames(["time", "channel1", "channel2", "channel3"]);

const channelsMap = model.channelsMap;
const filtered = filterOverviewChannels(model.overviewchannels, channelsMap, model.headers);

expect(filtered).toEqual(["channel1", "channel2"]);
expect(filtered.length).toBe(2);
});

it("should filter out invalid channel names", () => {
const model = TimeSeriesModel.create(
{
name: "timeseries",
value: "$timeseries",
valuetype: "json",
timecolumn: "time",
overviewchannels: "channel1,invalidChannel,channel2",
children: [
{ type: "channel", column: "channel1" },
{ type: "channel", column: "channel2" },
{ type: "channel", column: "channel3" },
],
},
mockEnv,
);

const store = MockStore.create({ timeseries: model }, mockEnv);
model.setColumnNames(["time", "channel1", "channel2", "channel3"]);

const channelsMap = model.channelsMap;
const filtered = filterOverviewChannels(model.overviewchannels, channelsMap, model.headers);

// Should only include valid channels
expect(filtered).toEqual(["channel1", "channel2"]);
expect(filtered.length).toBe(2);
expect(filtered).not.toContain("invalidChannel");
expect(filtered).not.toContain("channel3");
});

it("should return all channels when all specified channels are invalid", () => {
const model = TimeSeriesModel.create(
{
name: "timeseries",
value: "$timeseries",
valuetype: "json",
timecolumn: "time",
overviewchannels: "invalid1,invalid2",
children: [
{ type: "channel", column: "channel1" },
{ type: "channel", column: "channel2" },
],
},
mockEnv,
);

const store = MockStore.create({ timeseries: model }, mockEnv);
model.setColumnNames(["time", "channel1", "channel2"]);

const channelsMap = model.channelsMap;
const filtered = filterOverviewChannels(model.overviewchannels, channelsMap, model.headers);

// When all channels are invalid, should return all channels (fallback behavior)
expect(filtered).toEqual(["channel1", "channel2"]);
});

it("should handle single channel in overviewChannels", () => {
const model = TimeSeriesModel.create(
{
name: "timeseries",
value: "$timeseries",
valuetype: "json",
timecolumn: "time",
overviewchannels: "channel2",
children: [
{ type: "channel", column: "channel1" },
{ type: "channel", column: "channel2" },
{ type: "channel", column: "channel3" },
],
},
mockEnv,
);

const store = MockStore.create({ timeseries: model }, mockEnv);
model.setColumnNames(["time", "channel1", "channel2", "channel3"]);

const channelsMap = model.channelsMap;
const filtered = filterOverviewChannels(model.overviewchannels, channelsMap, model.headers);

expect(filtered).toEqual(["channel2"]);
expect(filtered.length).toBe(1);
});

it("should handle mixed numeric indices and channel names", () => {
const model = TimeSeriesModel.create(
{
name: "timeseries",
value: "$timeseries",
valuetype: "json",
timecolumn: "time",
overviewchannels: "1,channel3",
children: [
{ type: "channel", column: "velocity" },
{ type: "channel", column: "acceleration" },
{ type: "channel", column: "channel3" },
],
},
mockEnv,
);

const store = MockStore.create({ timeseries: model }, mockEnv);
// Headers: [0: "time", 1: "velocity", 2: "acceleration", 3: "channel3"]
model.setColumnNames(["time", "velocity", "acceleration", "channel3"]);

const channelsMap = model.channelsMap;
const filtered = filterOverviewChannels(model.overviewchannels, channelsMap, model.headers);

// Index 1 maps to "velocity", "channel3" is used directly
expect(filtered).toEqual(["velocity", "channel3"]);
expect(filtered.length).toBe(2);
});

it("should handle headless CSV with numeric column indices", () => {
const model = TimeSeriesModel.create(
{
name: "timeseries",
value: "$timeseries",
valuetype: "json",
timecolumn: "0",
overviewchannels: "1,2",
children: [
{ type: "channel", column: "1" },
{ type: "channel", column: "2" },
{ type: "channel", column: "3" },
],
},
mockEnv,
);

const store = MockStore.create({ timeseries: model }, mockEnv);
// Headless CSV: headers are numeric strings ["0", "1", "2", "3"]
model.setColumnNames(["0", "1", "2", "3"]);

const channelsMap = model.channelsMap;
const filtered = filterOverviewChannels(model.overviewchannels, channelsMap, model.headers);

// For headless CSV, numeric indices map to header names
expect(filtered).toEqual(["1", "2"]);
expect(filtered.length).toBe(2);
});
});

ff.reset();
Loading
Loading