diff --git a/packages/safe-ds-eda/src/apis/historyApi.ts b/packages/safe-ds-eda/src/apis/historyApi.ts index 2a8e1ee51..2b659d083 100644 --- a/packages/safe-ds-eda/src/apis/historyApi.ts +++ b/packages/safe-ds-eda/src/apis/historyApi.ts @@ -1,16 +1,18 @@ import { get } from 'svelte/store'; import type { FromExtensionMessage, RunnerExecutionResultMessage } from '../../types/messaging'; import type { + CategoricalFilter, EmptyTab, ExternalHistoryEntry, HistoryEntry, InteralEmptyTabHistoryEntry, InternalHistoryEntry, + NumericalFilter, RealTab, Tab, TabHistoryEntry, } from '../../types/state'; -import { cancelTabIdsWaiting, tabs, history, currentTabIndex, table } from '../webviewState'; +import { cancelTabIdsWaiting, tabs, history, currentTabIndex, table, tableLoading } from '../webviewState'; import { executeRunner } from './extensionApi'; // Wait for results to return from the server @@ -22,6 +24,34 @@ export const getAndIncrementEntryId = function (): number { return entryIdCounter++; }; +const generateOverrideId = function (entry: ExternalHistoryEntry | InternalHistoryEntry): string { + switch (entry.action) { + case 'hideColumn': + case 'showColumn': + case 'resizeColumn': + case 'reorderColumns': + case 'highlightColumn': + return entry.columnName + '.' + entry.action; + case 'sortByColumn': + return entry.action; // Thus enforcing override sort + case 'voidSortByColumn': + return 'sortByColumn'; // This overriding previous sorts + case 'filterColumn': + return entry.columnName + entry.filter.type + '.' + entry.action; + case 'linePlot': + case 'scatterPlot': + case 'histogram': + case 'boxPlot': + case 'infoPanel': + case 'heatmap': + case 'emptyTab': + const tabId = entry.newTabId ?? entry.existingTabId; + return entry.type + '.' + tabId; + default: + throw new Error('Unknown action type to generateOverrideId'); + } +}; + window.addEventListener('message', (event) => { const message = event.data as FromExtensionMessage; @@ -40,6 +70,11 @@ window.addEventListener('message', (event) => { deployResult(message, asyncQueue[0]); asyncQueue.shift(); + + if (asyncQueue.length === 0) { + tableLoading.set(false); + } + evaluateMessagesWaitingForTurn(); } else if (message.command === 'cancelRunnerExecution') { cancelExecuteExternalHistoryEntry(message.value); @@ -51,6 +86,7 @@ export const addInternalToHistory = function (entry: InternalHistoryEntry): void const entryWithId: HistoryEntry = { ...entry, id: getAndIncrementEntryId(), + overrideId: generateOverrideId(entry), }; const newHistory = [...state, entryWithId]; return newHistory; @@ -60,10 +96,18 @@ export const addInternalToHistory = function (entry: InternalHistoryEntry): void }; export const executeExternalHistoryEntry = function (entry: ExternalHistoryEntry): void { + // Set table to loading if loading takes longer than 500ms + setTimeout(() => { + if (asyncQueue.length > 0) { + tableLoading.set(true); + } + }, 500); + history.update((state) => { const entryWithId: HistoryEntry = { ...entry, id: getAndIncrementEntryId(), + overrideId: generateOverrideId(entry), }; const newHistory = [...state, entryWithId]; @@ -91,7 +135,7 @@ export const addAndDeployTabHistoryEntry = function (entry: TabHistoryEntry & { } history.update((state) => { - return [...state, entry]; + return [...state, { ...entry, overrideId: generateOverrideId(entry) }]; }); tabs.update((state) => { const newTabs = (state ?? []).concat(tab); @@ -101,20 +145,22 @@ export const addAndDeployTabHistoryEntry = function (entry: TabHistoryEntry & { }; export const addEmptyTabHistoryEntry = function (): void { + const tabId = crypto.randomUUID(); const entry: InteralEmptyTabHistoryEntry & { id: number } = { action: 'emptyTab', type: 'internal', alias: 'New empty tab', id: getAndIncrementEntryId(), + newTabId: tabId, }; const tab: EmptyTab = { type: 'empty', - id: crypto.randomUUID(), + id: tabId, isInGeneration: true, }; history.update((state) => { - return [...state, entry]; + return [...state, { ...entry, overrideId: generateOverrideId(entry) }]; }); tabs.update((state) => { const newTabs = (state ?? []).concat(tab); @@ -136,6 +182,10 @@ export const cancelExecuteExternalHistoryEntry = function (entry: HistoryEntry): unsetTabAsGenerating(tab); } } + + if (asyncQueue.length === 0) { + tableLoading.set(false); + } } else { throw new Error('Entry already fully executed'); } @@ -181,13 +231,14 @@ export const unsetTabAsGenerating = function (tab: RealTab): void { const deployResult = function (result: RunnerExecutionResultMessage, historyEntry: ExternalHistoryEntry) { const resultContent = result.value; if (resultContent.type === 'tab') { - if (resultContent.content.id) { - const existingTab = get(tabs).find((et) => et.id === resultContent.content.id); + if (historyEntry.type !== 'external-visualizing') throw new Error('Deploying tab from non-visualizing entry'); + if (historyEntry.existingTabId) { + const existingTab = get(tabs).find((et) => et.id === historyEntry.existingTabId); if (existingTab) { const tabIndex = get(tabs).indexOf(existingTab); tabs.update((state) => state.map((t) => { - if (t.id === resultContent.content.id) { + if (t.id === historyEntry.existingTabId) { return resultContent.content; } else { return t; @@ -197,14 +248,44 @@ const deployResult = function (result: RunnerExecutionResultMessage, historyEntr currentTabIndex.set(tabIndex); return; } + } else { + const tab = resultContent.content; + tab.id = historyEntry.newTabId!; // Must exist if not existingTabId, not sure why ts does not pick up on it itself here + tabs.update((state) => state.concat(tab)); + currentTabIndex.set(get(tabs).indexOf(tab)); } - const tab = resultContent.content; - tab.id = crypto.randomUUID(); - tabs.update((state) => state.concat(tab)); - currentTabIndex.set(get(tabs).indexOf(tab)); } else if (resultContent.type === 'table') { + table.update((state) => { + for (const column of resultContent.content.columns) { + const existingColumn = state?.columns.find((c) => c.name === column.name); + if (!existingColumn) throw new Error('New Column not found in current table!'); + + column.profiling = existingColumn.profiling; // Preserve profiling, after this if it was a type that invalidated profiling, it will be invalidated + column.hidden = existingColumn.hidden; + column.highlighted = existingColumn.highlighted; + if (historyEntry.action === 'sortByColumn' && column.name === historyEntry.columnName) { + column.appliedSort = historyEntry.sort; // Set sorted column to sorted if it was a sort action, otherwise if also not a void sort preserve + } else if (historyEntry.action !== 'sortByColumn' && historyEntry.action !== 'voidSortByColumn') { + column.appliedSort = existingColumn.appliedSort; + } + if (historyEntry.action === 'filterColumn' && column.name === historyEntry.columnName) { + if (existingColumn.type === 'numerical') { + column.appliedFilters = existingColumn.appliedFilters.concat([ + historyEntry.filter as NumericalFilter, + ]); // Set filtered column to filtered if it was a filter action, otherwise preserve + } else if (existingColumn.type === 'categorical') { + column.appliedFilters = existingColumn.appliedFilters.concat([ + historyEntry.filter as CategoricalFilter, + ]); // Set filtered column to filtered if it was a filter action, otherwise preserve + } + } else if (historyEntry.action !== 'filterColumn') { + column.appliedFilters = existingColumn.appliedFilters; + } + } + return resultContent.content; + }); + updateTabOutdated(historyEntry); - throw new Error('Not implemented'); } }; diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index 86c156159..ac217c889 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -1,7 +1,7 @@ - + + + diff --git a/packages/safe-ds-eda/src/icons/Filter.svelte b/packages/safe-ds-eda/src/icons/Filter.svelte index 6556842d7..8f1208c26 100644 --- a/packages/safe-ds-eda/src/icons/Filter.svelte +++ b/packages/safe-ds-eda/src/icons/Filter.svelte @@ -1,5 +1,5 @@ (); const history = writable([]); +const tableLoading = writable(false); + window.addEventListener('message', (event) => { const message = event.data as FromExtensionMessage; // eslint-disable-next-line no-console @@ -50,4 +52,4 @@ window.addEventListener('message', (event) => { } }); -export { history, tabs, table, currentTabIndex, preventClicks, cancelTabIdsWaiting }; +export { history, tabs, table, currentTabIndex, preventClicks, cancelTabIdsWaiting, tableLoading }; diff --git a/packages/safe-ds-eda/types/state.ts b/packages/safe-ds-eda/types/state.ts index 79f50005a..c87689821 100644 --- a/packages/safe-ds-eda/types/state.ts +++ b/packages/safe-ds-eda/types/state.ts @@ -1,6 +1,6 @@ type InternalAction = 'reorderColumns' | 'resizeColumn' | 'hideColumn' | 'showColumn' | 'highlightColumn' | 'emptyTab'; -type ExternalManipulatingAction = 'filterColumn' | 'sortColumn' | TableFilterTypes; -type ExternalVisualizingAction = TabType | 'refreshTab'; +type ExternalManipulatingAction = 'filterColumn' | 'sortByColumn' | 'voidSortByColumn' | TableFilterTypes; +type ExternalVisualizingAction = TabType; type Action = InternalAction | ExternalManipulatingAction | ExternalVisualizingAction; interface HistoryEntryBase { @@ -19,13 +19,26 @@ interface ExternalManipulatingHistoryEntryBase extends HistoryEntryBase { action: ExternalManipulatingAction; } -interface ExternalVisualizingHistoryEntryBase extends HistoryEntryBase { +interface ExternalVisualizingHistoryEntryBaseExisting extends HistoryEntryBase { type: 'external-visualizing'; action: ExternalVisualizingAction; columnNumber: 'one' | 'two' | 'none'; - existingTabId?: string; + existingTabId: string; + newTabId?: never; } +interface ExternalVisualizingHistoryEntryBaseNew extends HistoryEntryBase { + type: 'external-visualizing'; + action: ExternalVisualizingAction; + columnNumber: 'one' | 'two' | 'none'; + newTabId: string; + existingTabId?: never; +} + +type ExternalVisualizingHistoryEntryBase = + | ExternalVisualizingHistoryEntryBaseExisting + | ExternalVisualizingHistoryEntryBaseNew; + export interface InternalColumnWithValueHistoryEntry extends InternalHistoryEntryBase { action: 'reorderColumns' | 'resizeColumn'; columnName: string; @@ -39,6 +52,7 @@ export interface InternalColumnHistoryEntry extends InternalHistoryEntryBase { export interface InteralEmptyTabHistoryEntry extends InternalHistoryEntryBase { action: 'emptyTab'; + newTabId: string; } export interface ExternalManipulatingColumnFilterHistoryEntry extends ExternalManipulatingHistoryEntryBase { @@ -52,32 +66,33 @@ export interface ExternalManipulatingTableFilterHistoryEntry extends ExternalMan } export interface ExternalManipulatingColumnSortHistoryEntry extends ExternalManipulatingHistoryEntryBase { - action: 'sortColumn'; + action: 'sortByColumn'; columnName: string; sort: PossibleSorts; } -export interface ExternalVisualizingNoColumnHistoryEntry extends ExternalVisualizingHistoryEntryBase { +export interface ExternalManipulatingColumnSortVoidHistoryEntry extends ExternalManipulatingHistoryEntryBase { + action: 'voidSortByColumn'; + columnName: string; +} + +export type ExternalVisualizingNoColumnHistoryEntry = { action: NoColumnTabTypes; columnNumber: 'none'; -} +} & ExternalVisualizingHistoryEntryBase; -export interface ExternalVisualizingOneColumnHistoryEntry extends ExternalVisualizingHistoryEntryBase { +export type ExternalVisualizingOneColumnHistoryEntry = { action: OneColumnTabTypes; columnName: string; columnNumber: 'one'; -} +} & ExternalVisualizingHistoryEntryBase; -export interface ExternalVisualizingTwoColumnHistoryEntry extends ExternalVisualizingHistoryEntryBase { +export type ExternalVisualizingTwoColumnHistoryEntry = { action: TwoColumnTabTypes; xAxisColumnName: string; yAxisColumnName: string; columnNumber: 'two'; -} - -export interface ExternalVisualizingRefreshHistoryEntry extends ExternalVisualizingHistoryEntryBase { - action: 'refreshTab'; -} +} & ExternalVisualizingHistoryEntryBase; export type TabHistoryEntry = | ExternalVisualizingNoColumnHistoryEntry @@ -90,17 +105,18 @@ export type InternalHistoryEntry = export type ExternalManipulatingHistoryEntry = | ExternalManipulatingColumnFilterHistoryEntry | ExternalManipulatingTableFilterHistoryEntry - | ExternalManipulatingColumnSortHistoryEntry; + | ExternalManipulatingColumnSortHistoryEntry + | ExternalManipulatingColumnSortVoidHistoryEntry; export type ExternalVisualizingHistoryEntry = | ExternalVisualizingNoColumnHistoryEntry | ExternalVisualizingOneColumnHistoryEntry - | ExternalVisualizingTwoColumnHistoryEntry - | ExternalVisualizingRefreshHistoryEntry; + | ExternalVisualizingTwoColumnHistoryEntry; export type ExternalHistoryEntry = ExternalManipulatingHistoryEntry | ExternalVisualizingHistoryEntry; export type HistoryEntry = (InternalHistoryEntry | ExternalHistoryEntry) & { id: number; + overrideId: string; }; // ------------------ Types for the Tabs ------------------ @@ -110,7 +126,7 @@ export type NoColumnTabTypes = 'heatmap'; type TabType = TwoColumnTabTypes | OneColumnTabTypes | NoColumnTabTypes; interface TabObject { - id?: string; + id: string; type: TabType; tabComment: string; content: Object; @@ -228,7 +244,7 @@ export interface ProfilingDetailName extends ProfilingDetailBase { export type ProfilingDetail = ProfilingDetailStatistical | ProfilingDetailImage | ProfilingDetailName; // ------------ Types for the Columns ----------- -type PossibleSorts = 'asc' | 'desc' | null; +export type PossibleSorts = 'asc' | 'desc'; interface ColumnBase { type: 'numerical' | 'categorical'; @@ -236,7 +252,7 @@ interface ColumnBase { values: any; hidden: boolean; highlighted: boolean; - appliedSort: PossibleSorts; + appliedSort: PossibleSorts | null; profiling?: Profiling; } @@ -260,7 +276,6 @@ interface FilterBase { interface ColumnFilterBase extends FilterBase { type: 'valueRange' | 'specificValue' | 'searchString'; - columnName: string; } export interface PossibleSearchStringFilter extends ColumnFilterBase { diff --git a/packages/safe-ds-vscode/media/styles.css b/packages/safe-ds-vscode/media/styles.css index f0455764e..1a890aca3 100644 --- a/packages/safe-ds-vscode/media/styles.css +++ b/packages/safe-ds-vscode/media/styles.css @@ -9,7 +9,8 @@ --medium-color: #ccc; --dark-color: #6d6d6d; --darkest-color: #292929; - --transparent: #ffffffa1; + --transparent-light: #ffffffd9; + --transparent-medium: #ffffffa1; } .noSelect { diff --git a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts index 741126262..dfbfc94d6 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -3,6 +3,7 @@ import { Column, ExternalHistoryEntry, HistoryEntry, + PossibleSorts, Profiling, ProfilingDetailStatistical, Table, @@ -119,6 +120,45 @@ export class RunnerApi { } //#endregion + //#region Helpers + private runnerResultToTable(tableName: string, runnerResult: any, columnIsNumeric: Map): Table { + const table: Table = { + totalRows: 0, + name: tableName, + columns: [] as Table['columns'], + appliedFilters: [] as Table['appliedFilters'], + }; + + let currentMax = 0; + for (const [columnName, columnValues] of Object.entries(runnerResult)) { + if (!Array.isArray(columnValues)) { + continue; + } + if (currentMax < columnValues.length) { + currentMax = columnValues.length; + } + + const columnType = columnIsNumeric.get(columnName) ? 'numerical' : 'categorical'; + + const column: Column = { + name: columnName, + values: columnValues, + type: columnType, + hidden: false, + highlighted: false, + appliedFilters: [], + appliedSort: null, + coloredHighLow: false, + }; + table.columns.push(column); + } + table.totalRows = currentMax; + table.visibleRows = currentMax; + + return table; + } + //#endregion + //#region SDS code generation private sdsStringForHistoryEntry( historyEntry: ExternalHistoryEntry, @@ -175,6 +215,22 @@ export class RunnerApi { ), placeholderNames: [newPlaceholderName], }; + case 'sortByColumn': + return { + sdsString: this.sdsStringForSortRowsByColumn( + historyEntry.columnName, + historyEntry.sort, + overrideTablePlaceholder ?? this.tablePlaceholder, + newPlaceholderName, + ), + placeholderNames: [newPlaceholderName], + }; + case 'voidSortByColumn': + // This is a void action, no SDS code is generated and new placeholder is just previous one + return { + sdsString: '', + placeholderNames: [overrideTablePlaceholder ?? this.tablePlaceholder], + }; default: throw new Error('Unknown history entry action: ' + historyEntry.action); } @@ -276,10 +332,34 @@ export class RunnerApi { ); } + private sdsStringForSortRowsByColumn( + columnName: string, + direction: PossibleSorts, + tablePlaceholder: string, + newPlaceholderName: string, + ) { + if (!direction) throw new Error('Null direction not implemented!'); + return ( + 'val ' + + newPlaceholderName + + ' = ' + + tablePlaceholder + + '.sortRowsByColumn("' + + columnName + + '" , ' + + (direction === 'desc') + + '); \n' + ); + } + private sdsStringForRemoveColumns(columnNames: string[], tablePlaceholder: string, newPlaceholderName: string) { const quotedColumns = columnNames.map((name) => `"${name}"`).join(','); return 'val ' + newPlaceholderName + ' = ' + tablePlaceholder + `.removeColumns([${quotedColumns}]); \n`; } + + private sdsStringForTableSchema(tablePlaceholder: string, newPlaceholderName: string) { + return 'val ' + newPlaceholderName + ' = ' + tablePlaceholder + '.^schema; \n'; + } //#endregion //#region Placeholder handling @@ -340,46 +420,13 @@ export class RunnerApi { } await this.addToAndExecutePipeline(pipelineExecutionId, sdsLines, placeholderNames); - const columnIsNumeric = new Map(); + const columnIsNumeric = new Map(); for (const [columnName, placeholderName] of columnNameToPlaceholderIsNumericNameMap) { const columnType = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); - columnIsNumeric.set(columnName, columnType as string); + columnIsNumeric.set(columnName, columnType as boolean); } - const table: Table = { - totalRows: 0, - name: tableName, - columns: [] as Table['columns'], - appliedFilters: [] as Table['appliedFilters'], - }; - - let currentMax = 0; - for (const [columnName, columnValues] of Object.entries(pythonTableColumns)) { - if (!Array.isArray(columnValues)) { - continue; - } - if (currentMax < columnValues.length) { - currentMax = columnValues.length; - } - - const columnType = columnIsNumeric.get(columnName) ? 'numerical' : 'categorical'; - - const column: Column = { - name: columnName, - values: columnValues, - type: columnType, - hidden: false, - highlighted: false, - appliedFilters: [], - appliedSort: null, - coloredHighLow: false, - }; - table.columns.push(column); - } - table.totalRows = currentMax; - table.visibleRows = currentMax; - - return table; + return this.runnerResultToTable(tableName, pythonTableColumns, columnIsNumeric); } else { return undefined; } @@ -571,7 +618,7 @@ export class RunnerApi { validRatio, missingRatio, other: [ - { type: 'text', value: 'Categorical', interpretation: 'important' }, + { type: 'text', value: 'Numerical', interpretation: 'important' }, { type: 'text', value: uniqueValues + ' Distincts', @@ -622,44 +669,60 @@ export class RunnerApi { ): Promise { let sdsLines = ''; let placeholderNames: string[] = []; - for (const entry of pastEntries) { + let currentPlaceholderOverride = this.tablePlaceholder; + // let schemaPlaceHolder = this.genPlaceholderName('schema'); + + const filteredPastEntries: HistoryEntry[] = this.filterPastEntries(pastEntries, newEntry); + + for (const entry of filteredPastEntries) { if (entry.type === 'external-manipulating') { // Only manipulating actions have to be repeated before last entry that is of interest, others do not influence that end result - const sdsString = this.sdsStringForHistoryEntry(entry).sdsString; - if (sdsString) { - sdsLines += sdsString + '\n'; - } + const sdsStringObj = this.sdsStringForHistoryEntry(entry, currentPlaceholderOverride); + sdsLines += sdsStringObj.sdsString; + currentPlaceholderOverride = sdsStringObj.placeholderNames[0]!; safeDsLogger.debug(`Running old entry ${entry.id} with action ${entry.action}`); } } if (newEntry.type === 'external-visualizing') { - if (newEntry.action === 'infoPanel' || newEntry.action === 'refreshTab') throw new Error('Not implemented'); + if (newEntry.action === 'infoPanel') throw new Error('Not implemented'); let overriddenTablePlaceholder; if (hiddenColumns && hiddenColumns.length > 0) { overriddenTablePlaceholder = this.genPlaceholderName('hiddenColsOverride'); sdsLines += this.sdsStringForRemoveColumns( hiddenColumns, - this.tablePlaceholder, + currentPlaceholderOverride, overriddenTablePlaceholder, ); } - const sdsStringObj = this.sdsStringForHistoryEntry(newEntry, overriddenTablePlaceholder); + const sdsStringObj = this.sdsStringForHistoryEntry( + newEntry, + overriddenTablePlaceholder ?? currentPlaceholderOverride, + ); sdsLines += sdsStringObj.sdsString; placeholderNames = sdsStringObj.placeholderNames; safeDsLogger.debug(`Running new entry ${newEntry.id} with action ${newEntry.action}`); } else if (newEntry.type === 'external-manipulating') { - throw new Error('Not implemented'); + const sdsStringObj = this.sdsStringForHistoryEntry(newEntry, currentPlaceholderOverride); + sdsLines += sdsStringObj.sdsString; + placeholderNames = sdsStringObj.placeholderNames; + + safeDsLogger.debug(`Running new entry ${newEntry.id} with action ${newEntry.action}`); } else if (newEntry.type === 'internal') { throw new Error('Cannot execute internal history entry in Runner'); } const pipelineExecutionId = crypto.randomUUID(); try { - await this.addToAndExecutePipeline(pipelineExecutionId, sdsLines, placeholderNames); + await this.addToAndExecutePipeline( + pipelineExecutionId, + sdsLines, + // placeholderNames.concat(schemaPlaceHolder), + placeholderNames, + ); } catch (e) { throw e; } @@ -682,7 +745,7 @@ export class RunnerApi { columnNumber: newEntry.columnNumber, imageTab: true, isInGeneration: false, - id: newEntry.existingTabId, + id: newEntry.existingTabId ?? newEntry.newTabId, content: { encodedImage: image }, outdated: false, }, @@ -697,7 +760,7 @@ export class RunnerApi { columnNumber: newEntry.columnNumber, imageTab: true, isInGeneration: false, - id: newEntry.existingTabId, + id: newEntry.existingTabId ?? newEntry.newTabId, outdated: false, content: { encodedImage: image, @@ -716,15 +779,61 @@ export class RunnerApi { columnNumber: newEntry.columnNumber, imageTab: true, isInGeneration: false, - id: newEntry.existingTabId, + id: newEntry.existingTabId ?? newEntry.newTabId, content: { encodedImage: image, columnName: newEntry.columnName }, outdated: false, }, }; } } else { - throw new Error('Not implemented'); + const newTable = await this.getPlaceholderValue(placeholderNames[0]!, pipelineExecutionId); + // const schema = await this.getPlaceholderValue(schemaPlaceHolder, pipelineExecutionId); // Not displayable yet, waiting + + if (!newTable) throw new Error('Table not found'); + + return { + type: 'table', + historyId: newEntry.id, + content: this.runnerResultToTable( + this.tablePlaceholder, + newTable, + new Map( + Object.keys(newTable).map((col) => [col, typeof newTable[col][0] === 'number']), + ), // temp until schema works as otherwise we would need another execution to get column names + ), + }; + } + } + + filterPastEntries(pastEntries: HistoryEntry[], newEntry: HistoryEntry): HistoryEntry[] { + // Keep only the last occurrence of each unique overrideId + const lastOccurrenceMap = new Map(); + const filteredPastEntries: HistoryEntry[] = []; + + // New entry's overrideId is never appended to filteredPastEntries but accounted for in lastOccurrenceMap to have it override other past entries + lastOccurrenceMap.set(newEntry.overrideId, pastEntries.length); + + // Traverse from end to start to record the last occurrence of each unique overrideId + for (let i = pastEntries.length - 1; i >= 0; i--) { + const entry = pastEntries[i]!; + const overrideId = entry.overrideId; + + if (!lastOccurrenceMap.has(overrideId)) { + lastOccurrenceMap.set(overrideId, i); + } } + + // Traverse from start to end to build the final result with only the last occurrences + for (let i = 0; i < pastEntries.length; i++) { + const entry = pastEntries[i]!; + const overrideId = entry.overrideId; + + if (lastOccurrenceMap.get(overrideId) === i) { + filteredPastEntries.push(entry); + } + } + + return filteredPastEntries; } //#endregion //#endregion // Public API