From 7250d2b99479e259617eaac57110005f55ae5e62 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Thu, 29 Feb 2024 20:52:20 +0100 Subject: [PATCH 01/33] fix: eda -> runner comms to own API file & no more undefined eda vars - runnerApi that now handles any eda vscode requests to runner, for starter getPlaceholderValue - edaPanel now renamed panelsMap to instancesMap that also saves RunnerApi instance - RunnerApi instance takes: services, pipelinePath (which is needed to get the document of pipeline that is to be extended) - along with above no longer option for PanelIdentifier or pipelineId to be undefined, not needed anymore without dev methods that start blank eda sessions and this way more safe and solid --- .../src/extension/eda/apis/runnerApi.ts | 78 +++++++++++++++++ .../src/extension/eda/edaPanel.ts | 84 +++++++------------ .../src/extension/mainClient.ts | 44 ++++++++-- 3 files changed, 142 insertions(+), 64 deletions(-) create mode 100644 packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts diff --git a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts new file mode 100644 index 000000000..ed47750c5 --- /dev/null +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -0,0 +1,78 @@ +import { Column, State, Table } from '@safe-ds/eda/types/state.js'; +import { SafeDsServices, messages } from '@safe-ds/lang'; +import { printOutputMessage } from '../../output.ts'; +import * as vscode from 'vscode'; + +export class RunnerApi { + services: SafeDsServices; + pipelinePath: vscode.Uri; + + constructor(services: SafeDsServices, pipelinePath: vscode.Uri) { + this.services = services; + this.pipelinePath = pipelinePath; + } + + public async getPlaceholderValue(tableIdentifier: string, pipelineId: string): Promise { + return new Promise((resolve) => { + if (tableIdentifier === '') { + resolve(undefined); + } + + const placeholderValueCallback = (message: messages.PlaceholderValueMessage) => { + if (message.id !== pipelineId || message.data.name !== tableIdentifier) { + return; + } + this.services.runtime.Runner.removeMessageCallback(placeholderValueCallback, 'placeholder_value'); + + const pythonTableColumns = message.data.value; + const table: Table = { + totalRows: 0, + name: tableIdentifier, + columns: [] as Table['columns'], + appliedFilters: [] as Table['appliedFilters'], + }; + + let i = 0; + let currentMax = 0; + for (const [columnName, columnValues] of Object.entries(pythonTableColumns)) { + if (!Array.isArray(columnValues)) { + continue; + } + if (currentMax < columnValues.length) { + currentMax = columnValues.length; + } + + const isNumerical = typeof columnValues[0] === 'number'; + const columnType = isNumerical ? 'numerical' : 'categorical'; + + const column: Column = { + name: columnName, + values: columnValues, + type: columnType, + hidden: false, + highlighted: false, + appliedFilters: [], + appliedSort: null, + profiling: { top: [], bottom: [] }, + coloredHighLow: false, + }; + table.columns.push([i++, column]); + } + table.totalRows = currentMax; + table.visibleRows = currentMax; + printOutputMessage('Got placeholder from Runner!'); + resolve({ tableIdentifier, history: [], defaultState: false, table }); + }; + + this.services.runtime.Runner.addMessageCallback(placeholderValueCallback, 'placeholder_value'); + printOutputMessage('Getting placeholder from Runner ...'); + this.services.runtime.Runner.sendMessageToPythonServer( + messages.createPlaceholderQueryMessage(pipelineId, tableIdentifier), + ); + + setTimeout(() => { + resolve(undefined); + }, 30000); + }); + } +} diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index 2c5d2c830..4da7614be 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -1,15 +1,14 @@ import * as vscode from 'vscode'; import { ToExtensionMessage } from '@safe-ds/eda/types/messaging.js'; import * as webviewApi from './apis/webviewApi.ts'; -import { Column, State, Table } from '@safe-ds/eda/types/state.js'; +import { State } from '@safe-ds/eda/types/state.js'; import { logOutput, printOutputMessage } from '../output.ts'; -import { messages, SafeDsServices } from '@safe-ds/lang'; - -export const undefinedPanelIdentifier = 'undefinedPanelIdentifier'; +import { SafeDsServices } from '@safe-ds/lang'; +import { RunnerApi } from './apis/runnerApi.ts'; export class EDAPanel { // Map to track multiple panels - private static panelsMap: Map = new Map(); + private static instancesMap: Map = new Map(); private static context: vscode.ExtensionContext; private static services: SafeDsServices; @@ -18,17 +17,18 @@ export class EDAPanel { private readonly panel: vscode.WebviewPanel; private readonly extensionUri: vscode.Uri; private disposables: vscode.Disposable[] = []; - private tableIdentifier: string | undefined; - private startPipelineId: string = ''; + private tableIdentifier: string; private column: vscode.ViewColumn | undefined; private webviewListener: vscode.Disposable | undefined; private viewStateChangeListener: vscode.Disposable | undefined; + private updateHtmlDone: boolean = false; + private startPipelineId: string; private constructor( panel: vscode.WebviewPanel, extensionUri: vscode.Uri, startPipeLineId: string, - tableIdentifier?: string, + tableIdentifier: string, ) { this.panel = panel; this.extensionUri = extensionUri; @@ -36,6 +36,7 @@ export class EDAPanel { this.startPipelineId = startPipeLineId; // Set the webview's initial html content + this.updateHtmlDone = false; this._update(); // Listen for when the panel is disposed @@ -99,23 +100,27 @@ export class EDAPanel { extensionUri: vscode.Uri, context: vscode.ExtensionContext, startPipelineId: string, - servicess: SafeDsServices, - tableIdentifier?: string, + services: SafeDsServices, + tableIdentifier: string, + pipelinePath: vscode.Uri, ) { EDAPanel.context = context; - EDAPanel.services = servicess; + EDAPanel.services = services; // Set column to the active editor if it exists const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; // If we already have a panel, show it. - let panel = EDAPanel.panelsMap.get(tableIdentifier ?? undefinedPanelIdentifier); - if (panel) { + let instance = EDAPanel.instancesMap.get(tableIdentifier); + if (instance) { + let panel = instance.panel; panel.panel.reveal(panel.column); panel.tableIdentifier = tableIdentifier; panel.startPipelineId = startPipelineId; + EDAPanel.instancesMap.set(tableIdentifier, { panel, runnerApi: new RunnerApi(services, pipelinePath) }); // Have to update and construct state as table placeholder could've changed in code + panel.updateHtmlDone = false; panel._update(); panel.constructCurrentState().then((state) => { webviewApi.postMessage(panel!.panel.webview, { @@ -143,7 +148,10 @@ export class EDAPanel { ); const edaPanel = new EDAPanel(newPanel, extensionUri, startPipelineId, tableIdentifier); - EDAPanel.panelsMap.set(tableIdentifier ?? undefinedPanelIdentifier, edaPanel); + EDAPanel.instancesMap.set(tableIdentifier, { + panel: edaPanel, + runnerApi: new RunnerApi(services, pipelinePath), + }); edaPanel.column = column; edaPanel.panel.iconPath = { light: vscode.Uri.joinPath(edaPanel.extensionUri, 'img', 'binoculars-solid.png'), @@ -160,24 +168,15 @@ export class EDAPanel { public static kill(tableIdentifier: string) { printOutputMessage('kill ' + tableIdentifier); - let panel = EDAPanel.panelsMap.get(tableIdentifier); - if (panel) { - panel.dispose(); - EDAPanel.panelsMap.delete(tableIdentifier); - } - } - - public static revive(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, tableIdentifier: string) { - const existingPanel = EDAPanel.panelsMap.get(tableIdentifier); - if (existingPanel) { - existingPanel.dispose(); + let instance = EDAPanel.instancesMap.get(tableIdentifier); + if (instance) { + instance.panel.dispose(); + EDAPanel.instancesMap.delete(tableIdentifier); } - const revivedPanel = new EDAPanel(panel, extensionUri, existingPanel?.startPipelineId ?? '', tableIdentifier); - EDAPanel.panelsMap.set(tableIdentifier, revivedPanel); } public dispose() { - EDAPanel.panelsMap.delete(this.tableIdentifier ?? undefinedPanelIdentifier); + EDAPanel.instancesMap.delete(this.tableIdentifier); // Clean up our panel this.panel.dispose(); @@ -194,6 +193,7 @@ export class EDAPanel { private async _update() { const webview = this.panel.webview; this.panel.webview.html = await this._getHtmlForWebview(webview); + this.updateHtmlDone = true; } private findCurrentState(): State | undefined { @@ -240,34 +240,6 @@ export class EDAPanel { } const isNumerical = typeof columnValues[0] === 'number'; - const columnType = isNumerical ? 'numerical' : 'categorical'; - - const column: Column = { - name: columnName, - values: columnValues, - type: columnType, - hidden: false, - highlighted: false, - appliedFilters: [], - appliedSort: null, - profiling: { top: [], bottom: [] }, - coloredHighLow: false, - }; - table.columns.push([i++, column]); - } - table.totalRows = currentMax; - table.visibleRows = currentMax; - printOutputMessage('Got placeholder from Runner!'); - resolve({ tableIdentifier: this.tableIdentifier, history: [], defaultState: false, table }); - }; - - EDAPanel.services.runtime.Runner.addMessageCallback(placeholderValueCallback, 'placeholder_value'); - printOutputMessage('Getting placeholder from Runner ...'); - EDAPanel.services.runtime.Runner.sendMessageToPythonServer( - messages.createPlaceholderQueryMessage(this.startPipelineId, this.tableIdentifier), - ); - - setTimeout(() => reject(new Error('Timeout waiting for placeholder value')), 30000); }); } diff --git a/packages/safe-ds-vscode/src/extension/mainClient.ts b/packages/safe-ds-vscode/src/extension/mainClient.ts index 1b3e54d43..1c46cda24 100644 --- a/packages/safe-ds-vscode/src/extension/mainClient.ts +++ b/packages/safe-ds-vscode/src/extension/mainClient.ts @@ -7,7 +7,7 @@ import { NodeFileSystem } from 'langium/node'; import { getSafeDSOutputChannel, initializeLog, logError, logOutput, printOutputMessage } from './output.js'; import crypto from 'crypto'; import { LangiumDocument, URI } from 'langium'; -import { EDAPanel, undefinedPanelIdentifier } from './eda/edaPanel.ts'; +import { EDAPanel } from './eda/edaPanel.ts'; import { dumpDiagnostics } from './commands/dumpDiagnostics.js'; import { openDiagnosticsDumps } from './commands/openDiagnosticsDumps.js'; @@ -15,6 +15,7 @@ let client: LanguageClient; let services: SafeDsServices; let lastFinishedPipelineId: string | undefined; let lastSuccessfulPlaceholderName: string | undefined; +let lastSuccessfulPipelinePath: vscode.Uri | undefined; // This function is called when the extension is activated. export const activate = async function (context: vscode.ExtensionContext) { @@ -215,6 +216,7 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { message.data.name === requestedPlaceholderName ) { lastFinishedPipelineId = pipelineId; + lastSuccessfulPipelinePath = editor.document.uri; lastSuccessfulPlaceholderName = requestedPlaceholderName; EDAPanel.createOrShow( context.extensionUri, @@ -222,6 +224,7 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { pipelineId, services, message.data.name, + vscode.window.activeTextEditor!.document.uri, ); services.runtime.Runner.removeMessageCallback(placeholderTypeCallback, 'placeholder_type'); cleanupLoadingIndication(); @@ -263,7 +266,7 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { runPipelineFile(editor.document.uri, pipelineId); } else { - EDAPanel.createOrShow(context.extensionUri, context, '', services, undefined); + vscode.window.showErrorMessage('No placeholder selected!'); } } else { vscode.window.showErrorMessage('No ative text editor!'); @@ -274,14 +277,19 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('safe-ds.refreshWebview', () => { - EDAPanel.kill(lastSuccessfulPlaceholderName ? lastSuccessfulPlaceholderName : undefinedPanelIdentifier); + if (!lastSuccessfulPipelinePath || !lastFinishedPipelineId || !lastSuccessfulPlaceholderName) { + vscode.window.showErrorMessage('No EDA Panel to refresh!'); + return; + } + EDAPanel.kill(lastSuccessfulPlaceholderName); setTimeout(() => { EDAPanel.createOrShow( context.extensionUri, context, - '', + lastFinishedPipelineId!, services, - lastSuccessfulPlaceholderName ? lastSuccessfulPlaceholderName : undefinedPanelIdentifier, + lastSuccessfulPlaceholderName!, + lastSuccessfulPipelinePath!, ); }, 100); setTimeout(() => { @@ -292,6 +300,19 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { }; const runPipelineFile = async function (filePath: vscode.Uri | undefined, pipelineId: string) { + const document = await getPipelineDocument(filePath); + + if (document) { + // Run it + printOutputMessage(`Launching Pipeline (${pipelineId}): ${document?.uri.toString()}`); + + await services.runtime.Runner.executePipeline(document, pipelineId); + } +}; + +export const getPipelineDocument = async function ( + filePath: vscode.Uri | undefined, +): Promise { let pipelinePath = filePath; // Allow execution via command menu if (!pipelinePath && vscode.window.activeTextEditor) { @@ -334,8 +355,6 @@ const runPipelineFile = async function (filePath: vscode.Uri | undefined, pipeli vscode.window.showErrorMessage(validationErrorMessage); return; } - // Run it - printOutputMessage(`Launching Pipeline (${pipelineId}): ${pipelinePath}`); let mainDocument; if (!services.shared.workspace.LangiumDocuments.hasDocument(pipelinePath)) { @@ -348,9 +367,18 @@ const runPipelineFile = async function (filePath: vscode.Uri | undefined, pipeli } else { mainDocument = await services.shared.workspace.LangiumDocuments.getOrCreateDocument(pipelinePath); } - await services.runtime.Runner.executePipeline(mainDocument, pipelineId); + + return mainDocument; }; +// const text = mainDocument.textDocument.getText(); +// // find last "}" in text +// const lastBracket = text.lastIndexOf('}'); +// // insert string in front of it as own line +// const newString = 'val mvr2 = columns[1].missing_value_ratio(); \n'; +// const newText = text.slice(0, lastBracket) + newString + text.slice(lastBracket); +// const newDoc = services.shared.workspace.LangiumDocumentFactory.fromString(newText, pipelinePath); + const commandRunPipelineFile = async function (filePath: vscode.Uri | undefined) { await vscode.workspace.saveAll(); await runPipelineFile(filePath, crypto.randomUUID()); From 217cc3f647baa311a973720bd32125eec5335c9f Mon Sep 17 00:00:00 2001 From: Jonas B Date: Thu, 29 Feb 2024 20:53:15 +0100 Subject: [PATCH 02/33] fix: undefined state on reveal panel - revaling already exisiting panel bug where undefined state, found out it's because the _update() method is only fully executed after current state was already found - new variable updateHtmlDone that is set to false on either creating new panel or revealing current one and to true on _update() done; constructCurrentState will only be executed once that variable is true or with a timeout of 10s --- .../src/extension/eda/edaPanel.ts | 82 +++++++++++-------- 1 file changed, 49 insertions(+), 33 deletions(-) diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index 4da7614be..f69591ea7 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -202,44 +202,60 @@ export class EDAPanel { } private constructCurrentState(): Promise { - return new Promise((resolve, reject) => { - const existingCurrentState = this.findCurrentState(); - if (existingCurrentState) { - printOutputMessage('Found current State.'); - resolve(existingCurrentState); - return; - } - - if (!this.tableIdentifier) { - resolve({ tableIdentifier: undefined, history: [], defaultState: true }); - return; - } - - const placeholderValueCallback = (message: messages.PlaceholderValueMessage) => { - if (message.id !== this.startPipelineId || message.data.name !== this.tableIdentifier) { - return; - } - EDAPanel.services.runtime.Runner.removeMessageCallback(placeholderValueCallback, 'placeholder_value'); - - const pythonTableColumns = message.data.value; - const table: Table = { - totalRows: 0, - name: this.tableIdentifier, - columns: [] as Table['columns'], - appliedFilters: [] as Table['appliedFilters'], + // Helper function to wait until updateHtmlDone is true or timeout + const waitForUpdateHtmlDone = (timeoutMs: number): Promise => { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + // Function to check updateHtmlDone status + const check = () => { + if (this.updateHtmlDone) { + resolve(); + } else if (Date.now() - startTime > timeoutMs) { + reject(new Error('Timeout waiting for updateHtmlDone')); + } else { + setTimeout(check, 100); // Check every 100ms + } }; + check(); + }); + }; - let i = 0; - let currentMax = 0; - for (const [columnName, columnValues] of Object.entries(pythonTableColumns)) { - if (!Array.isArray(columnValues)) { - continue; + return new Promise((resolve, reject) => { + // Wait for updateHtmlDone to be true or timeout after 10s + waitForUpdateHtmlDone(10000) + .then(() => { + // Proceed with the original logic after waiting + const existingCurrentState = this.findCurrentState(); + if (existingCurrentState) { + printOutputMessage('Found current State.'); + resolve(existingCurrentState); + return; } - if (currentMax < columnValues.length) { - currentMax = columnValues.length; + + if (!this.tableIdentifier) { + resolve({ tableIdentifier: undefined, history: [], defaultState: true }); + return; } - const isNumerical = typeof columnValues[0] === 'number'; + const instance = EDAPanel.instancesMap.get(this.tableIdentifier); + if (!instance) { + reject(new Error('RunnerApi instance not found.')); + } else { + instance.runnerApi + .getPlaceholderValue(this.tableIdentifier, this.startPipelineId) + .then((state) => { + if (state === undefined) { + reject(new Error('Timeout waiting for placeholder value')); + } else { + resolve(state); + } + }); + } + }) + .catch((error) => { + // Handle timeout or other errors + reject(error); + }); }); } From bc1ac24e40547f141141a58f40c866c816c151cb Mon Sep 17 00:00:00 2001 From: Jonas B Date: Thu, 29 Feb 2024 20:58:47 +0100 Subject: [PATCH 03/33] fix: waitForUpdateHtmlDone now not part of constructCurrentState anymore --- .../src/extension/eda/edaPanel.ts | 112 ++++++++---------- 1 file changed, 52 insertions(+), 60 deletions(-) diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index f69591ea7..55d9d7de3 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -122,12 +122,14 @@ export class EDAPanel { // Have to update and construct state as table placeholder could've changed in code panel.updateHtmlDone = false; panel._update(); - panel.constructCurrentState().then((state) => { - webviewApi.postMessage(panel!.panel.webview, { - command: 'setWebviewState', - value: state, - }); - }); + panel.waitForUpdateHtmlDone(10000).then(() => + panel.constructCurrentState().then((state) => { + webviewApi.postMessage(panel!.panel.webview, { + command: 'setWebviewState', + value: state, + }); + }), + ); return; } else { // Otherwise, create a new panel. @@ -157,12 +159,14 @@ export class EDAPanel { light: vscode.Uri.joinPath(edaPanel.extensionUri, 'img', 'binoculars-solid.png'), dark: vscode.Uri.joinPath(edaPanel.extensionUri, 'img', 'binoculars-solid.png'), }; - edaPanel.constructCurrentState().then((state) => { - webviewApi.postMessage(edaPanel!.panel.webview, { - command: 'setWebviewState', - value: state, - }); - }); + edaPanel.waitForUpdateHtmlDone(10000).then(() => + edaPanel.constructCurrentState().then((state) => { + webviewApi.postMessage(edaPanel!.panel.webview, { + command: 'setWebviewState', + value: state, + }); + }), + ); } } @@ -196,66 +200,54 @@ export class EDAPanel { this.updateHtmlDone = true; } + private waitForUpdateHtmlDone = (timeoutMs: number): Promise => { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + // Function to check updateHtmlDone status + const check = () => { + if (this.updateHtmlDone) { + resolve(); + } else if (Date.now() - startTime > timeoutMs) { + reject(new Error('Timeout waiting for updateHtmlDone')); + } else { + setTimeout(check, 100); // Check every 100ms + } + }; + check(); + }); + }; + private findCurrentState(): State | undefined { const existingStates = (EDAPanel.context.globalState.get('webviewState') ?? []) as State[]; return existingStates.find((s) => s.tableIdentifier === this.tableIdentifier); } private constructCurrentState(): Promise { - // Helper function to wait until updateHtmlDone is true or timeout - const waitForUpdateHtmlDone = (timeoutMs: number): Promise => { - return new Promise((resolve, reject) => { - const startTime = Date.now(); - // Function to check updateHtmlDone status - const check = () => { - if (this.updateHtmlDone) { - resolve(); - } else if (Date.now() - startTime > timeoutMs) { - reject(new Error('Timeout waiting for updateHtmlDone')); - } else { - setTimeout(check, 100); // Check every 100ms - } - }; - check(); - }); - }; - return new Promise((resolve, reject) => { - // Wait for updateHtmlDone to be true or timeout after 10s - waitForUpdateHtmlDone(10000) - .then(() => { - // Proceed with the original logic after waiting - const existingCurrentState = this.findCurrentState(); - if (existingCurrentState) { - printOutputMessage('Found current State.'); - resolve(existingCurrentState); - return; - } + const existingCurrentState = this.findCurrentState(); + if (existingCurrentState) { + printOutputMessage('Found current State.'); + resolve(existingCurrentState); + return; + } - if (!this.tableIdentifier) { - resolve({ tableIdentifier: undefined, history: [], defaultState: true }); - return; - } + if (!this.tableIdentifier) { + resolve({ tableIdentifier: undefined, history: [], defaultState: true }); + return; + } - const instance = EDAPanel.instancesMap.get(this.tableIdentifier); - if (!instance) { - reject(new Error('RunnerApi instance not found.')); + const instance = EDAPanel.instancesMap.get(this.tableIdentifier); + if (!instance) { + reject(new Error('RunnerApi instance not found.')); + } else { + instance.runnerApi.getPlaceholderValue(this.tableIdentifier, this.startPipelineId).then((state) => { + if (state === undefined) { + reject(new Error('Timeout waiting for placeholder value')); } else { - instance.runnerApi - .getPlaceholderValue(this.tableIdentifier, this.startPipelineId) - .then((state) => { - if (state === undefined) { - reject(new Error('Timeout waiting for placeholder value')); - } else { - resolve(state); - } - }); + resolve(state); } - }) - .catch((error) => { - // Handle timeout or other errors - reject(error); }); + } }); } From 5afb4dfcf06a8044711565338be12c441791dfdb Mon Sep 17 00:00:00 2001 From: Jonas B Date: Thu, 29 Feb 2024 23:30:33 +0100 Subject: [PATCH 04/33] fix: small things & feat: first runnerApi methods that append pipeline --- .../src/extension/eda/apis/runnerApi.ts | 218 ++++++++++++++---- .../src/extension/eda/edaPanel.ts | 12 +- .../src/extension/mainClient.ts | 10 +- 3 files changed, 189 insertions(+), 51 deletions(-) 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 ed47750c5..67bfae1c3 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -2,6 +2,8 @@ import { Column, State, Table } from '@safe-ds/eda/types/state.js'; import { SafeDsServices, messages } from '@safe-ds/lang'; import { printOutputMessage } from '../../output.ts'; import * as vscode from 'vscode'; +import { getPipelineDocument } from '../../mainClient.ts'; +import crypto from 'crypto'; export class RunnerApi { services: SafeDsServices; @@ -12,62 +14,90 @@ export class RunnerApi { this.pipelinePath = pipelinePath; } - public async getPlaceholderValue(tableIdentifier: string, pipelineId: string): Promise { + private async addToAndExecutePipeline(pipelineId: string, addedLines: string): Promise { + return new Promise(async (resolve, reject) => { + const baseDocument = await getPipelineDocument(this.pipelinePath); + if (baseDocument) { + const documentText = baseDocument.textDocument.getText(); + const lastBracket = documentText.lastIndexOf('}'); + + const newText = documentText.slice(0, lastBracket) + addedLines + documentText.slice(lastBracket); + const newDoc = this.services.shared.workspace.LangiumDocumentFactory.fromString( + newText, + this.pipelinePath, + ); + await this.services.runtime.Runner.executePipeline(newDoc, pipelineId); + + const runtimeCallback = (message: messages.RuntimeProgressMessage) => { + if (message.id !== pipelineId) { + return; + } + if (message.data === 'done') { + this.services.runtime.Runner.removeMessageCallback(runtimeCallback, 'runtime_progress'); + resolve(); + } + }; + this.services.runtime.Runner.addMessageCallback(runtimeCallback, 'runtime_progress'); + + setTimeout(() => { + reject('Pipeline execution timed out'); + }, 30000); + } else { + reject('Could not find pipeline document'); + } + }); + } + + private sdsStringForMultMissingValueRatio( + columnsPlaceholder: string, + columnIndex: number, + newPlaceholderName: string, + ): string { + return ( + 'val ' + newPlaceholderName + ' = ' + columnsPlaceholder + '[' + columnIndex + '].missing_value_ratio(); \n' + ); + } + + private sdsStringForColumnNames(tableIdentifier: string, newPlaceholderName: string): string { + return 'val ' + newPlaceholderName + ' = ' + tableIdentifier + '.column_names; \n'; + } + + private sdsStringForColumns(tableIdentifier: string, newPlaceholderName: string): string { + return 'val ' + newPlaceholderName + ' = ' + tableIdentifier + '.to_columns(); \n'; + } + + private randomPlaceholderName(): string { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const charactersLength = characters.length; + const randomArray = new Uint8Array(20); + crypto.getRandomValues(randomArray); + let result = ''; + randomArray.forEach((value) => { + result += characters.charAt(value % charactersLength); + }); + return result; + } + + private async getPlaceholderValue(placeholder: string, pipelineId: string): Promise { return new Promise((resolve) => { - if (tableIdentifier === '') { + if (placeholder === '') { resolve(undefined); } const placeholderValueCallback = (message: messages.PlaceholderValueMessage) => { - if (message.id !== pipelineId || message.data.name !== tableIdentifier) { + if (message.id !== pipelineId || message.data.name !== placeholder) { return; } this.services.runtime.Runner.removeMessageCallback(placeholderValueCallback, 'placeholder_value'); - const pythonTableColumns = message.data.value; - const table: Table = { - totalRows: 0, - name: tableIdentifier, - columns: [] as Table['columns'], - appliedFilters: [] as Table['appliedFilters'], - }; - - let i = 0; - let currentMax = 0; - for (const [columnName, columnValues] of Object.entries(pythonTableColumns)) { - if (!Array.isArray(columnValues)) { - continue; - } - if (currentMax < columnValues.length) { - currentMax = columnValues.length; - } - - const isNumerical = typeof columnValues[0] === 'number'; - const columnType = isNumerical ? 'numerical' : 'categorical'; - - const column: Column = { - name: columnName, - values: columnValues, - type: columnType, - hidden: false, - highlighted: false, - appliedFilters: [], - appliedSort: null, - profiling: { top: [], bottom: [] }, - coloredHighLow: false, - }; - table.columns.push([i++, column]); - } - table.totalRows = currentMax; - table.visibleRows = currentMax; printOutputMessage('Got placeholder from Runner!'); - resolve({ tableIdentifier, history: [], defaultState: false, table }); + resolve(message.data.value); }; this.services.runtime.Runner.addMessageCallback(placeholderValueCallback, 'placeholder_value'); printOutputMessage('Getting placeholder from Runner ...'); this.services.runtime.Runner.sendMessageToPythonServer( - messages.createPlaceholderQueryMessage(pipelineId, tableIdentifier), + messages.createPlaceholderQueryMessage(pipelineId, placeholder), ); setTimeout(() => { @@ -75,4 +105,110 @@ export class RunnerApi { }, 30000); }); } + + // --- Public API --- + + public async getStateByPlaceholder(tableIdentifier: string, pipelineId: string): Promise { + const pythonTableColumns = await this.getPlaceholderValue(tableIdentifier, pipelineId); + if (pythonTableColumns) { + const table: Table = { + totalRows: 0, + name: tableIdentifier, + columns: [] as Table['columns'], + appliedFilters: [] as Table['appliedFilters'], + }; + + let i = 0; + let currentMax = 0; + for (const [columnName, columnValues] of Object.entries(pythonTableColumns)) { + if (!Array.isArray(columnValues)) { + continue; + } + if (currentMax < columnValues.length) { + currentMax = columnValues.length; + } + + const isNumerical = typeof columnValues[0] === 'number'; + const columnType = isNumerical ? 'numerical' : 'categorical'; + + const column: Column = { + name: columnName, + values: columnValues, + type: columnType, + hidden: false, + highlighted: false, + appliedFilters: [], + appliedSort: null, + profiling: { top: [], bottom: [] }, + coloredHighLow: false, + }; + table.columns.push([i++, column]); + } + table.totalRows = currentMax; + table.visibleRows = currentMax; + + return { tableIdentifier, history: [], defaultState: false, table }; + } else { + return undefined; + } + } + + public async getProfiling(tableIdentifier: string): Promise { + const columnsInfo = await this.getColumns(tableIdentifier); + // eslint-disable-next-line no-console + console.log(columnsInfo.columns); + const columns = columnsInfo.columns; + let missingValueRatioSdsStrings = ''; + const placeholderNameToColumnNameMap = new Map(); + const missingValueRatioMap = new Map(); + + for (let i = 0; i < columns.length; i++) { + const newPlaceholderName = this.randomPlaceholderName(); + missingValueRatioMap.set(newPlaceholderName, 'null'); + placeholderNameToColumnNameMap.set(newPlaceholderName, columns[i]!); + missingValueRatioSdsStrings += this.sdsStringForMultMissingValueRatio( + columnsInfo.placeholderName, + i, + newPlaceholderName, + ); + } + + printOutputMessage(missingValueRatioSdsStrings); + + const pipelineId = crypto.randomUUID(); + await this.addToAndExecutePipeline(pipelineId, missingValueRatioSdsStrings); + + for (const [placeholderName] of missingValueRatioMap) { + const missingValueRatio = await this.getPlaceholderValue(placeholderName, pipelineId); + if (missingValueRatio) { + missingValueRatioMap.set(placeholderName, missingValueRatio as string); + } + } + + missingValueRatioMap.forEach((value, key) => { + printOutputMessage(placeholderNameToColumnNameMap.get(key) + ': ' + value); + }); + } + + public async getColumnNames(tableIdentifier: string): Promise { + const newPlaceholderName = this.randomPlaceholderName(); + const columnNamesSdsCode = this.sdsStringForColumnNames(tableIdentifier, newPlaceholderName); + const pipelineId = crypto.randomUUID(); + await this.addToAndExecutePipeline(pipelineId, columnNamesSdsCode); + const columnNames = await this.getPlaceholderValue(newPlaceholderName, pipelineId); + // eslint-disable-next-line no-console + console.log(columnNames); + return columnNames as string[]; + } + + public async getColumns(tableIdentifier: string): Promise<{ columns: any; placeholderName: string }> { + const newPlaceholderName = this.randomPlaceholderName(); + const columnsSdsCode = this.sdsStringForColumns(tableIdentifier, newPlaceholderName); + const pipelineId = crypto.randomUUID(); + await this.addToAndExecutePipeline(pipelineId, columnsSdsCode); + const columns = await this.getPlaceholderValue(newPlaceholderName, pipelineId); + // eslint-disable-next-line no-console + console.log(columns); + return { columns, placeholderName: newPlaceholderName }; + } } diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index 55d9d7de3..f0ab4b832 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -128,6 +128,9 @@ export class EDAPanel { command: 'setWebviewState', value: state, }); + instance!.runnerApi.getProfiling(tableIdentifier).then(() => { + printOutputMessage('JSON.stringify(columnNames)'); + }); }), ); return; @@ -165,6 +168,13 @@ export class EDAPanel { command: 'setWebviewState', value: state, }); + + EDAPanel.instancesMap + .get(tableIdentifier)! + .runnerApi.getProfiling(tableIdentifier) + .then(() => { + printOutputMessage('JSON.stringify(columnNames)'); + }); }), ); } @@ -240,7 +250,7 @@ export class EDAPanel { if (!instance) { reject(new Error('RunnerApi instance not found.')); } else { - instance.runnerApi.getPlaceholderValue(this.tableIdentifier, this.startPipelineId).then((state) => { + instance.runnerApi.getStateByPlaceholder(this.tableIdentifier, this.startPipelineId).then((state) => { if (state === undefined) { reject(new Error('Timeout waiting for placeholder value')); } else { diff --git a/packages/safe-ds-vscode/src/extension/mainClient.ts b/packages/safe-ds-vscode/src/extension/mainClient.ts index 1c46cda24..6b9e615ad 100644 --- a/packages/safe-ds-vscode/src/extension/mainClient.ts +++ b/packages/safe-ds-vscode/src/extension/mainClient.ts @@ -224,7 +224,7 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { pipelineId, services, message.data.name, - vscode.window.activeTextEditor!.document.uri, + editor.document.uri, ); services.runtime.Runner.removeMessageCallback(placeholderTypeCallback, 'placeholder_type'); cleanupLoadingIndication(); @@ -371,14 +371,6 @@ export const getPipelineDocument = async function ( return mainDocument; }; -// const text = mainDocument.textDocument.getText(); -// // find last "}" in text -// const lastBracket = text.lastIndexOf('}'); -// // insert string in front of it as own line -// const newString = 'val mvr2 = columns[1].missing_value_ratio(); \n'; -// const newText = text.slice(0, lastBracket) + newString + text.slice(lastBracket); -// const newDoc = services.shared.workspace.LangiumDocumentFactory.fromString(newText, pipelinePath); - const commandRunPipelineFile = async function (filePath: vscode.Uri | undefined) { await vscode.workspace.saveAll(); await runPipelineFile(filePath, crypto.randomUUID()); From 00a491d05b0ace2fb51428cce48245193134d889 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Fri, 1 Mar 2024 18:21:50 +0100 Subject: [PATCH 05/33] fix: properly show "0" values in table --- packages/safe-ds-eda/src/components/TableView.svelte | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index ba48c01a7..ea94bba1c 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -642,7 +642,10 @@ on:mousemove={(event) => throttledHandleReorderDragOver(event, index)} class:selectedColumn={selectedColumnIndexes.includes(index) || selectedRowIndexes.includes(visibleStart + i)} - >{column[1].values[visibleStart + i] || ''}{column[1].values[visibleStart + i] !== null && + column[1].values[visibleStart + i] !== undefined + ? column[1].values[visibleStart + i] + : ''} {/each} Date: Fri, 1 Mar 2024 18:23:19 +0100 Subject: [PATCH 06/33] feat: get and set basic profiling for new table --- .../src/components/TableView.svelte | 24 +++- packages/safe-ds-eda/src/webviewState.ts | 28 ++++- packages/safe-ds-eda/types/messaging.d.ts | 7 +- packages/safe-ds-eda/types/state.d.ts | 2 +- packages/safe-ds-vscode/media/styles.css | 1 + .../src/extension/eda/apis/runnerApi.ts | 105 +++++++++++------- .../src/extension/eda/edaPanel.ts | 45 +++++--- 7 files changed, 152 insertions(+), 60 deletions(-) diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index ea94bba1c..f126e08b1 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -578,14 +578,29 @@ class="borderColumn border-right profiling" on:mousemove={(event) => throttledHandleReorderDragOver(event, 0)} > - {#each $currentState.table.columns as _column, index} + {#each $currentState.table.columns as column, index} throttledHandleReorderDragOver(event, index)} >
- Heyyyyyyyyyyy
Hey
Hey
Hey
Hey
Hey
Hey + {#if column[1].profiling.top.length === 0 && column[1].profiling.bottom.length === 0} +
Loading ...
+ {:else} + {#each column[1].profiling.top as profilingTopItem} + {#if profilingTopItem.type === 'numerical'} +
+ {profilingTopItem.name}: + {profilingTopItem.value} +
+ {/if} + {/each} + {/if}
{/each} @@ -791,6 +806,11 @@ overflow: visible; } + .profilingItem { + display: flex; + justify-content: space-between; + } + .borderColumn { padding: 5px 5px 5px 5px; width: 45px; diff --git a/packages/safe-ds-eda/src/webviewState.ts b/packages/safe-ds-eda/src/webviewState.ts index 0e51f3d8a..a0a4744e2 100644 --- a/packages/safe-ds-eda/src/webviewState.ts +++ b/packages/safe-ds-eda/src/webviewState.ts @@ -1,7 +1,7 @@ import type { FromExtensionMessage } from '../types/messaging'; import type { State } from '../types/state'; import * as extensionApi from './apis/extensionApi'; -import { writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; let currentTabIndex = writable(0); @@ -26,6 +26,32 @@ window.addEventListener('message', (event) => { // This should be fired immediately whenever the panel is created or made visible again currentState.set(message.value); break; + case 'setProfiling': + if (get(currentState) && get(currentState).table) { + currentState.update((state) => { + return { + ...state, + table: { + ...state.table!, + columns: state.table!.columns.map((column) => { + const profiling = message.value.find((p) => p.columnName === column[1].name); + if (profiling) { + return [ + column[0], + { + ...column[1], + profiling: profiling.profiling, + }, + ]; + } else { + return column; + } + }), + }, + }; + }); + } + break; } }); diff --git a/packages/safe-ds-eda/types/messaging.d.ts b/packages/safe-ds-eda/types/messaging.d.ts index 2b0a645f8..438c7081e 100644 --- a/packages/safe-ds-eda/types/messaging.d.ts +++ b/packages/safe-ds-eda/types/messaging.d.ts @@ -46,4 +46,9 @@ interface FromExtensionSetStateMessage extends FromExtensionCommandMessage { value: defaultTypes.State; } -export type FromExtensionMessage = FromExtensionSetStateMessage; +interface FromExtensionSetProfilingMessage extends FromExtensionCommandMessage { + command: 'setProfiling'; + value: { columnName: string; profiling: defaultTypes.Profiling }[]; +} + +export type FromExtensionMessage = FromExtensionSetStateMessage | FromExtensionSetProfilingMessage; diff --git a/packages/safe-ds-eda/types/state.d.ts b/packages/safe-ds-eda/types/state.d.ts index 26080d7a8..0bfa36b3d 100644 --- a/packages/safe-ds-eda/types/state.d.ts +++ b/packages/safe-ds-eda/types/state.d.ts @@ -83,7 +83,7 @@ export interface ProfilingDetailBase { export interface ProfilingDetailStatistical extends ProfilingDetailBase { type: 'numerical'; name: string; - value: number; + value: string; color?: string; } diff --git a/packages/safe-ds-vscode/media/styles.css b/packages/safe-ds-vscode/media/styles.css index dcd555217..a7f53bccc 100644 --- a/packages/safe-ds-vscode/media/styles.css +++ b/packages/safe-ds-vscode/media/styles.css @@ -1,6 +1,7 @@ :root { --primary-color: #036ed1; --primary-color-desaturated: rgb(3 109 209 / 16.4%); + --error-color: #ce0003; --bg-bright: white; --bg-dark: #f2f2f2; --bg-medium: #f9f9f9; 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 67bfae1c3..7572af599 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -1,4 +1,4 @@ -import { Column, State, Table } from '@safe-ds/eda/types/state.js'; +import { Column, Profiling, ProfilingDetailStatistical, State, Table } from '@safe-ds/eda/types/state.js'; import { SafeDsServices, messages } from '@safe-ds/lang'; import { printOutputMessage } from '../../output.ts'; import * as vscode from 'vscode'; @@ -48,13 +48,19 @@ export class RunnerApi { }); } - private sdsStringForMultMissingValueRatio( - columnsPlaceholder: string, - columnIndex: number, + private sdsStringForMissingValueRatioByColumnName( + columnName: string, + tablePlaceholder: string, newPlaceholderName: string, ): string { return ( - 'val ' + newPlaceholderName + ' = ' + columnsPlaceholder + '[' + columnIndex + '].missing_value_ratio(); \n' + 'val ' + + newPlaceholderName + + ' = ' + + tablePlaceholder + + '.get_column("' + + columnName + + '").missing_value_ratio(); \n' ); } @@ -67,7 +73,7 @@ export class RunnerApi { } private randomPlaceholderName(): string { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const charactersLength = characters.length; const randomArray = new Uint8Array(20); crypto.getRandomValues(randomArray); @@ -89,8 +95,6 @@ export class RunnerApi { return; } this.services.runtime.Runner.removeMessageCallback(placeholderValueCallback, 'placeholder_value'); - - printOutputMessage('Got placeholder from Runner!'); resolve(message.data.value); }; @@ -153,31 +157,31 @@ export class RunnerApi { } } - public async getProfiling(tableIdentifier: string): Promise { - const columnsInfo = await this.getColumns(tableIdentifier); - // eslint-disable-next-line no-console - console.log(columnsInfo.columns); - const columns = columnsInfo.columns; - let missingValueRatioSdsStrings = ''; - const placeholderNameToColumnNameMap = new Map(); - const missingValueRatioMap = new Map(); + public async getProfiling(tableIdentifier: string): Promise<{ columnName: string; profiling: Profiling }[]> { + const columnNames = await this.getColumnNames(tableIdentifier); + + let missingValueRatioSdsStrings = ''; // SDS code to get missing value ratio for each column + const placeholderNameToColumnNameMap = new Map(); // Mapping random placeholder name back to column name + const missingValueRatioMap = new Map(); // Saved by random placeholder name - for (let i = 0; i < columns.length; i++) { + // Generate SDS code to get missing value ratio for each column + for (let i = 0; i < columnNames.length; i++) { const newPlaceholderName = this.randomPlaceholderName(); missingValueRatioMap.set(newPlaceholderName, 'null'); - placeholderNameToColumnNameMap.set(newPlaceholderName, columns[i]!); - missingValueRatioSdsStrings += this.sdsStringForMultMissingValueRatio( - columnsInfo.placeholderName, - i, + placeholderNameToColumnNameMap.set(newPlaceholderName, columnNames[i]!); + + missingValueRatioSdsStrings += this.sdsStringForMissingValueRatioByColumnName( + columnNames[i]!, + tableIdentifier, newPlaceholderName, ); } - printOutputMessage(missingValueRatioSdsStrings); - + // Execute with generated SDS code const pipelineId = crypto.randomUUID(); await this.addToAndExecutePipeline(pipelineId, missingValueRatioSdsStrings); + // Get missing value ratio for each column for (const [placeholderName] of missingValueRatioMap) { const missingValueRatio = await this.getPlaceholderValue(placeholderName, pipelineId); if (missingValueRatio) { @@ -185,9 +189,35 @@ export class RunnerApi { } } - missingValueRatioMap.forEach((value, key) => { - printOutputMessage(placeholderNameToColumnNameMap.get(key) + ': ' + value); - }); + // Create profiling data, interpret numerical values and color them + const profiling: { columnName: string; profiling: Profiling }[] = []; + for (const [placeholderName, columnName] of placeholderNameToColumnNameMap.entries()) { + const missingValuesRatio = parseFloat(missingValueRatioMap.get(placeholderName)!) * 100; + + const validRatio: ProfilingDetailStatistical = { + type: 'numerical', + name: 'Valid', + value: missingValuesRatio ? (100 - missingValuesRatio).toFixed(2) + '%' : '100%', + color: 'var(--primary-color)', + }; + + const missingRatio: ProfilingDetailStatistical = { + type: 'numerical', + name: 'Missing', + value: missingValuesRatio ? missingValuesRatio.toFixed(2) + '%' : '0%', + color: missingValuesRatio > 0 ? 'var(--error-color)' : 'var(--font-light)', + }; + + profiling.push({ + columnName, + profiling: { + top: [validRatio, missingRatio], + bottom: [], + }, + }); + } + + return profiling; } public async getColumnNames(tableIdentifier: string): Promise { @@ -196,19 +226,18 @@ export class RunnerApi { const pipelineId = crypto.randomUUID(); await this.addToAndExecutePipeline(pipelineId, columnNamesSdsCode); const columnNames = await this.getPlaceholderValue(newPlaceholderName, pipelineId); - // eslint-disable-next-line no-console - console.log(columnNames); return columnNames as string[]; } - public async getColumns(tableIdentifier: string): Promise<{ columns: any; placeholderName: string }> { - const newPlaceholderName = this.randomPlaceholderName(); - const columnsSdsCode = this.sdsStringForColumns(tableIdentifier, newPlaceholderName); - const pipelineId = crypto.randomUUID(); - await this.addToAndExecutePipeline(pipelineId, columnsSdsCode); - const columns = await this.getPlaceholderValue(newPlaceholderName, pipelineId); - // eslint-disable-next-line no-console - console.log(columns); - return { columns, placeholderName: newPlaceholderName }; - } + // Doesn't work as columns cannot be serialized yet by Runner + // public async getColumns(tableIdentifier: string): Promise<{ columns: any; placeholderName: string }> { + // const newPlaceholderName = this.randomPlaceholderName(); + // const columnsSdsCode = this.sdsStringForColumns(tableIdentifier, newPlaceholderName); + // const pipelineId = crypto.randomUUID(); + // await this.addToAndExecutePipeline(pipelineId, columnsSdsCode); + // const columns = await this.getPlaceholderValue(newPlaceholderName, pipelineId); + // // eslint-disable-next-line no-console + // console.log(columns); + // return { columns, placeholderName: newPlaceholderName }; + // } } diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index f0ab4b832..eee9f7c90 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -123,14 +123,20 @@ export class EDAPanel { panel.updateHtmlDone = false; panel._update(); panel.waitForUpdateHtmlDone(10000).then(() => - panel.constructCurrentState().then((state) => { + panel.constructCurrentState().then((stateInfo) => { webviewApi.postMessage(panel!.panel.webview, { command: 'setWebviewState', - value: state, - }); - instance!.runnerApi.getProfiling(tableIdentifier).then(() => { - printOutputMessage('JSON.stringify(columnNames)'); + value: stateInfo.state, }); + + if (!stateInfo.fromExisting) { + instance!.runnerApi.getProfiling(tableIdentifier).then((profiling) => { + webviewApi.postMessage(panel!.panel.webview, { + command: 'setProfiling', + value: profiling, + }); + }); + } }), ); return; @@ -163,18 +169,23 @@ export class EDAPanel { dark: vscode.Uri.joinPath(edaPanel.extensionUri, 'img', 'binoculars-solid.png'), }; edaPanel.waitForUpdateHtmlDone(10000).then(() => - edaPanel.constructCurrentState().then((state) => { + edaPanel.constructCurrentState().then((stateInfo) => { webviewApi.postMessage(edaPanel!.panel.webview, { command: 'setWebviewState', - value: state, + value: stateInfo.state, }); - EDAPanel.instancesMap - .get(tableIdentifier)! - .runnerApi.getProfiling(tableIdentifier) - .then(() => { - printOutputMessage('JSON.stringify(columnNames)'); - }); + if (!stateInfo.fromExisting) { + EDAPanel.instancesMap + .get(tableIdentifier)! + .runnerApi.getProfiling(tableIdentifier) + .then((profiling) => { + webviewApi.postMessage(edaPanel!.panel.webview, { + command: 'setProfiling', + value: profiling, + }); + }); + } }), ); } @@ -232,17 +243,17 @@ export class EDAPanel { return existingStates.find((s) => s.tableIdentifier === this.tableIdentifier); } - private constructCurrentState(): Promise { + private constructCurrentState(): Promise<{ state: State; fromExisting: boolean }> { return new Promise((resolve, reject) => { const existingCurrentState = this.findCurrentState(); if (existingCurrentState) { printOutputMessage('Found current State.'); - resolve(existingCurrentState); + resolve({ state: existingCurrentState, fromExisting: true }); return; } if (!this.tableIdentifier) { - resolve({ tableIdentifier: undefined, history: [], defaultState: true }); + resolve({ state: { tableIdentifier: undefined, history: [], defaultState: true }, fromExisting: true }); return; } @@ -254,7 +265,7 @@ export class EDAPanel { if (state === undefined) { reject(new Error('Timeout waiting for placeholder value')); } else { - resolve(state); + resolve({ state, fromExisting: false }); } }); } From 926d79ceb4faa0a766545d2de0129aa145bbfd2e Mon Sep 17 00:00:00 2001 From: Jonas B Date: Fri, 1 Mar 2024 18:29:38 +0100 Subject: [PATCH 07/33] fix: col/row deselection if click row/col again and only one --- .../src/components/TableView.svelte | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index f126e08b1..0aeaf0cf0 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -228,9 +228,15 @@ addColumnToSelection(columnIndex); } } else { - // Replace the current selection - // Replace the current selection with a new array to trigger reactivity - setSelectionToColumn(columnIndex); + const index = selectedColumnIndexes.indexOf(columnIndex); + if (index > -1 && selectedColumnIndexes.length === 1) { + // Already selected, so clear selection + selectedColumnIndexes = []; + } else { + // Not selected, replace the current selection + // Replace the current selection with a new array to trigger reactivity + setSelectionToColumn(columnIndex); + } } }; @@ -280,9 +286,15 @@ selectedRowIndexes = [...selectedRowIndexes, rowIndex]; } } else { - // Replace the current selection - // Replace the current selection with a new array to trigger reactivity - selectedRowIndexes = [rowIndex]; + const index = selectedRowIndexes.indexOf(rowIndex); + if (index > -1 && selectedRowIndexes.length === 1) { + // Already selected, so clear selection + selectedRowIndexes = []; + } else { + // Not selected, replace the current selection + // Replace the current selection with a new array to trigger reactivity + selectedRowIndexes = [rowIndex]; + } } }; From 0b368f7bfa19b0bfe04560fbea5fa12f6df40064 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Fri, 1 Mar 2024 18:39:08 +0100 Subject: [PATCH 08/33] fix: profiling minimal better styling --- .../src/components/TableView.svelte | 121 +++++++++--------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index 0aeaf0cf0..f8a3c1de8 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -806,23 +806,6 @@ z-index: 10; } - .profilingBannerRow { - position: relative; - z-index: 10; - border-top: 2px solid var(--bg-bright); - } - - .profilingBannerRow * { - border-left: none !important; - border-right: none !important; - overflow: visible; - } - - .profilingItem { - display: flex; - justify-content: space-between; - } - .borderColumn { padding: 5px 5px 5px 5px; width: 45px; @@ -844,22 +827,6 @@ vertical-align: middle; } - .profilingBanner { - height: 35px; - width: 100%; - background-color: var(--bg-dark); - font-size: 1.1rem; - border-top: 2px solid var(--bg-bright); - border-left: 3px solid var(--bg-bright); - border-bottom: 3px solid var(--bg-bright); - user-select: none; - padding-left: 0; - z-index: 10; - } - .profilingBanner:hover { - cursor: pointer; - } - .rotate { transform: rotate(180deg); } @@ -872,33 +839,6 @@ margin-left: 5px; } - .profiling { - padding: 0; - border-right: 2px solid var(--bg-bright); - border-left: 3px solid var(--bg-bright); - background-color: var(--bg-dark) !important; - } - - .profiling.expanded { - padding: 8px 12px; - } - - .profiling .content { - max-height: 0; - overflow: hidden; - opacity: 0; - transition: none; - } - - .profiling .content.expanded { - overflow-y: scroll; - max-height: 200px; /* Adjust this value based on the actual content size */ - opacity: 1; - transition: - max-height 0.7s ease, - opacity 0.5s ease; - } - .reorderHighlightedLeft { background: linear-gradient(to right, #036ed1 0%, #036ed1 calc(100% - 2px), white calc(100% - 2px), white 100%); } @@ -937,4 +877,65 @@ background-color: var(--primary-color); color: var(--font-bright); } + + .profilingBannerRow { + position: relative; + z-index: 10; + border-top: 2px solid var(--bg-bright); + } + + .profilingBannerRow * { + border-left: none !important; + border-right: none !important; + overflow: visible; + } + + .profiling { + padding: 0; + border-right: 2px solid var(--bg-bright); + border-left: 3px solid var(--bg-bright); + background-color: var(--bg-dark) !important; + } + + .profiling.expanded { + padding: 8px 2px 8px 12px; + } + + .profiling .content { + max-height: 0; + overflow: hidden; + opacity: 0; + transition: none; + } + + .profilingBanner { + height: 35px; + width: 100%; + background-color: var(--bg-dark); + font-size: 1.1rem; + border-top: 2px solid var(--bg-bright); + border-left: 3px solid var(--bg-bright); + border-bottom: 3px solid var(--bg-bright); + user-select: none; + padding-left: 0; + z-index: 10; + } + + .profilingBanner:hover { + cursor: pointer; + } + + .profiling .content.expanded { + overflow-y: scroll; + max-height: 500px; /* Adjust this value based on the actual content size */ + opacity: 1; + transition: + max-height 0.7s ease, + opacity 0.5s ease; + } + + .profilingItem { + display: flex; + justify-content: space-between; + } From f220bf5ad265d6210e13f99f03cfa78c02cddaff Mon Sep 17 00:00:00 2001 From: Jonas B Date: Fri, 1 Mar 2024 18:52:58 +0100 Subject: [PATCH 09/33] feat: optionally pass table to getProfiling for performance --- .../src/extension/eda/apis/runnerApi.ts | 12 ++++++++++-- .../safe-ds-vscode/src/extension/eda/edaPanel.ts | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) 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 7572af599..1f7ce5ee0 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -157,8 +157,16 @@ export class RunnerApi { } } - public async getProfiling(tableIdentifier: string): Promise<{ columnName: string; profiling: Profiling }[]> { - const columnNames = await this.getColumnNames(tableIdentifier); + public async getProfiling( + tableIdentifier: string, + table?: Table, + ): Promise<{ columnName: string; profiling: Profiling }[]> { + let columnNames: string[] = []; + if (!table) { + columnNames = await this.getColumnNames(tableIdentifier); + } else { + columnNames = table.columns.map((column) => column[1].name); + } let missingValueRatioSdsStrings = ''; // SDS code to get missing value ratio for each column const placeholderNameToColumnNameMap = new Map(); // Mapping random placeholder name back to column name diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index eee9f7c90..53c4af2d1 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -130,7 +130,7 @@ export class EDAPanel { }); if (!stateInfo.fromExisting) { - instance!.runnerApi.getProfiling(tableIdentifier).then((profiling) => { + instance!.runnerApi.getProfiling(tableIdentifier, stateInfo.state.table).then((profiling) => { webviewApi.postMessage(panel!.panel.webview, { command: 'setProfiling', value: profiling, @@ -178,7 +178,7 @@ export class EDAPanel { if (!stateInfo.fromExisting) { EDAPanel.instancesMap .get(tableIdentifier)! - .runnerApi.getProfiling(tableIdentifier) + .runnerApi.getProfiling(tableIdentifier, stateInfo.state.table) .then((profiling) => { webviewApi.postMessage(edaPanel!.panel.webview, { command: 'setProfiling', From e953262abb9f9893dd3275a0764e97d2da9e75a5 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Fri, 1 Mar 2024 23:18:33 +0100 Subject: [PATCH 10/33] feat: more profiling info & profiling styling --- .../src/components/TableView.svelte | 162 +++++++++++++++--- .../src/extension/eda/apis/runnerApi.ts | 133 +++++++++++--- .../src/extension/eda/edaPanel.ts | 9 +- 3 files changed, 249 insertions(+), 55 deletions(-) diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index f8a3c1de8..152cdbaec 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -3,6 +3,7 @@ import { throttle } from 'lodash'; import { currentState, preventClicks } from '../webviewState'; import CaretIcon from '../icons/Caret.svelte'; + import type { ProfilingDetailBase } from '../../types/state'; export let sidebarWidth: number; @@ -15,6 +16,7 @@ let numRows = 0; const borderColumnWidth = 45; // Set in CSS, change here if changes in css const headerElements: HTMLElement[] = []; + let tallestProfiling: { type: ProfilingDetailBase['type']; count: number }; const savedColumnWidths: Map = new Map(); $: { @@ -26,10 +28,68 @@ numRows = column[1].values.length; } minTableWidth += 100; + + // Find which is the talles profiling type present in this table to adjust which profilings to give small height to, to have them adhere to good spacing + // (cannot give to tallest one, as then it will all be small) + // TODO very hacky way and not scalable for the Profiling types to change, but found no better way + if (column[1].profiling.top.length > 0 || column[1].profiling.bottom.length > 0) { + const imageCount = column[1].profiling.top.filter((p) => p.type === 'image').length; + const numericalCount = column[1].profiling.bottom.filter((p) => p.type === 'numerical').length; + const nameCount = column[1].profiling.bottom.filter((p) => p.type === 'name').length; + + if (imageCount > 0) { + if ( + !tallestProfiling || + tallestProfiling.type === 'name' || + tallestProfiling.type === 'numerical' + ) { + tallestProfiling = { type: 'image', count: imageCount }; + } else if (tallestProfiling.type === 'image' && tallestProfiling.count < imageCount) { + tallestProfiling = { type: 'image', count: imageCount }; + } + } else if (numericalCount > 0) { + if (!tallestProfiling || tallestProfiling.type === 'name') { + tallestProfiling = { type: 'numerical', count: numericalCount }; + } else if (tallestProfiling.type === 'numerical' && tallestProfiling.count < numericalCount) { + tallestProfiling = { type: 'numerical', count: numericalCount }; + } + } else if (nameCount > 0) { + if (!tallestProfiling) { + tallestProfiling = { type: 'name', count: nameCount }; + } else if (tallestProfiling.type === 'name' && tallestProfiling.count < nameCount) { + tallestProfiling = { type: 'name', count: nameCount }; + } + } + } }); } } + const getOptionalProfilingHeight = function (profiling: ProfilingDetailBase[]): string { + if (tallestProfiling.type === 'image') { + const imageCount = profiling.filter((p) => p.type === 'image').length; + if (imageCount > 0 && tallestProfiling.count === imageCount) { + return ''; + } else { + return '10px'; + } + } else if (tallestProfiling.type === 'numerical') { + const numericalCount = profiling.filter((p) => p.type === 'numerical').length; + if (numericalCount > 0 && tallestProfiling.count === numericalCount) { + return ''; + } else { + return '10px'; + } + } else { + const nameCount = profiling.filter((p) => p.type === 'name').length; + if (nameCount > 0 && tallestProfiling.count === nameCount) { + return ''; + } else { + return '10px'; + } + } + }; + const getColumnWidth = function (columnName: string): number { if (savedColumnWidths.has(columnName)) { return savedColumnWidths.get(columnName)!; @@ -594,24 +654,64 @@ throttledHandleReorderDragOver(event, index)} >
{#if column[1].profiling.top.length === 0 && column[1].profiling.bottom.length === 0}
Loading ...
{:else} - {#each column[1].profiling.top as profilingTopItem} - {#if profilingTopItem.type === 'numerical'} -
- {profilingTopItem.name}: - {profilingTopItem.value} -
- {/if} - {/each} +
+ {#each column[1].profiling.top as profilingTopItem} + {#if profilingTopItem.type === 'name'} +
+ {@html profilingTopItem.name} +
+ {:else if profilingTopItem.type === 'numerical'} +
+ {profilingTopItem.name}: + {profilingTopItem.value} +
+ {/if} + {/each} +
+
+ {#each column[1].profiling.bottom as profilingBottomItem} + {#if profilingBottomItem.type === 'name'} +
+ {@html profilingBottomItem.name} +
+ {:else if profilingBottomItem.type === 'numerical'} +
+ {profilingBottomItem.name}: + {profilingBottomItem.value} +
+ {/if} + {/each} +
{/if}
@@ -898,7 +998,7 @@ } .profiling.expanded { - padding: 8px 2px 8px 12px; + padding: 10px 2px 10px 12px; } .profiling .content { @@ -906,6 +1006,19 @@ overflow: hidden; opacity: 0; transition: none; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + } + + .profiling .content.expanded { + overflow-y: scroll; + max-height: 500px; + opacity: 1; + transition: + max-height 0.7s ease, + opacity 0.5s ease; } .profilingBanner { @@ -925,17 +1038,22 @@ cursor: pointer; } - .profiling .content.expanded { - overflow-y: scroll; - max-height: 500px; /* Adjust this value based on the actual content size */ - opacity: 1; - transition: - max-height 0.7s ease, - opacity 0.5s ease; - } - .profilingItem { display: flex; justify-content: space-between; + margin-bottom: 1px; + } + + .profilingItemFirst { + margin-right: 10px; + } + + .profilingItemsTop { + margin-bottom: 20px; + width: 100%; + } + + .profilingItemsBottom { + width: 100%; } 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 1f7ce5ee0..f989f8d39 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -72,6 +72,12 @@ export class RunnerApi { return 'val ' + newPlaceholderName + ' = ' + tableIdentifier + '.to_columns(); \n'; } + private sdsStringForIDnessByColumnName(columnName: string, tablePlaceholder: string, newPlaceholderName: string) { + return ( + 'val ' + newPlaceholderName + ' = ' + tablePlaceholder + '.get_column("' + columnName + '").idness(); \n' + ); + } + private randomPlaceholderName(): string { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const charactersLength = characters.length; @@ -159,35 +165,46 @@ export class RunnerApi { public async getProfiling( tableIdentifier: string, - table?: Table, + table: Table, ): Promise<{ columnName: string; profiling: Profiling }[]> { - let columnNames: string[] = []; - if (!table) { - columnNames = await this.getColumnNames(tableIdentifier); - } else { - columnNames = table.columns.map((column) => column[1].name); - } + const columns = table.columns; - let missingValueRatioSdsStrings = ''; // SDS code to get missing value ratio for each column - const placeholderNameToColumnNameMap = new Map(); // Mapping random placeholder name back to column name + let sdsStrings = ''; + + const columnNameToPlaceholderMVNameMap = new Map(); // Mapping random placeholder name for missing value ratio back to column name const missingValueRatioMap = new Map(); // Saved by random placeholder name + const columnNameToPlaceholderIDnessNameMap = new Map(); // Mapping random placeholder name for IDness back to column name + const idnessMap = new Map(); // Saved by random placeholder name + // Generate SDS code to get missing value ratio for each column - for (let i = 0; i < columnNames.length; i++) { - const newPlaceholderName = this.randomPlaceholderName(); - missingValueRatioMap.set(newPlaceholderName, 'null'); - placeholderNameToColumnNameMap.set(newPlaceholderName, columnNames[i]!); + for (let i = 0; i < columns.length; i++) { + const newMvPlaceholderName = this.randomPlaceholderName(); + missingValueRatioMap.set(newMvPlaceholderName, 'null'); + columnNameToPlaceholderMVNameMap.set(columns[i]![1].name, newMvPlaceholderName); - missingValueRatioSdsStrings += this.sdsStringForMissingValueRatioByColumnName( - columnNames[i]!, + sdsStrings += this.sdsStringForMissingValueRatioByColumnName( + columns[i]![1].name, tableIdentifier, - newPlaceholderName, + newMvPlaceholderName, ); + + // Only need to check IDness for non-numerical columns + if (columns[i]![1].type !== 'numerical') { + const newIDnessPlaceholderName = this.randomPlaceholderName(); + idnessMap.set(newIDnessPlaceholderName, 1); + columnNameToPlaceholderIDnessNameMap.set(columns[i]![1].name, newIDnessPlaceholderName); + sdsStrings += this.sdsStringForIDnessByColumnName( + columns[i]![1].name, + tableIdentifier, + newIDnessPlaceholderName, + ); + } } // Execute with generated SDS code const pipelineId = crypto.randomUUID(); - await this.addToAndExecutePipeline(pipelineId, missingValueRatioSdsStrings); + await this.addToAndExecutePipeline(pipelineId, sdsStrings); // Get missing value ratio for each column for (const [placeholderName] of missingValueRatioMap) { @@ -197,10 +214,19 @@ export class RunnerApi { } } + // Get IDness for each column + for (const [placeholderName] of idnessMap) { + const idness = await this.getPlaceholderValue(placeholderName, pipelineId); + if (idness) { + idnessMap.set(placeholderName, idness as number); + } + } + // Create profiling data, interpret numerical values and color them const profiling: { columnName: string; profiling: Profiling }[] = []; - for (const [placeholderName, columnName] of placeholderNameToColumnNameMap.entries()) { - const missingValuesRatio = parseFloat(missingValueRatioMap.get(placeholderName)!) * 100; + for (const column of columns) { + const missingValuesRatio = + parseFloat(missingValueRatioMap.get(columnNameToPlaceholderMVNameMap.get(column[1].name)!)!) * 100; const validRatio: ProfilingDetailStatistical = { type: 'numerical', @@ -216,13 +242,68 @@ export class RunnerApi { color: missingValuesRatio > 0 ? 'var(--error-color)' : 'var(--font-light)', }; - profiling.push({ - columnName, - profiling: { - top: [validRatio, missingRatio], - bottom: [], - }, - }); + // If not numerical, add proper profilings according to idness results + if (column[1].type !== 'numerical') { + const idness = idnessMap.get(columnNameToPlaceholderIDnessNameMap.get(column[1].name)!)!; + const uniqueValues = idness * column[1].values.length; + + if (uniqueValues <= 3) { + // Can display each separate percentages of unique values + // Find all unique values + const uniqueValueCounts = new Map(); + for (let i = 0; i < column[1].values.length; i++) { + if (column[1].values[i]) + uniqueValueCounts.set( + column[1].values[i], + (uniqueValueCounts.get(column[1].values[i]) || 0) + 1, + ); + } + + let uniqueProfilings: ProfilingDetailStatistical[] = []; + for (const [key, value] of uniqueValueCounts) { + uniqueProfilings.push({ + type: 'numerical', + name: key, + value: ((value / column[1].values.length) * 100).toFixed(2) + '%', + color: 'var(--font-light)', + }); + } + + profiling.push({ + columnName: column[1].name, + profiling: { + top: [validRatio, missingRatio], + bottom: [ + { type: 'name', name: 'Categorical', color: 'var(--font-dark)' }, + ...uniqueProfilings, + ], + }, + }); + } else { + // Display only the number of unique values + profiling.push({ + columnName: column[1].name, + profiling: { + top: [validRatio, missingRatio], + bottom: [ + { + type: 'name', + name: uniqueValues + ' Unique
Strings', + color: 'var(--font-light)', + }, + ], + }, + }); + } + } else { + profiling.push({ + columnName: column[1].name, + profiling: { + top: [validRatio, missingRatio], + bottom: [], + }, + }); + } } return profiling; diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index 53c4af2d1..2766c6431 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -130,7 +130,7 @@ export class EDAPanel { }); if (!stateInfo.fromExisting) { - instance!.runnerApi.getProfiling(tableIdentifier, stateInfo.state.table).then((profiling) => { + instance!.runnerApi.getProfiling(tableIdentifier, stateInfo.state.table!).then((profiling) => { webviewApi.postMessage(panel!.panel.webview, { command: 'setProfiling', value: profiling, @@ -178,7 +178,7 @@ export class EDAPanel { if (!stateInfo.fromExisting) { EDAPanel.instancesMap .get(tableIdentifier)! - .runnerApi.getProfiling(tableIdentifier, stateInfo.state.table) + .runnerApi.getProfiling(tableIdentifier, stateInfo.state.table!) .then((profiling) => { webviewApi.postMessage(edaPanel!.panel.webview, { command: 'setProfiling', @@ -252,11 +252,6 @@ export class EDAPanel { return; } - if (!this.tableIdentifier) { - resolve({ state: { tableIdentifier: undefined, history: [], defaultState: true }, fromExisting: true }); - return; - } - const instance = EDAPanel.instancesMap.get(this.tableIdentifier); if (!instance) { reject(new Error('RunnerApi instance not found.')); From cebbda63bf001c40e9ddfa84a9e5124beac2a9a0 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Tue, 5 Mar 2024 16:41:45 +0100 Subject: [PATCH 11/33] fix: ProfilingInfo own comp & better profiling height calc --- .../src/components/TableView.svelte | 156 ++++-------------- .../components/profiling/ProfilingInfo.svelte | 73 ++++++++ 2 files changed, 106 insertions(+), 123 deletions(-) create mode 100644 packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index 152cdbaec..c78539746 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -3,7 +3,8 @@ import { throttle } from 'lodash'; import { currentState, preventClicks } from '../webviewState'; import CaretIcon from '../icons/Caret.svelte'; - import type { ProfilingDetailBase } from '../../types/state'; + import type { Profiling, ProfilingDetailBase } from '../../types/state'; + import ProfilingInfo from './profiling/ProfilingInfo.svelte'; export let sidebarWidth: number; @@ -16,7 +17,7 @@ let numRows = 0; const borderColumnWidth = 45; // Set in CSS, change here if changes in css const headerElements: HTMLElement[] = []; - let tallestProfiling: { type: ProfilingDetailBase['type']; count: number }; + let maxProfilingItemCount = 0; const savedColumnWidths: Map = new Map(); $: { @@ -31,62 +32,36 @@ // Find which is the talles profiling type present in this table to adjust which profilings to give small height to, to have them adhere to good spacing // (cannot give to tallest one, as then it will all be small) - // TODO very hacky way and not scalable for the Profiling types to change, but found no better way if (column[1].profiling.top.length > 0 || column[1].profiling.bottom.length > 0) { - const imageCount = column[1].profiling.top.filter((p) => p.type === 'image').length; - const numericalCount = column[1].profiling.bottom.filter((p) => p.type === 'numerical').length; - const nameCount = column[1].profiling.bottom.filter((p) => p.type === 'name').length; - - if (imageCount > 0) { - if ( - !tallestProfiling || - tallestProfiling.type === 'name' || - tallestProfiling.type === 'numerical' - ) { - tallestProfiling = { type: 'image', count: imageCount }; - } else if (tallestProfiling.type === 'image' && tallestProfiling.count < imageCount) { - tallestProfiling = { type: 'image', count: imageCount }; - } - } else if (numericalCount > 0) { - if (!tallestProfiling || tallestProfiling.type === 'name') { - tallestProfiling = { type: 'numerical', count: numericalCount }; - } else if (tallestProfiling.type === 'numerical' && tallestProfiling.count < numericalCount) { - tallestProfiling = { type: 'numerical', count: numericalCount }; - } - } else if (nameCount > 0) { - if (!tallestProfiling) { - tallestProfiling = { type: 'name', count: nameCount }; - } else if (tallestProfiling.type === 'name' && tallestProfiling.count < nameCount) { - tallestProfiling = { type: 'name', count: nameCount }; - } + maxProfilingItemCount = 0; + for (const profilingItem of column[1].profiling.top.concat(column[1].profiling.bottom)) { + maxProfilingItemCount += calcProfilingItemValue(profilingItem); } } }); } } - const getOptionalProfilingHeight = function (profiling: ProfilingDetailBase[]): string { - if (tallestProfiling.type === 'image') { - const imageCount = profiling.filter((p) => p.type === 'image').length; - if (imageCount > 0 && tallestProfiling.count === imageCount) { - return ''; - } else { - return '10px'; - } - } else if (tallestProfiling.type === 'numerical') { - const numericalCount = profiling.filter((p) => p.type === 'numerical').length; - if (numericalCount > 0 && tallestProfiling.count === numericalCount) { - return ''; - } else { - return '10px'; - } + const getOptionalProfilingHeight = function (profiling: Profiling): string { + let profilingItemCount = 0; + + for (const profilingItem of profiling.top.concat(profiling.bottom)) { + profilingItemCount += calcProfilingItemValue(profilingItem); + } + + if (profilingItemCount === maxProfilingItemCount) { + return ''; } else { - const nameCount = profiling.filter((p) => p.type === 'name').length; - if (nameCount > 0 && tallestProfiling.count === nameCount) { - return ''; - } else { - return '10px'; - } + return '30px'; + } + }; + + const calcProfilingItemValue = function (profilingItem: ProfilingDetailBase): number { + // To edit when Profiling type scales/changes + if (profilingItem.type === 'image') { + return 3; // Bigger than normal text line, should be set to 3x line height + } else { + return 1; } }; @@ -613,6 +588,7 @@ {:else}
+ + @@ -722,6 +652,7 @@ throttledHandleReorderDragOver(event, $currentState.table?.columns.length ?? 0)} > + + {#each Array(Math.min(visibleEnd, numRows) - visibleStart) as _, i} @@ -1006,9 +938,6 @@ overflow: hidden; opacity: 0; transition: none; - display: flex; - flex-direction: column; - justify-content: space-between; height: 100%; } @@ -1037,23 +966,4 @@ .profilingBanner:hover { cursor: pointer; } - - .profilingItem { - display: flex; - justify-content: space-between; - margin-bottom: 1px; - } - - .profilingItemFirst { - margin-right: 10px; - } - - .profilingItemsTop { - margin-bottom: 20px; - width: 100%; - } - - .profilingItemsBottom { - width: 100%; - } diff --git a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte new file mode 100644 index 000000000..904f20de9 --- /dev/null +++ b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte @@ -0,0 +1,73 @@ + + +
+
+ {#each profiling.top as profilingTopItem} + {#if profilingTopItem.type === 'name'} +
+ {profilingTopItem.name} +
+ {:else if profilingTopItem.type === 'numerical'} +
+ {profilingTopItem.name}: + {profilingTopItem.value} +
+ {/if} + {/each} +
+
+ {#each profiling.bottom as profilingBottomItem} + {#if profilingBottomItem.type === 'name'} +
+ {profilingBottomItem.name} +
+ {:else if profilingBottomItem.type === 'numerical'} +
+ {profilingBottomItem.name}: + {profilingBottomItem.value} +
+ {/if} + {/each} +
+
+ + From 2789859c206fc0bbb16614809cb3d0901a90b46d Mon Sep 17 00:00:00 2001 From: Jonas B Date: Tue, 5 Mar 2024 18:45:04 +0100 Subject: [PATCH 12/33] fix: profiling fetch also if state existing but no profiling info --- .../safe-ds-vscode/src/extension/eda/edaPanel.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index 2766c6431..4fb60f6c3 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -129,7 +129,13 @@ export class EDAPanel { value: stateInfo.state, }); - if (!stateInfo.fromExisting) { + if ( + !stateInfo.fromExisting || + !stateInfo.state.table || + !stateInfo.state + .table!.columns.map((c) => c[1].profiling) + .find((p) => p.bottom.length > 0 || p.top.length > 0) + ) { instance!.runnerApi.getProfiling(tableIdentifier, stateInfo.state.table!).then((profiling) => { webviewApi.postMessage(panel!.panel.webview, { command: 'setProfiling', @@ -175,7 +181,13 @@ export class EDAPanel { value: stateInfo.state, }); - if (!stateInfo.fromExisting) { + if ( + !stateInfo.fromExisting || + !stateInfo.state.table || + !stateInfo.state + .table!.columns.map((c) => c[1].profiling) + .find((p) => p.bottom.length > 0 || p.top.length > 0) + ) { EDAPanel.instancesMap .get(tableIdentifier)! .runnerApi.getProfiling(tableIdentifier, stateInfo.state.table!) From 2cb1da694065e570ea0388a9c52cd09a710e75c3 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Tue, 5 Mar 2024 18:45:48 +0100 Subject: [PATCH 13/33] fix: icon colors & profiling height bug & warn icon if missing vals --- .../src/components/TableView.svelte | 87 +++++++++++++------ packages/safe-ds-eda/src/icons/BarPlot.svelte | 2 +- packages/safe-ds-eda/src/icons/Caret.svelte | 2 +- .../safe-ds-eda/src/icons/LinePlot.svelte | 2 +- packages/safe-ds-eda/src/icons/Table.svelte | 2 +- packages/safe-ds-eda/src/icons/Undo.svelte | 2 +- packages/safe-ds-eda/src/icons/Warn.svelte | 6 ++ packages/safe-ds-vscode/media/styles.css | 2 +- .../src/extension/eda/apis/runnerApi.ts | 40 ++++++++- 9 files changed, 111 insertions(+), 34 deletions(-) create mode 100644 packages/safe-ds-eda/src/icons/Warn.svelte diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index c78539746..c99d24389 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -3,6 +3,7 @@ import { throttle } from 'lodash'; import { currentState, preventClicks } from '../webviewState'; import CaretIcon from '../icons/Caret.svelte'; + import WarnIcon from '../icons/Warn.svelte'; import type { Profiling, ProfilingDetailBase } from '../../types/state'; import ProfilingInfo from './profiling/ProfilingInfo.svelte'; @@ -24,6 +25,7 @@ if ($currentState.table) { minTableWidth = 0; numRows = 0; + maxProfilingItemCount = 0; $currentState.table.columns.forEach((column) => { if (column[1].values.length > numRows) { numRows = column[1].values.length; @@ -33,38 +35,18 @@ // Find which is the talles profiling type present in this table to adjust which profilings to give small height to, to have them adhere to good spacing // (cannot give to tallest one, as then it will all be small) if (column[1].profiling.top.length > 0 || column[1].profiling.bottom.length > 0) { - maxProfilingItemCount = 0; + let profilingItemCount = 0; for (const profilingItem of column[1].profiling.top.concat(column[1].profiling.bottom)) { - maxProfilingItemCount += calcProfilingItemValue(profilingItem); + profilingItemCount += calcProfilingItemValue(profilingItem); + } + if (profilingItemCount > maxProfilingItemCount) { + maxProfilingItemCount = profilingItemCount; } } }); } } - const getOptionalProfilingHeight = function (profiling: Profiling): string { - let profilingItemCount = 0; - - for (const profilingItem of profiling.top.concat(profiling.bottom)) { - profilingItemCount += calcProfilingItemValue(profilingItem); - } - - if (profilingItemCount === maxProfilingItemCount) { - return ''; - } else { - return '30px'; - } - }; - - const calcProfilingItemValue = function (profilingItem: ProfilingDetailBase): number { - // To edit when Profiling type scales/changes - if (profilingItem.type === 'image') { - return 3; // Bigger than normal text line, should be set to 3x line height - } else { - return 1; - } - }; - const getColumnWidth = function (columnName: string): number { if (savedColumnWidths.has(columnName)) { return savedColumnWidths.get(columnName)!; @@ -522,6 +504,44 @@ if (!$preventClicks) showProfiling = !showProfiling; }; + const getOptionalProfilingHeight = function (profiling: Profiling): string { + let profilingItemCount = 0; + + for (const profilingItem of profiling.top.concat(profiling.bottom)) { + profilingItemCount += calcProfilingItemValue(profilingItem); + } + + if (profilingItemCount === maxProfilingItemCount) { + return ''; + } else { + return '30px'; + } + }; + + const calcProfilingItemValue = function (profilingItem: ProfilingDetailBase): number { + // To edit when Profiling type scales/changes + if (profilingItem.type === 'image') { + return 3; // Bigger than normal text line, should be set to 3x line height + } else { + return 1; + } + }; + + const profilingWarnings = function (): boolean { + for (const column of $currentState.table!.columns) { + for (const profilingItem of column[1].profiling.top) { + if ( + profilingItem.type === 'numerical' && + profilingItem.name === 'Missing' && + parseFloat(profilingItem.value.replace('%', '')) > 0 + ) { + return true; + } + } + } + return false; + }; + // --- Lifecycle --- let interval: NodeJS.Timeout; @@ -666,7 +686,12 @@ >
{showProfiling ? 'Hide Profiling' : 'Show Profiling'} -
+ {#if profilingWarnings()} +
+ +
+ {/if} +
@@ -863,7 +888,7 @@ transform: rotate(180deg); } - .iconWrapper { + .caretIconWrapper { display: inline-flex; justify-content: center; height: 100%; @@ -871,6 +896,14 @@ margin-left: 5px; } + .warnIconWrapper { + display: inline-flex; + justify-content: center; + height: 100%; + width: 13px; + margin-left: 2px; + } + .reorderHighlightedLeft { background: linear-gradient(to right, #036ed1 0%, #036ed1 calc(100% - 2px), white calc(100% - 2px), white 100%); } diff --git a/packages/safe-ds-eda/src/icons/BarPlot.svelte b/packages/safe-ds-eda/src/icons/BarPlot.svelte index fed3904b6..f3728e13f 100644 --- a/packages/safe-ds-eda/src/icons/BarPlot.svelte +++ b/packages/safe-ds-eda/src/icons/BarPlot.svelte @@ -1,6 +1,6 @@ diff --git a/packages/safe-ds-eda/src/icons/Caret.svelte b/packages/safe-ds-eda/src/icons/Caret.svelte index 7ffe2c10e..b1e3cbc24 100644 --- a/packages/safe-ds-eda/src/icons/Caret.svelte +++ b/packages/safe-ds-eda/src/icons/Caret.svelte @@ -1,3 +1,3 @@ - + diff --git a/packages/safe-ds-eda/src/icons/LinePlot.svelte b/packages/safe-ds-eda/src/icons/LinePlot.svelte index 31ee95841..7bf240b0d 100644 --- a/packages/safe-ds-eda/src/icons/LinePlot.svelte +++ b/packages/safe-ds-eda/src/icons/LinePlot.svelte @@ -1,6 +1,6 @@ diff --git a/packages/safe-ds-eda/src/icons/Table.svelte b/packages/safe-ds-eda/src/icons/Table.svelte index d3b185d04..3ff160b6a 100644 --- a/packages/safe-ds-eda/src/icons/Table.svelte +++ b/packages/safe-ds-eda/src/icons/Table.svelte @@ -1,6 +1,6 @@ diff --git a/packages/safe-ds-eda/src/icons/Undo.svelte b/packages/safe-ds-eda/src/icons/Undo.svelte index 0642d4590..f0f87726a 100644 --- a/packages/safe-ds-eda/src/icons/Undo.svelte +++ b/packages/safe-ds-eda/src/icons/Undo.svelte @@ -1,6 +1,6 @@ diff --git a/packages/safe-ds-eda/src/icons/Warn.svelte b/packages/safe-ds-eda/src/icons/Warn.svelte new file mode 100644 index 000000000..25dcad6b9 --- /dev/null +++ b/packages/safe-ds-eda/src/icons/Warn.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/safe-ds-vscode/media/styles.css b/packages/safe-ds-vscode/media/styles.css index a7f53bccc..12dcb4015 100644 --- a/packages/safe-ds-vscode/media/styles.css +++ b/packages/safe-ds-vscode/media/styles.css @@ -1,7 +1,7 @@ :root { --primary-color: #036ed1; --primary-color-desaturated: rgb(3 109 209 / 16.4%); - --error-color: #ce0003; + --error-color: #a01417; --bg-bright: white; --bg-dark: #f2f2f2; --bg-medium: #f9f9f9; 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 f989f8d39..b947c045b 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -78,6 +78,22 @@ export class RunnerApi { ); } + private sdsStringForHistogramByColumnName( + columnName: string, + tablePlaceholder: string, + newPlaceholderName: string, + ) { + return ( + 'val ' + + newPlaceholderName + + ' = ' + + tablePlaceholder + + '.get_column("' + + columnName + + '").plot_histogram(); \n' + ); + } + private randomPlaceholderName(): string { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const charactersLength = characters.length; @@ -177,6 +193,9 @@ export class RunnerApi { const columnNameToPlaceholderIDnessNameMap = new Map(); // Mapping random placeholder name for IDness back to column name const idnessMap = new Map(); // Saved by random placeholder name + const columnNameToPlaceholderHistogramNameMap = new Map(); // Mapping random placeholder name for histogram back to column name + const histogramMap = new Map(); // Saved by random placeholder name + // Generate SDS code to get missing value ratio for each column for (let i = 0; i < columns.length; i++) { const newMvPlaceholderName = this.randomPlaceholderName(); @@ -189,6 +208,16 @@ export class RunnerApi { newMvPlaceholderName, ); + const newHistogramPlaceholderName = this.randomPlaceholderName(); + histogramMap.set(newHistogramPlaceholderName, 'null'); + columnNameToPlaceholderHistogramNameMap.set(columns[i]![1].name, newHistogramPlaceholderName); + + sdsStrings += this.sdsStringForHistogramByColumnName( + columns[i]![1].name, + tableIdentifier, + newHistogramPlaceholderName, + ); + // Only need to check IDness for non-numerical columns if (columns[i]![1].type !== 'numerical') { const newIDnessPlaceholderName = this.randomPlaceholderName(); @@ -222,6 +251,14 @@ export class RunnerApi { } } + // // Get histogram for each column + // for (const [placeholderName] of histogramMap) { + // const histogram = await this.getPlaceholderValue(placeholderName, pipelineId); + // if (histogram) { + // histogramMap.set(placeholderName, histogram as string); + // } + // } + // Create profiling data, interpret numerical values and color them const profiling: { columnName: string; profiling: Profiling }[] = []; for (const column of columns) { @@ -286,9 +323,10 @@ export class RunnerApi { profiling: { top: [validRatio, missingRatio], bottom: [ + { type: 'name', name: 'Categorical', color: 'var(--font-dark)' }, { type: 'name', - name: uniqueValues + ' Unique
Strings', + name: uniqueValues + ' Uniques', color: 'var(--font-light)', }, ], From 6ee653ef1b07002a684c13bd255a354452af6151 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Tue, 5 Mar 2024 21:57:47 +0100 Subject: [PATCH 14/33] feat: filter and sort icons in col headers --- .../src/components/TableView.svelte | 54 ++++++++++++++++--- packages/safe-ds-eda/src/icons/Caret.svelte | 6 ++- packages/safe-ds-eda/src/icons/Filter.svelte | 10 ++++ packages/safe-ds-vscode/media/styles.css | 1 + 4 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 packages/safe-ds-eda/src/icons/Filter.svelte diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index c99d24389..0cdda23af 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -4,6 +4,7 @@ import { currentState, preventClicks } from '../webviewState'; import CaretIcon from '../icons/Caret.svelte'; import WarnIcon from '../icons/Warn.svelte'; + import FilterIcon from '../icons/Filter.svelte'; import type { Profiling, ProfilingDetailBase } from '../../types/state'; import ProfilingInfo from './profiling/ProfilingInfo.svelte'; @@ -627,11 +628,19 @@ on:mousedown={(event) => handleColumnInteractionStart(event, index)} on:mousemove={(event) => throttledHandleReorderDragOver(event, index)} >{column[1].name} +
+ +
+
+
+ +
+
+ +
+
-
startResizeDrag(event, index)} - >
+
startResizeDrag(event, index)}>
{/each}
{#each $currentState.table.columns as column, index} @@ -675,7 +684,7 @@
throttledHandleReorderDragOver(event, index)} > @@ -663,55 +641,7 @@ {#if column[1].profiling.top.length === 0 && column[1].profiling.bottom.length === 0}
Loading ...
{:else} -
- {#each column[1].profiling.top as profilingTopItem} - {#if profilingTopItem.type === 'name'} -
- {@html profilingTopItem.name} -
- {:else if profilingTopItem.type === 'numerical'} -
- {profilingTopItem.name}: - {profilingTopItem.value} -
- {/if} - {/each} -
-
- {#each column[1].profiling.bottom as profilingBottomItem} - {#if profilingBottomItem.type === 'name'} -
- {@html profilingBottomItem.name} -
- {:else if profilingBottomItem.type === 'numerical'} -
- {profilingBottomItem.name}: - {profilingBottomItem.value} -
- {/if} - {/each} -
+ {/if}
throttledHandleReorderDragOver(event, 0)} >
throttledHandleReorderDragOver(event, 0)} > + export let color = 'var(--font-light)'; + + - + diff --git a/packages/safe-ds-eda/src/icons/Filter.svelte b/packages/safe-ds-eda/src/icons/Filter.svelte new file mode 100644 index 000000000..6556842d7 --- /dev/null +++ b/packages/safe-ds-eda/src/icons/Filter.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/safe-ds-vscode/media/styles.css b/packages/safe-ds-vscode/media/styles.css index 12dcb4015..d5e1736b1 100644 --- a/packages/safe-ds-vscode/media/styles.css +++ b/packages/safe-ds-vscode/media/styles.css @@ -8,6 +8,7 @@ --font-dark: #292929; --font-light: #6d6d6d; --font-bright: #fff; + --transparent: #ffffffa1; } .noSelect { From d9ba845accc69c6e3d32279b2cf7e13bba5b517a Mon Sep 17 00:00:00 2001 From: Jonas B Date: Wed, 6 Mar 2024 22:10:38 +0100 Subject: [PATCH 15/33] feat: filter context menu start & fix: row/col select clear - filters: - for the filter to decide between: 1. search string 2. value range 3. distinct value, the tableView must decide for each column if numerical => value range or categorical => many values => search string OR little Values => distinct value - for that profiling for numerical items (that show % for example) now have new property "interpretation" which can be "warn" (for missing val), "category" (for this) or "default" - faster than iterating through categorical cols to count values for huge data, since we have this info on profiling generation already - whereas for value range find min max ourselves, since otherwise would need more in pipeline and more placeholder value queries, and this should be faster filters then in own component that decides what to show and calls vscode to initiate the runner code execution - as deliberated before kind of: selections are now not cleared by clicks anywhere anymore but only clicks on main cells, as the global window listener to clear was getting too convoluted, now don't need the rowClicked or columnClicked params anymore - preventClicks store is now set to false on context menu close with 100ms delay to allow time to prevent clicks - handleRightClickEnd decides per menu if to close and set those cleanup things or not, like for filter it doesn't if clicked anywhere in context menu by looping over html elements and their parents --- .../src/components/TableView.svelte | 225 +++++++++++++----- .../column-filters/ColumnFilters.svelte | 42 ++++ packages/safe-ds-eda/types/state.d.ts | 18 +- .../src/extension/eda/apis/runnerApi.ts | 3 + 4 files changed, 222 insertions(+), 66 deletions(-) create mode 100644 packages/safe-ds-eda/src/components/column-filters/ColumnFilters.svelte diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index 0cdda23af..c2e78518f 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -5,8 +5,14 @@ import CaretIcon from '../icons/Caret.svelte'; import WarnIcon from '../icons/Warn.svelte'; import FilterIcon from '../icons/Filter.svelte'; - import type { Profiling, ProfilingDetailBase } from '../../types/state'; + import type { + PossibleColumnFilter, + Profiling, + ProfilingDetailBase, + ProfilingDetailStatistical, + } from '../../types/state'; import ProfilingInfo from './profiling/ProfilingInfo.svelte'; + import ColumnFilters from './column-filters/ColumnFilters.svelte'; export let sidebarWidth: number; @@ -72,6 +78,13 @@ return baseWidth + Math.log(columnName.length + 1) * scale; }; + const handleMainCellClick = function (): void { + if (!$preventClicks) { + selectedColumnIndexes = []; + selectedRowIndexes = []; + } + }; + // --- Column resizing --- let isResizeDragging = false; let startWidth: number; @@ -95,7 +108,6 @@ const startResizeDrag = function (event: MouseEvent, columnIndex: number): void { event.stopPropagation(); - clickOnColumn = true; const columnElement = headerElements[columnIndex]; isResizeDragging = true; startX = event.clientX; @@ -120,7 +132,6 @@ let preventResizeTableSpaceUpdate = false; let holdTimeout: NodeJS.Timeout; let isClick = true; // Flag to distinguish between click and hold - let clickOnColumn = false; // For global window click clear of selection let currentMouseUpHandler: ((event: MouseEvent) => void) | null = null; // For being able to properly remove the mouseup listener when col clicked and not held @@ -140,8 +151,6 @@ // Check if the left or right mouse button was pressed if (event.button !== 0 && event.button !== 2) return; - clickOnColumn = true; // For global window click clear of selection - if (event.button === 2) { // Right click handleColumnRightClick(event, columnIndex); @@ -259,11 +268,19 @@ }; const addColumnToSelection = function (columnIndex: number): void { + if (selectedRowIndexes.length > 0) { + selectedRowIndexes = []; + } + // Add the index and create a new array to trigger reactivity selectedColumnIndexes = [...selectedColumnIndexes, columnIndex]; }; const removeColumnFromSelection = function (columnIndex: number, selectedColumnIndexesIndex?: number): void { + if (selectedRowIndexes.length > 0) { + selectedRowIndexes = []; + } + // Remove the index and create a new array to trigger reactivity selectedColumnIndexes = [ ...selectedColumnIndexes.slice(0, selectedColumnIndexesIndex ?? selectedColumnIndexes.indexOf(columnIndex)), @@ -274,13 +291,16 @@ }; const setSelectionToColumn = function (columnIndex: number): void { + if (selectedRowIndexes.length > 0) { + selectedRowIndexes = []; + } + // Replace the current selection with a new array to trigger reactivity selectedColumnIndexes = [columnIndex]; }; // --- Row selecting --- let selectedRowIndexes: number[] = []; - let clickOnRow = false; const handleRowClick = function (event: MouseEvent, rowIndex: number): void { // Logic for what happens when a row is clicked @@ -288,7 +308,9 @@ return; } - clickOnRow = true; // For global window click clear of selection + if (selectedColumnIndexes.length > 0) { + selectedColumnIndexes = []; + } // Check if Ctrl (or Cmd on Mac) is held down if (event.ctrlKey || event.metaKey) { @@ -414,10 +436,12 @@ } // --- Right clicks --- + let currentContextMenu: HTMLElement | null = null; + + // Column header right click let showingColumnHeaderRightClickMenu = false; let rightClickedColumnIndex = -1; let rightClickColumnMenuElement: HTMLElement; - let currentContextMenu: HTMLElement | null = null; const handleColumnRightClick = function (event: MouseEvent, columnIndex: number): void { // Logic for what happens when a header is right clicked @@ -431,25 +455,82 @@ rightClickColumnMenuElement!.style.top = event.clientY + scrollTop + 'px'; }); - // Click anywhere else to close the menu, context menu selection has to prevent propagation + // Click anywhere else to close the menu window.addEventListener('click', handleRightClickEnd); }; + // Filter context menu + let showingFilterContextMenu = false; + let filterColumnIndex = -1; + let filterContextMenuElement: HTMLElement; + + const handleFilterContextMenu = function (event: MouseEvent, columnIndex: number): void { + if (event.button !== 0) return; + + // Logic for what happens when a filter icon is clicked + event.stopPropagation(); + doDefaultContextMenuSetup(); + showingFilterContextMenu = true; + filterColumnIndex = columnIndex; + + requestAnimationFrame(() => { + currentContextMenu = filterContextMenuElement; // So scrolling can edit the position, somehow assignment does only work in requestAnimationFrame, maybe bc of delay, could lead to bugs maybe in future, keep note of + filterContextMenuElement!.style.left = event.clientX + tableContainer.scrollLeft - sidebarWidth + 'px'; + filterContextMenuElement!.style.top = event.clientY + scrollTop + 'px'; + }); + + // Click anywhere else to close the menu, if not clicked in the menu + window.addEventListener('mousedown', handleRightClickEnd); + }; + + // Scaling methods + const doDefaultContextMenuSetup = function (): void { preventClicks.set(true); disableNonContextMenuEffects(); }; - const handleRightClickEnd = function (): void { + const handleRightClickEnd = function (event: MouseEvent): void { + const generalCleanup = function (): void { + restoreNonContextMenuEffects(); + setTimeout(() => preventClicks.set(false), 100); // To give time for relevant click events to be prevented + currentContextMenu = null; + window.removeEventListener('click', handleRightClickEnd); + window.removeEventListener('mousedown', handleRightClickEnd); + }; + // Code specific to each menu - showingColumnHeaderRightClickMenu = false; - rightClickedColumnIndex = -1; - // ---- - - restoreNonContextMenuEffects(); - preventClicks.set(false); - currentContextMenu = null; - window.removeEventListener('click', handleRightClickEnd); + if (showingColumnHeaderRightClickMenu) { + showingColumnHeaderRightClickMenu = false; + rightClickedColumnIndex = -1; + generalCleanup(); + } + if (showingFilterContextMenu) { + if (event.target instanceof HTMLElement) { + let element = event.target; + + const hasParentWithClass = (element: HTMLElement, className: string) => { + while (element && element !== document.body) { + if (element.classList.contains(className)) { + return true; + } + if (!element.parentElement) { + return false; + } + element = element.parentElement; + } + return false; + }; + + // Check if the clicked element or any of its parents have the 'contextMenu' class + if (hasParentWithClass(element, 'contextMenu')) { + return; + } + } + showingFilterContextMenu = false; + filterColumnIndex = -1; + generalCleanup(); + } }; const originalHoverStyles = new Map(); @@ -528,14 +609,10 @@ } }; - const profilingWarnings = function (): boolean { + const hasProfilingWarnings = function (): boolean { for (const column of $currentState.table!.columns) { for (const profilingItem of column[1].profiling.top) { - if ( - profilingItem.type === 'numerical' && - profilingItem.name === 'Missing' && - parseFloat(profilingItem.value.replace('%', '')) > 0 - ) { + if (profilingItem.type === 'numerical' && profilingItem.interpretation === 'warn') { return true; } } @@ -543,45 +620,58 @@ return false; }; - // --- Lifecycle --- - let interval: NodeJS.Timeout; + const getPosiibleColumnFilters = function (columnIndex: number): PossibleColumnFilter[] { + if (!$currentState.table) return []; - const clearSelections = function (event: MouseEvent): void { - // Clears selections if last click was not on a column or row and currrent click is not on a context menu item if context menu is open - // WARN/TODO: Does not yet work for subemnus in context menus or menus with non possible closing clicks, those will need yet another class to be detected and handled - // This also prepares selection clearing for next iteration if click was on column or row - - // Clear column selection if approriate - if ( - !clickOnColumn && - !( - currentContextMenu && - event.target instanceof HTMLElement && - !(event.target as HTMLElement).classList.contains('contextItem') - ) - ) { - // Clear if click last item clicked was not on a column or if current click is on a context menu item if context menu is open, - // which should just close the context menu and not clear selection - selectedColumnIndexes = []; - } - clickOnColumn = false; // meaning if next click is not on a column, selection will be cleared in next iteration - - // Clear row selection if approriate - if ( - !clickOnRow && - !( - currentContextMenu && - event.target instanceof HTMLElement && - !(event.target as HTMLElement).classList.contains('contextItem') - ) - ) { - // Clear if click last item clicked was not on a row or if current click is on a context menu item if context menu is open, - // which should just close the context menu and not clear selection - selectedRowIndexes = []; + const column = $currentState.table.columns[columnIndex][1]; + + const possibleColumnFilters: PossibleColumnFilter[] = []; + + if (column.type === 'categorical') { + const profilingCategories: ProfilingDetailStatistical[] = column.profiling.bottom + .concat(column.profiling.top) + .filter( + (profilingItem) => + profilingItem.type === 'numerical' && profilingItem.interpretation === 'category', + ) as ProfilingDetailStatistical[]; + + // If there is distinct categories in profiling, use those as filter options, else use search string + if (profilingCategories.length > 0) { + possibleColumnFilters.push({ + type: 'specificValue', + values: profilingCategories.map((profilingItem) => profilingItem.value.replace('%', '')), + columnName: column.name, + }); + } else { + possibleColumnFilters.push({ + type: 'searchString', + columnName: column.name, + }); + } + } else { + const colMax = column.values.reduce( + (acc: number, val: number) => Math.max(acc, val), + Number.NEGATIVE_INFINITY, + ); + const colMin = column.values.reduce( + (acc: number, val: number) => Math.min(acc, val), + Number.NEGATIVE_INFINITY, + ); + + possibleColumnFilters.push({ + type: 'valueRange', + min: colMin, + max: colMax, + columnName: column.name, + }); } - clickOnRow = false; // meaning if next click is not on a row, selection will be cleared in next iteration + + return possibleColumnFilters; }; + // --- Lifecycle --- + let interval: NodeJS.Timeout; + onMount(() => { updateScrollTop(); recalculateVisibleRowCount(); @@ -589,7 +679,6 @@ tableContainer.addEventListener('scroll', updateScrollTop); window.addEventListener('resize', throttledRecalculateVisibleRowCount); window.addEventListener('resize', throttledUpdateTableSpace); - window.addEventListener('click', clearSelections); interval = setInterval(updateVisibleRows, 500); // To catch cases of fast scroll bar scrolling that leave table blank return () => { @@ -597,7 +686,6 @@ tableContainer.addEventListener('scroll', updateScrollTop); window.removeEventListener('resize', throttledRecalculateVisibleRowCount); window.removeEventListener('resize', throttledUpdateTableSpace); - window.removeEventListener('click', clearSelections); clearInterval(interval); }; }); @@ -628,7 +716,12 @@ on:mousedown={(event) => handleColumnInteractionStart(event, index)} on:mousemove={(event) => throttledHandleReorderDragOver(event, index)} >{column[1].name} -
+ + +
handleFilterContextMenu(event, index)} + >
@@ -695,7 +788,7 @@ >
{showProfiling ? 'Hide Profiling' : 'Show Profiling'} - {#if profilingWarnings()} + {#if hasProfilingWarnings()}
@@ -732,6 +825,7 @@ > {#each $currentState.table.columns as column, index}
throttledHandleReorderDragOver(event, index)} class:selectedColumn={selectedColumnIndexes.includes(index) || selectedRowIndexes.includes(visibleStart + i)} @@ -783,6 +877,11 @@ {/if} {/if} + {#if showingFilterContextMenu} +
+ +
+ {/if} diff --git a/packages/safe-ds-eda/types/state.d.ts b/packages/safe-ds-eda/types/state.d.ts index 0bfa36b3d..1c6dd7ce2 100644 --- a/packages/safe-ds-eda/types/state.d.ts +++ b/packages/safe-ds-eda/types/state.d.ts @@ -85,6 +85,7 @@ export interface ProfilingDetailStatistical extends ProfilingDetailBase { name: string; value: string; color?: string; + interpretation: 'warn' | 'category' | 'default'; } export interface ProfilingDetailImage extends ProfilingDetailBase { @@ -140,6 +141,10 @@ export interface SearchStringFilter extends ColumnFilterBase { searchString: string; } +export interface PossibleSearchStringFilter extends ColumnFilterBase { + type: 'searchString'; +} + export interface ValueRangeFilter extends ColumnFilterBase { type: 'valueRange'; min: number; @@ -148,11 +153,18 @@ export interface ValueRangeFilter extends ColumnFilterBase { export interface SpecificValueFilter extends ColumnFilterBase { type: 'specificValue'; - value: number; + value: string; +} + +export interface PossibleSpecificValueFilter extends ColumnFilterBase { + type: 'specificValue'; + values: string[]; } -export type NumericalFilter = ValueRangeFilter | SpecificValueFilter; -export type CategoricalFilter = SearchStringFilter; +export type NumericalFilter = ValueRangeFilter; +export type CategoricalFilter = SearchStringFilter | SpecificValueFilter; + +export type PossibleColumnFilter = ValueRangeFilter | PossibleSearchStringFilter | PossibleSpecificValueFilter; export interface TableFilter extends FilterBase { type: 'hideMissingValueColumns' | 'hideNonNumericalColumns' | 'hideDuplicateRows' | 'hideRowsWithOutliers'; 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 b947c045b..6f214fcc8 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -270,6 +270,7 @@ export class RunnerApi { name: 'Valid', value: missingValuesRatio ? (100 - missingValuesRatio).toFixed(2) + '%' : '100%', color: 'var(--primary-color)', + interpretation: 'default', }; const missingRatio: ProfilingDetailStatistical = { @@ -277,6 +278,7 @@ export class RunnerApi { name: 'Missing', value: missingValuesRatio ? missingValuesRatio.toFixed(2) + '%' : '0%', color: missingValuesRatio > 0 ? 'var(--error-color)' : 'var(--font-light)', + interpretation: missingValuesRatio > 0 ? 'warn' : 'default', }; // If not numerical, add proper profilings according to idness results @@ -303,6 +305,7 @@ export class RunnerApi { name: key, value: ((value / column[1].values.length) * 100).toFixed(2) + '%', color: 'var(--font-light)', + interpretation: 'category', }); } From 6237bfcc88ca52579d5ff7d0e9e3718e3c54ea13 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Thu, 7 Mar 2024 22:46:43 +0100 Subject: [PATCH 16/33] fix: multiple pipeline support & refactoring - pipelineId renamed to piprlineExecutionId for more sense multiple pipelines: - eda from context now gets exact ast node of placeholder with range of the executed context and from there the pipeline container => pipeline name - pipeline name is passed to execute pipeline which is now needed - also sent to eda where 1. it is used for tableIdentifier as pipelineName + '.' + tableName (tableName new param used and passed outside of tableIdentifier) and 2. it is passed to runner for pipeline execution - in Runner the pipeline in question is found and in front of it's closing } the new code is then added - getStateByPlaceholder now getTableByPlaceholder and transform to state obj in eda class, whee table name and table identifier are known, this method now only relevant stuff - createOrShow async as well as calling register command methods - runnerApi now instance var of panel --- packages/safe-ds-eda/src/webviewState.ts | 2 +- packages/safe-ds-eda/types/globals.d.ts | 1 - .../src/extension/eda/apis/runnerApi.ts | 124 ++++++----- .../src/extension/eda/edaPanel.ts | 202 ++++++++++-------- .../src/extension/mainClient.ts | 152 +++++++++---- 5 files changed, 291 insertions(+), 190 deletions(-) diff --git a/packages/safe-ds-eda/src/webviewState.ts b/packages/safe-ds-eda/src/webviewState.ts index a0a4744e2..a19d1459d 100644 --- a/packages/safe-ds-eda/src/webviewState.ts +++ b/packages/safe-ds-eda/src/webviewState.ts @@ -8,7 +8,7 @@ let currentTabIndex = writable(0); let preventClicks = writable(false); // Define the stores, current state to default in case the extension never calls setWebviewState( Shouldn't happen) -let currentState = writable({ tableIdentifier: window.tableIdentifier, history: [], defaultState: true }); +let currentState = writable({ tableIdentifier: undefined, history: [], defaultState: true }); // Set Global states whenever updatedAllStates changes currentState.subscribe(($currentState) => { diff --git a/packages/safe-ds-eda/types/globals.d.ts b/packages/safe-ds-eda/types/globals.d.ts index 881c89e61..d78d1ed9d 100644 --- a/packages/safe-ds-eda/types/globals.d.ts +++ b/packages/safe-ds-eda/types/globals.d.ts @@ -5,6 +5,5 @@ declare global { injVscode: { postMessage: (message: ToExtensionMessage) => void; }; - tableIdentifier: string; } } 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 6f214fcc8..da1bdfa40 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -1,50 +1,67 @@ -import { Column, Profiling, ProfilingDetailStatistical, State, Table } from '@safe-ds/eda/types/state.js'; +import { Column, Profiling, ProfilingDetailStatistical, Table } from '@safe-ds/eda/types/state.js'; import { SafeDsServices, messages } from '@safe-ds/lang'; import { printOutputMessage } from '../../output.ts'; import * as vscode from 'vscode'; -import { getPipelineDocument } from '../../mainClient.ts'; import crypto from 'crypto'; +import { getPipelineDocument } from '../../mainClient.ts'; export class RunnerApi { services: SafeDsServices; pipelinePath: vscode.Uri; + pipelineName: string; - constructor(services: SafeDsServices, pipelinePath: vscode.Uri) { + constructor(services: SafeDsServices, pipelinePath: vscode.Uri, pipelineName: string) { this.services = services; this.pipelinePath = pipelinePath; + this.pipelineName = pipelineName; } - private async addToAndExecutePipeline(pipelineId: string, addedLines: string): Promise { + private async addToAndExecutePipeline(pipelineExecutionId: string, addedLines: string): Promise { return new Promise(async (resolve, reject) => { const baseDocument = await getPipelineDocument(this.pipelinePath); - if (baseDocument) { - const documentText = baseDocument.textDocument.getText(); - const lastBracket = documentText.lastIndexOf('}'); - - const newText = documentText.slice(0, lastBracket) + addedLines + documentText.slice(lastBracket); - const newDoc = this.services.shared.workspace.LangiumDocumentFactory.fromString( - newText, - this.pipelinePath, - ); - await this.services.runtime.Runner.executePipeline(newDoc, pipelineId); + if (!baseDocument) { + reject('Pipeline not found'); + return; + } - const runtimeCallback = (message: messages.RuntimeProgressMessage) => { - if (message.id !== pipelineId) { - return; - } - if (message.data === 'done') { - this.services.runtime.Runner.removeMessageCallback(runtimeCallback, 'runtime_progress'); - resolve(); - } - }; - this.services.runtime.Runner.addMessageCallback(runtimeCallback, 'runtime_progress'); + const documentText = baseDocument.textDocument.getText(); - setTimeout(() => { - reject('Pipeline execution timed out'); - }, 30000); - } else { - reject('Could not find pipeline document'); + // Find pattern "pipeline {" and add the SDS code before the closing bracket of it + const pipelineStart = documentText.indexOf('pipeline ' + this.pipelineName); + if (pipelineStart === -1) { + reject('Pipeline not found'); + return; } + const nextCurlyBraceEnd = documentText.indexOf('}', pipelineStart); + if (nextCurlyBraceEnd === -1) { + reject('Internal error: Pipeline end not found'); + return; + } + + const beforeClosingBrace = documentText.substring(0, nextCurlyBraceEnd); + const afterClosingBrace = documentText.substring(nextCurlyBraceEnd); + const newDocumentText = beforeClosingBrace + addedLines + afterClosingBrace; + + const newDoc = this.services.shared.workspace.LangiumDocumentFactory.fromString( + newDocumentText, + this.pipelinePath, + ); + await this.services.runtime.Runner.executePipeline(pipelineExecutionId, newDoc, this.pipelineName); + + const runtimeCallback = (message: messages.RuntimeProgressMessage) => { + if (message.id !== pipelineExecutionId) { + return; + } + if (message.data === 'done') { + this.services.runtime.Runner.removeMessageCallback(runtimeCallback, 'runtime_progress'); + resolve(); + } + }; + this.services.runtime.Runner.addMessageCallback(runtimeCallback, 'runtime_progress'); + + setTimeout(() => { + reject('Pipeline execution timed out'); + }, 30000); }); } @@ -95,7 +112,7 @@ export class RunnerApi { } private randomPlaceholderName(): string { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; // __gen, code gen prefix (konstante importieren) const charactersLength = characters.length; const randomArray = new Uint8Array(20); crypto.getRandomValues(randomArray); @@ -106,14 +123,14 @@ export class RunnerApi { return result; } - private async getPlaceholderValue(placeholder: string, pipelineId: string): Promise { + private async getPlaceholderValue(placeholder: string, pipelineExecutionId: string): Promise { return new Promise((resolve) => { if (placeholder === '') { resolve(undefined); } const placeholderValueCallback = (message: messages.PlaceholderValueMessage) => { - if (message.id !== pipelineId || message.data.name !== placeholder) { + if (message.id !== pipelineExecutionId || message.data.name !== placeholder) { return; } this.services.runtime.Runner.removeMessageCallback(placeholderValueCallback, 'placeholder_value'); @@ -123,7 +140,7 @@ export class RunnerApi { this.services.runtime.Runner.addMessageCallback(placeholderValueCallback, 'placeholder_value'); printOutputMessage('Getting placeholder from Runner ...'); this.services.runtime.Runner.sendMessageToPythonServer( - messages.createPlaceholderQueryMessage(pipelineId, placeholder), + messages.createPlaceholderQueryMessage(pipelineExecutionId, placeholder), ); setTimeout(() => { @@ -134,12 +151,12 @@ export class RunnerApi { // --- Public API --- - public async getStateByPlaceholder(tableIdentifier: string, pipelineId: string): Promise { - const pythonTableColumns = await this.getPlaceholderValue(tableIdentifier, pipelineId); + public async getTableByPlaceholder(tableName: string, pipelineExecutionId: string): Promise { + const pythonTableColumns = await this.getPlaceholderValue(tableName, pipelineExecutionId); if (pythonTableColumns) { const table: Table = { totalRows: 0, - name: tableIdentifier, + name: tableName, columns: [] as Table['columns'], appliedFilters: [] as Table['appliedFilters'], }; @@ -173,16 +190,13 @@ export class RunnerApi { table.totalRows = currentMax; table.visibleRows = currentMax; - return { tableIdentifier, history: [], defaultState: false, table }; + return table; } else { return undefined; } } - public async getProfiling( - tableIdentifier: string, - table: Table, - ): Promise<{ columnName: string; profiling: Profiling }[]> { + public async getProfiling(table: Table): Promise<{ columnName: string; profiling: Profiling }[]> { const columns = table.columns; let sdsStrings = ''; @@ -204,7 +218,7 @@ export class RunnerApi { sdsStrings += this.sdsStringForMissingValueRatioByColumnName( columns[i]![1].name, - tableIdentifier, + table.name, newMvPlaceholderName, ); @@ -214,7 +228,7 @@ export class RunnerApi { sdsStrings += this.sdsStringForHistogramByColumnName( columns[i]![1].name, - tableIdentifier, + table.name, newHistogramPlaceholderName, ); @@ -225,19 +239,19 @@ export class RunnerApi { columnNameToPlaceholderIDnessNameMap.set(columns[i]![1].name, newIDnessPlaceholderName); sdsStrings += this.sdsStringForIDnessByColumnName( columns[i]![1].name, - tableIdentifier, + table.name, newIDnessPlaceholderName, ); } } // Execute with generated SDS code - const pipelineId = crypto.randomUUID(); - await this.addToAndExecutePipeline(pipelineId, sdsStrings); + const pipelineExecutionId = crypto.randomUUID(); + await this.addToAndExecutePipeline(pipelineExecutionId, sdsStrings); // Get missing value ratio for each column for (const [placeholderName] of missingValueRatioMap) { - const missingValueRatio = await this.getPlaceholderValue(placeholderName, pipelineId); + const missingValueRatio = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); if (missingValueRatio) { missingValueRatioMap.set(placeholderName, missingValueRatio as string); } @@ -245,7 +259,7 @@ export class RunnerApi { // Get IDness for each column for (const [placeholderName] of idnessMap) { - const idness = await this.getPlaceholderValue(placeholderName, pipelineId); + const idness = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); if (idness) { idnessMap.set(placeholderName, idness as number); } @@ -253,7 +267,7 @@ export class RunnerApi { // // Get histogram for each column // for (const [placeholderName] of histogramMap) { - // const histogram = await this.getPlaceholderValue(placeholderName, pipelineId); + // const histogram = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); // if (histogram) { // histogramMap.set(placeholderName, histogram as string); // } @@ -353,9 +367,9 @@ export class RunnerApi { public async getColumnNames(tableIdentifier: string): Promise { const newPlaceholderName = this.randomPlaceholderName(); const columnNamesSdsCode = this.sdsStringForColumnNames(tableIdentifier, newPlaceholderName); - const pipelineId = crypto.randomUUID(); - await this.addToAndExecutePipeline(pipelineId, columnNamesSdsCode); - const columnNames = await this.getPlaceholderValue(newPlaceholderName, pipelineId); + const pipelineExecutionId = crypto.randomUUID(); + await this.addToAndExecutePipeline(pipelineExecutionId, columnNamesSdsCode); + const columnNames = await this.getPlaceholderValue(newPlaceholderName, pipelineExecutionId); return columnNames as string[]; } @@ -363,9 +377,9 @@ export class RunnerApi { // public async getColumns(tableIdentifier: string): Promise<{ columns: any; placeholderName: string }> { // const newPlaceholderName = this.randomPlaceholderName(); // const columnsSdsCode = this.sdsStringForColumns(tableIdentifier, newPlaceholderName); - // const pipelineId = crypto.randomUUID(); - // await this.addToAndExecutePipeline(pipelineId, columnsSdsCode); - // const columns = await this.getPlaceholderValue(newPlaceholderName, pipelineId); + // const pipelineExecutionId = crypto.randomUUID(); + // await this.addToAndExecutePipeline(pipelineExecutionId, columnsSdsCode); + // const columns = await this.getPlaceholderValue(newPlaceholderName, pipelineExecutionId); // // eslint-disable-next-line no-console // console.log(columns); // return { columns, placeholderName: newPlaceholderName }; diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index 4fb60f6c3..b148e8ab0 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -8,7 +8,7 @@ import { RunnerApi } from './apis/runnerApi.ts'; export class EDAPanel { // Map to track multiple panels - private static instancesMap: Map = new Map(); + private static panelsMap: Map = new Map(); private static context: vscode.ExtensionContext; private static services: SafeDsServices; @@ -18,22 +18,28 @@ export class EDAPanel { private readonly extensionUri: vscode.Uri; private disposables: vscode.Disposable[] = []; private tableIdentifier: string; + private tableName: string; private column: vscode.ViewColumn | undefined; private webviewListener: vscode.Disposable | undefined; private viewStateChangeListener: vscode.Disposable | undefined; private updateHtmlDone: boolean = false; - private startPipelineId: string; + private startPipelineExecutionId: string; + private runnerApi: RunnerApi; private constructor( panel: vscode.WebviewPanel, extensionUri: vscode.Uri, - startPipeLineId: string, - tableIdentifier: string, + startPipelineExecutionId: string, + pipelinePath: vscode.Uri, + pipelineName: string, + tableName: string, ) { + this.tableIdentifier = pipelineName + '.' + tableName; this.panel = panel; this.extensionUri = extensionUri; - this.tableIdentifier = tableIdentifier; - this.startPipelineId = startPipeLineId; + this.startPipelineExecutionId = startPipelineExecutionId; + this.runnerApi = new RunnerApi(EDAPanel.services, pipelinePath, pipelineName); + this.tableName = tableName; // Set the webview's initial html content this.updateHtmlDone = false; @@ -96,55 +102,59 @@ export class EDAPanel { this.disposables.push(this.webviewListener); } - public static createOrShow( + public static async createOrShow( extensionUri: vscode.Uri, context: vscode.ExtensionContext, - startPipelineId: string, + startPipelineExecutionId: string, services: SafeDsServices, - tableIdentifier: string, pipelinePath: vscode.Uri, - ) { + pipelineName: string, + tableName: string, + ): Promise { EDAPanel.context = context; EDAPanel.services = services; + let tableIdentifier = pipelineName + '.' + tableName; + // Set column to the active editor if it exists const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; // If we already have a panel, show it. - let instance = EDAPanel.instancesMap.get(tableIdentifier); - if (instance) { - let panel = instance.panel; + let panel = EDAPanel.panelsMap.get(tableIdentifier); + if (panel) { panel.panel.reveal(panel.column); panel.tableIdentifier = tableIdentifier; - panel.startPipelineId = startPipelineId; - EDAPanel.instancesMap.set(tableIdentifier, { panel, runnerApi: new RunnerApi(services, pipelinePath) }); + panel.startPipelineExecutionId = startPipelineExecutionId; + panel.runnerApi = new RunnerApi(services, pipelinePath, pipelineName); + panel.tableName = tableName; + EDAPanel.panelsMap.set(tableIdentifier, panel); // Have to update and construct state as table placeholder could've changed in code panel.updateHtmlDone = false; panel._update(); - panel.waitForUpdateHtmlDone(10000).then(() => - panel.constructCurrentState().then((stateInfo) => { - webviewApi.postMessage(panel!.panel.webview, { - command: 'setWebviewState', - value: stateInfo.state, - }); - - if ( - !stateInfo.fromExisting || - !stateInfo.state.table || - !stateInfo.state - .table!.columns.map((c) => c[1].profiling) - .find((p) => p.bottom.length > 0 || p.top.length > 0) - ) { - instance!.runnerApi.getProfiling(tableIdentifier, stateInfo.state.table!).then((profiling) => { - webviewApi.postMessage(panel!.panel.webview, { - command: 'setProfiling', - value: profiling, - }); - }); - } - }), - ); + await panel.waitForUpdateHtmlDone(10000); + + // Get and send state + const stateInfo = await panel.constructCurrentState(); + webviewApi.postMessage(panel!.panel.webview, { + command: 'setWebviewState', + value: stateInfo.state, + }); + + // If not present, get and send profiling + if ( + !stateInfo.fromExisting || + !stateInfo.state.table || + !stateInfo.state + .table!.columns.map((c) => c[1].profiling) + .find((p) => p.bottom.length > 0 || p.top.length > 0) + ) { + const profiling = await panel.runnerApi.getProfiling(stateInfo.state.table!); + webviewApi.postMessage(panel!.panel.webview, { + command: 'setProfiling', + value: profiling, + }); + } return; } else { // Otherwise, create a new panel. @@ -164,56 +174,57 @@ export class EDAPanel { }, ); - const edaPanel = new EDAPanel(newPanel, extensionUri, startPipelineId, tableIdentifier); - EDAPanel.instancesMap.set(tableIdentifier, { - panel: edaPanel, - runnerApi: new RunnerApi(services, pipelinePath), - }); + const edaPanel = new EDAPanel( + newPanel, + extensionUri, + startPipelineExecutionId, + pipelinePath, + pipelineName, + tableName, + ); + EDAPanel.panelsMap.set(tableIdentifier, edaPanel); edaPanel.column = column; edaPanel.panel.iconPath = { light: vscode.Uri.joinPath(edaPanel.extensionUri, 'img', 'binoculars-solid.png'), dark: vscode.Uri.joinPath(edaPanel.extensionUri, 'img', 'binoculars-solid.png'), }; - edaPanel.waitForUpdateHtmlDone(10000).then(() => - edaPanel.constructCurrentState().then((stateInfo) => { - webviewApi.postMessage(edaPanel!.panel.webview, { - command: 'setWebviewState', - value: stateInfo.state, - }); - - if ( - !stateInfo.fromExisting || - !stateInfo.state.table || - !stateInfo.state - .table!.columns.map((c) => c[1].profiling) - .find((p) => p.bottom.length > 0 || p.top.length > 0) - ) { - EDAPanel.instancesMap - .get(tableIdentifier)! - .runnerApi.getProfiling(tableIdentifier, stateInfo.state.table!) - .then((profiling) => { - webviewApi.postMessage(edaPanel!.panel.webview, { - command: 'setProfiling', - value: profiling, - }); - }); - } - }), - ); + await edaPanel.waitForUpdateHtmlDone(10000); + const stateInfo = await edaPanel.constructCurrentState(); + webviewApi.postMessage(edaPanel!.panel.webview, { + command: 'setWebviewState', + value: stateInfo.state, + }); + + if ( + !stateInfo.fromExisting || + !stateInfo.state.table || + !stateInfo.state + .table!.columns.map((c) => c[1].profiling) + .find((p) => p.bottom.length > 0 || p.top.length > 0) + ) { + const profiling = await EDAPanel.panelsMap + .get(tableIdentifier)! + .runnerApi.getProfiling(stateInfo.state.table!); + + webviewApi.postMessage(edaPanel!.panel.webview, { + command: 'setProfiling', + value: profiling, + }); + } } } public static kill(tableIdentifier: string) { printOutputMessage('kill ' + tableIdentifier); - let instance = EDAPanel.instancesMap.get(tableIdentifier); - if (instance) { - instance.panel.dispose(); - EDAPanel.instancesMap.delete(tableIdentifier); + let panel = EDAPanel.panelsMap.get(tableIdentifier); + if (panel) { + panel.panel.dispose(); + EDAPanel.panelsMap.delete(tableIdentifier); } } public dispose() { - EDAPanel.instancesMap.delete(this.tableIdentifier); + EDAPanel.panelsMap.delete(this.tableIdentifier); // Clean up our panel this.panel.dispose(); @@ -255,28 +266,32 @@ export class EDAPanel { return existingStates.find((s) => s.tableIdentifier === this.tableIdentifier); } - private constructCurrentState(): Promise<{ state: State; fromExisting: boolean }> { - return new Promise((resolve, reject) => { - const existingCurrentState = this.findCurrentState(); - if (existingCurrentState) { - printOutputMessage('Found current State.'); - resolve({ state: existingCurrentState, fromExisting: true }); - return; - } + private async constructCurrentState(): Promise<{ state: State; fromExisting: boolean }> { + const existingCurrentState = this.findCurrentState(); + if (existingCurrentState) { + printOutputMessage('Found current State.'); + return { state: existingCurrentState, fromExisting: true }; + } - const instance = EDAPanel.instancesMap.get(this.tableIdentifier); - if (!instance) { - reject(new Error('RunnerApi instance not found.')); + const panel = EDAPanel.panelsMap.get(this.tableIdentifier); + if (!panel) { + throw new Error('RunnerApi panel not found.'); + } else { + const table = await panel.runnerApi.getTableByPlaceholder(this.tableName, this.startPipelineExecutionId); + if (!table) { + throw new Error('Timeout waiting for placeholder value'); } else { - instance.runnerApi.getStateByPlaceholder(this.tableIdentifier, this.startPipelineId).then((state) => { - if (state === undefined) { - reject(new Error('Timeout waiting for placeholder value')); - } else { - resolve({ state, fromExisting: false }); - } - }); + return { + state: { + tableIdentifier: panel.tableIdentifier, + history: [], + defaultState: false, + table, + }, + fromExisting: false, + }; } - }); + } } private async _getHtmlForWebview(webview: vscode.Webview) { @@ -318,7 +333,6 @@ export class EDAPanel { diff --git a/packages/safe-ds-vscode/src/extension/mainClient.ts b/packages/safe-ds-vscode/src/extension/mainClient.ts index ae27067dd..85346f835 100644 --- a/packages/safe-ds-vscode/src/extension/mainClient.ts +++ b/packages/safe-ds-vscode/src/extension/mainClient.ts @@ -6,15 +6,16 @@ import { ast, createSafeDsServices, getModuleMembers, messages, SafeDsServices } import { NodeFileSystem } from 'langium/node'; import { getSafeDSOutputChannel, initializeLog, logError, logOutput, printOutputMessage } from './output.js'; import crypto from 'crypto'; -import { LangiumDocument, URI } from 'langium'; +import { LangiumDocument, URI, AstUtils, AstNode } from 'langium'; import { EDAPanel } from './eda/edaPanel.ts'; import { dumpDiagnostics } from './commands/dumpDiagnostics.js'; import { openDiagnosticsDumps } from './commands/openDiagnosticsDumps.js'; let client: LanguageClient; let services: SafeDsServices; -let lastFinishedPipelineId: string | undefined; -let lastSuccessfulPlaceholderName: string | undefined; +let lastFinishedPipelineExecutionId: string | undefined; +let lastSuccessfulPipelineName: string | undefined; +let lastSuccessfulTableName: string | undefined; let lastSuccessfulPipelinePath: vscode.Uri | undefined; // This function is called when the extension is activated. @@ -87,7 +88,7 @@ const startLanguageClient = function (context: vscode.ExtensionContext): Languag return result; }; -const acceptRunRequests = function (context: vscode.ExtensionContext) { +const acceptRunRequests = async function (context: vscode.ExtensionContext) { // Register logging message callbacks registerMessageLoggingCallbacks(); // Register VS Code Entry Points @@ -146,7 +147,7 @@ const registerMessageLoggingCallbacks = function () { }; const registerVSCodeCommands = function (context: vscode.ExtensionContext) { - const registerCommandWithCheck = (commandId: string, callback: (...args: any[]) => any) => { + const registerCommandWithCheck = (commandId: string, callback: (...args: any[]) => Promise) => { return vscode.commands.registerCommand(commandId, (...args: any[]) => { if (!services.runtime.Runner.isPythonServerAvailable()) { vscode.window.showErrorMessage('Extension not fully started yet.'); @@ -164,7 +165,7 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand('safe-ds.runPipelineFile', commandRunPipelineFile)); context.subscriptions.push( - registerCommandWithCheck('safe-ds.runEdaFromContext', () => { + registerCommandWithCheck('safe-ds.runEdaFromContext', async () => { const editor = vscode.window.activeTextEditor; if (editor) { const position = editor.selection.active; @@ -179,8 +180,59 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { vscode.window.showErrorMessage('No .sdspipe file selected!'); return; } + + // Getting of pipeline name + const document = await getPipelineDocument(editor.document.uri); + if (!document) { + vscode.window.showErrorMessage('Internal error!'); + return; + } + const module = document.parseResult.value as ast.SdsModule; + + type CustomRangeType = { + start: { line: number; character: number }; + end: { line: number; character: number }; + }; + const isRangeEqual = function (lhs: CustomRangeType, rhs: CustomRangeType): boolean { + return ( + lhs.start.character === rhs.start.character && + lhs.start.line === rhs.start.line && + lhs.end.character === rhs.end.character && + lhs.end.line === rhs.end.line + ); + }; + + // Find node of placeholder + let placeholderNode: AstNode | undefined; + for (const node of AstUtils.streamAllContents(module)) { + // Entire node matches the range + const actualRange = node.$cstNode?.range; + if (actualRange && isRangeEqual(actualRange, range)) { + placeholderNode = node; + } + + // The node has a name node that matches the range + const actualNameRange = services.references.NameProvider.getNameNode(node)?.range; + if (actualNameRange && isRangeEqual(actualNameRange, range)) { + placeholderNode = node; + } + } + if (!placeholderNode) { + vscode.window.showErrorMessage('Internal error!'); + return; + } + + // Get pipeline container + const container = AstUtils.getContainerOfType(placeholderNode, ast.isSdsPipeline); + if (!container) { + vscode.window.showErrorMessage('Internal error!'); + return; + } + + const pipelineName = container?.name; + // gen custom id for pipeline - const pipelineId = crypto.randomUUID(); + const pipelineExecutionId = crypto.randomUUID(); let loadingInProgress = true; // Flag to track loading status // Show progress indicator @@ -211,27 +263,32 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { `Placeholder was calculated (${message.id}): ${message.data.name} of type ${message.data.type}`, ); if ( - message.id === pipelineId && + message.id === pipelineExecutionId && message.data.type === 'Table' && message.data.name === requestedPlaceholderName ) { - lastFinishedPipelineId = pipelineId; + lastFinishedPipelineExecutionId = pipelineExecutionId; lastSuccessfulPipelinePath = editor.document.uri; - lastSuccessfulPlaceholderName = requestedPlaceholderName; + lastSuccessfulTableName = requestedPlaceholderName; + lastSuccessfulPipelineName = pipelineName; EDAPanel.createOrShow( context.extensionUri, context, - pipelineId, + pipelineExecutionId, services, - message.data.name, editor.document.uri, + pipelineName, + message.data.name, ); services.runtime.Runner.removeMessageCallback(placeholderTypeCallback, 'placeholder_type'); cleanupLoadingIndication(); - } else if (message.id === pipelineId && message.data.name !== requestedPlaceholderName) { + } else if ( + message.id === pipelineExecutionId && + message.data.name !== requestedPlaceholderName + ) { return; - } else if (message.id === pipelineId) { - lastFinishedPipelineId = pipelineId; + } else if (message.id === pipelineExecutionId) { + lastFinishedPipelineExecutionId = pipelineExecutionId; vscode.window.showErrorMessage(`Selected placeholder is not of type 'Table'.`); services.runtime.Runner.removeMessageCallback(placeholderTypeCallback, 'placeholder_type'); cleanupLoadingIndication(); @@ -242,11 +299,11 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { const runtimeProgressCallback = function (message: messages.RuntimeProgressMessage) { printOutputMessage(`Runner-Progress (${message.id}): ${message.data}`); if ( - message.id === pipelineId && + message.id === pipelineExecutionId && message.data === 'done' && - lastFinishedPipelineId !== pipelineId + lastFinishedPipelineExecutionId !== pipelineExecutionId ) { - lastFinishedPipelineId = pipelineId; + lastFinishedPipelineExecutionId = pipelineExecutionId; vscode.window.showErrorMessage(`Selected text is not a placeholder!`); services.runtime.Runner.removeMessageCallback(runtimeProgressCallback, 'runtime_progress'); cleanupLoadingIndication(); @@ -255,8 +312,11 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { services.runtime.Runner.addMessageCallback(runtimeProgressCallback, 'runtime_progress'); const runtimeErrorCallback = function (message: messages.RuntimeErrorMessage) { - if (message.id === pipelineId && lastFinishedPipelineId !== pipelineId) { - lastFinishedPipelineId = pipelineId; + if ( + message.id === pipelineExecutionId && + lastFinishedPipelineExecutionId !== pipelineExecutionId + ) { + lastFinishedPipelineExecutionId = pipelineExecutionId; vscode.window.showErrorMessage(`Pipeline ran into an Error!`); services.runtime.Runner.removeMessageCallback(runtimeErrorCallback, 'runtime_error'); cleanupLoadingIndication(); @@ -264,7 +324,7 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { }; services.runtime.Runner.addMessageCallback(runtimeErrorCallback, 'runtime_error'); - runPipelineFile(editor.document.uri, pipelineId); + runPipelineFile(editor.document.uri, pipelineExecutionId, pipelineName, requestedPlaceholderName); } else { vscode.window.showErrorMessage('No placeholder selected!'); } @@ -277,19 +337,25 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('safe-ds.refreshWebview', () => { - if (!lastSuccessfulPipelinePath || !lastFinishedPipelineId || !lastSuccessfulPlaceholderName) { + if ( + !lastSuccessfulPipelinePath || + !lastFinishedPipelineExecutionId || + !lastSuccessfulPipelineName || + !lastSuccessfulTableName + ) { vscode.window.showErrorMessage('No EDA Panel to refresh!'); return; } - EDAPanel.kill(lastSuccessfulPlaceholderName); + EDAPanel.kill(lastSuccessfulPipelineName!); setTimeout(() => { EDAPanel.createOrShow( context.extensionUri, context, - lastFinishedPipelineId!, + lastFinishedPipelineExecutionId!, services, - lastSuccessfulPlaceholderName!, lastSuccessfulPipelinePath!, + lastSuccessfulPipelineName!, + lastSuccessfulTableName!, ); }, 100); setTimeout(() => { @@ -299,14 +365,32 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { ); }; -const runPipelineFile = async function (filePath: vscode.Uri | undefined, pipelineId: string) { +const runPipelineFile = async function ( + filePath: vscode.Uri | undefined, + pipelineExecutionId: string, + knownPipelineName?: string, + placeholderName?: string, +) { const document = await getPipelineDocument(filePath); if (document) { // Run it - printOutputMessage(`Launching Pipeline (${pipelineId}): ${document?.uri.toString()}`); + let pipelineName; + if (!knownPipelineName) { + const firstPipeline = getModuleMembers(document.parseResult.value).find(ast.isSdsPipeline); + if (firstPipeline === undefined) { + logError('Cannot execute: no pipeline found'); + vscode.window.showErrorMessage('The current file cannot be executed, as no pipeline could be found.'); + return; + } + pipelineName = services.builtins.Annotations.getPythonName(firstPipeline) || firstPipeline.name; + } else { + pipelineName = knownPipelineName; + } + + printOutputMessage(`Launching Pipeline (${pipelineExecutionId}): ${filePath} - ${pipelineName}`); - await services.runtime.Runner.executePipeline(document, pipelineId); + await services.runtime.Runner.executePipeline(pipelineExecutionId, document, pipelineName, placeholderName); } }; @@ -368,17 +452,7 @@ export const getPipelineDocument = async function ( mainDocument = await services.shared.workspace.LangiumDocuments.getOrCreateDocument(pipelinePath); } - const firstPipeline = getModuleMembers(mainDocument.parseResult.value).find(ast.isSdsPipeline); - if (firstPipeline === undefined) { - logError('Cannot execute: no pipeline found'); - vscode.window.showErrorMessage('The current file cannot be executed, as no pipeline could be found.'); - return; - } - const mainPipelineName = services.builtins.Annotations.getPythonName(firstPipeline) || firstPipeline.name; - - printOutputMessage(`Launching Pipeline (${pipelineId}): ${pipelinePath} - ${mainPipelineName}`); - - await services.runtime.Runner.executePipeline(pipelineId, mainDocument, mainPipelineName); + return mainDocument; }; const commandRunPipelineFile = async function (filePath: vscode.Uri | undefined) { From 8350e3c40a21234c29398dc9a9d34aaf0888df17 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Thu, 7 Mar 2024 23:01:19 +0100 Subject: [PATCH 17/33] feat: CODEGEN_PREFIX in front of generated placeholders --- packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 da1bdfa40..b2cd0ab98 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -4,6 +4,7 @@ import { printOutputMessage } from '../../output.ts'; import * as vscode from 'vscode'; import crypto from 'crypto'; import { getPipelineDocument } from '../../mainClient.ts'; +import { CODEGEN_PREFIX } from '../../../../../safe-ds-lang/src/language/generation/safe-ds-python-generator.ts'; export class RunnerApi { services: SafeDsServices; @@ -120,7 +121,7 @@ export class RunnerApi { randomArray.forEach((value) => { result += characters.charAt(value % charactersLength); }); - return result; + return CODEGEN_PREFIX + result; } private async getPlaceholderValue(placeholder: string, pipelineExecutionId: string): Promise { From 5a532c9be5e161bab1df09f615e3f487c700d4aa Mon Sep 17 00:00:00 2001 From: Jonas B Date: Thu, 7 Mar 2024 23:18:36 +0100 Subject: [PATCH 18/33] fix: rename current warn to error & no color in state, decided in svelte --- .../src/components/TableView.svelte | 18 +++++----- .../components/profiling/ProfilingInfo.svelte | 36 +++++++++++++------ .../src/icons/{Warn.svelte => Error.svelte} | 0 packages/safe-ds-eda/types/state.d.ts | 5 ++- packages/safe-ds-vscode/media/styles.css | 1 + .../src/extension/eda/apis/runnerApi.ts | 13 +++---- 6 files changed, 42 insertions(+), 31 deletions(-) rename packages/safe-ds-eda/src/icons/{Warn.svelte => Error.svelte} (100%) diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index c2e78518f..a363a319e 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -3,12 +3,12 @@ import { throttle } from 'lodash'; import { currentState, preventClicks } from '../webviewState'; import CaretIcon from '../icons/Caret.svelte'; - import WarnIcon from '../icons/Warn.svelte'; + import ErrorIcon from '../icons/Error.svelte'; import FilterIcon from '../icons/Filter.svelte'; import type { PossibleColumnFilter, Profiling, - ProfilingDetailBase, + ProfilingDetail, ProfilingDetailStatistical, } from '../../types/state'; import ProfilingInfo from './profiling/ProfilingInfo.svelte'; @@ -600,7 +600,7 @@ } }; - const calcProfilingItemValue = function (profilingItem: ProfilingDetailBase): number { + const calcProfilingItemValue = function (profilingItem: ProfilingDetail): number { // To edit when Profiling type scales/changes if (profilingItem.type === 'image') { return 3; // Bigger than normal text line, should be set to 3x line height @@ -609,10 +609,10 @@ } }; - const hasProfilingWarnings = function (): boolean { + const hasProfilingErrors = function (): boolean { for (const column of $currentState.table!.columns) { for (const profilingItem of column[1].profiling.top) { - if (profilingItem.type === 'numerical' && profilingItem.interpretation === 'warn') { + if (profilingItem.type === 'numerical' && profilingItem.interpretation === 'error') { return true; } } @@ -788,9 +788,9 @@ >
{showProfiling ? 'Hide Profiling' : 'Show Profiling'} - {#if hasProfilingWarnings()} -
- + {#if hasProfilingErrors()} +
+
{/if}
@@ -1004,7 +1004,7 @@ margin-left: 5px; } - .warnIconWrapper { + .errorIconWrapper { display: inline-flex; justify-content: center; height: 100%; diff --git a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte index 904f20de9..8da3ab716 100644 --- a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte +++ b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte @@ -1,7 +1,25 @@
@@ -9,15 +27,14 @@ {#each profiling.top as profilingTopItem} {#if profilingTopItem.type === 'name'}
- {profilingTopItem.name} + {profilingTopItem.name}
{:else if profilingTopItem.type === 'numerical'}
- {profilingTopItem.name}: - {profilingTopItem.value} + {profilingTopItem.value}
{/if} {/each} @@ -26,17 +43,14 @@ {#each profiling.bottom as profilingBottomItem} {#if profilingBottomItem.type === 'name'}
- {profilingBottomItem.name} + {profilingBottomItem.name}
{:else if profilingBottomItem.type === 'numerical'}
- {profilingBottomItem.name}: - {profilingBottomItem.value}{profilingBottomItem.value}
{/if} diff --git a/packages/safe-ds-eda/src/icons/Warn.svelte b/packages/safe-ds-eda/src/icons/Error.svelte similarity index 100% rename from packages/safe-ds-eda/src/icons/Warn.svelte rename to packages/safe-ds-eda/src/icons/Error.svelte diff --git a/packages/safe-ds-eda/types/state.d.ts b/packages/safe-ds-eda/types/state.d.ts index 1c6dd7ce2..c4be44939 100644 --- a/packages/safe-ds-eda/types/state.d.ts +++ b/packages/safe-ds-eda/types/state.d.ts @@ -78,14 +78,14 @@ export interface Profiling { export interface ProfilingDetailBase { type: 'numerical' | 'image' | 'name'; name: string; + interpretation: 'warn' | 'error' | 'default' | 'bold' | 'good'; } export interface ProfilingDetailStatistical extends ProfilingDetailBase { type: 'numerical'; name: string; value: string; - color?: string; - interpretation: 'warn' | 'category' | 'default'; + interpretation: ProfilingDetailBase['interpretation'] | 'category'; } export interface ProfilingDetailImage extends ProfilingDetailBase { @@ -97,7 +97,6 @@ export interface ProfilingDetailImage extends ProfilingDetailBase { export interface ProfilingDetailName extends ProfilingDetailBase { type: 'name'; name: string; - color?: string; } export type ProfilingDetail = ProfilingDetailStatistical | ProfilingDetailImage | ProfilingDetailName; diff --git a/packages/safe-ds-vscode/media/styles.css b/packages/safe-ds-vscode/media/styles.css index d5e1736b1..61b357140 100644 --- a/packages/safe-ds-vscode/media/styles.css +++ b/packages/safe-ds-vscode/media/styles.css @@ -2,6 +2,7 @@ --primary-color: #036ed1; --primary-color-desaturated: rgb(3 109 209 / 16.4%); --error-color: #a01417; + --warn-color: #a08b14; --bg-bright: white; --bg-dark: #f2f2f2; --bg-medium: #f9f9f9; 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 b2cd0ab98..b31c38362 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -284,16 +284,14 @@ export class RunnerApi { type: 'numerical', name: 'Valid', value: missingValuesRatio ? (100 - missingValuesRatio).toFixed(2) + '%' : '100%', - color: 'var(--primary-color)', - interpretation: 'default', + interpretation: 'good', }; const missingRatio: ProfilingDetailStatistical = { type: 'numerical', name: 'Missing', value: missingValuesRatio ? missingValuesRatio.toFixed(2) + '%' : '0%', - color: missingValuesRatio > 0 ? 'var(--error-color)' : 'var(--font-light)', - interpretation: missingValuesRatio > 0 ? 'warn' : 'default', + interpretation: missingValuesRatio > 0 ? 'error' : 'default', }; // If not numerical, add proper profilings according to idness results @@ -319,7 +317,6 @@ export class RunnerApi { type: 'numerical', name: key, value: ((value / column[1].values.length) * 100).toFixed(2) + '%', - color: 'var(--font-light)', interpretation: 'category', }); } @@ -329,7 +326,7 @@ export class RunnerApi { profiling: { top: [validRatio, missingRatio], bottom: [ - { type: 'name', name: 'Categorical', color: 'var(--font-dark)' }, + { type: 'name', name: 'Categorical', interpretation: 'bold' }, ...uniqueProfilings, ], }, @@ -341,11 +338,11 @@ export class RunnerApi { profiling: { top: [validRatio, missingRatio], bottom: [ - { type: 'name', name: 'Categorical', color: 'var(--font-dark)' }, + { type: 'name', name: 'Categorical', interpretation: 'bold' }, { type: 'name', name: uniqueValues + ' Uniques', - color: 'var(--font-light)', + interpretation: 'default', }, ], }, From 18a24e0d2ae43d88f7f2b9ac4931a4a64af8f4d0 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Fri, 8 Mar 2024 17:29:03 +0100 Subject: [PATCH 19/33] fix: updateTableSpace column names trimmed & minor things --- .../src/components/TableView.svelte | 85 ++++++++++--------- packages/safe-ds-eda/types/state.d.ts | 2 +- .../src/extension/eda/apis/runnerApi.ts | 35 ++++++-- .../src/extension/eda/edaPanel.ts | 52 ++++++------ 4 files changed, 99 insertions(+), 75 deletions(-) diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index a363a319e..9da25fff4 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -13,6 +13,7 @@ } from '../../types/state'; import ProfilingInfo from './profiling/ProfilingInfo.svelte'; import ColumnFilters from './column-filters/ColumnFilters.svelte'; + import { derived } from 'svelte/store'; export let sidebarWidth: number; @@ -97,9 +98,9 @@ const currentWidth = startWidth + event.clientX - startX; requestAnimationFrame(() => { targetColumn.style.width = `${currentWidth}px`; - savedColumnWidths.set(targetColumn.innerText, currentWidth); + savedColumnWidths.set(targetColumn.innerText.trim(), currentWidth); }); - resizeWidthMap.set(targetColumn.innerText, currentWidth); + resizeWidthMap.set(targetColumn.innerText.trim(), currentWidth); updateTableSpace(); } }; @@ -167,14 +168,14 @@ holdTimeout = setTimeout(() => { isClick = false; // If timeout completes, it's a hold document.addEventListener('mouseup', handleReorderDragEnd); - savedColumnWidthBeforeReorder = savedColumnWidths.get(headerElements[columnIndex].innerText)!; + savedColumnWidthBeforeReorder = savedColumnWidths.get(headerElements[columnIndex].innerText.trim())!; preventResizeTableSpaceUpdate = true; // To not add the new space to current dragged column isReorderDragging = true; dragStartIndex = columnIndex; dragCurrentIndex = columnIndex; draggedColumn = headerElements[columnIndex]; draggedColumn.classList.add('dragging'); - savedColumnWidths.set(draggedColumn.innerText, 0); + savedColumnWidths.set(draggedColumn.innerText.trim(), 0); updateTableSpace(); selectedColumnIndexes = []; // Clear so reordering doesn't interfere with selection }, 300); // milliseconds delay for hold detection @@ -201,7 +202,7 @@ if (isReorderDragging && dragStartIndex !== null && dragCurrentIndex !== null) { preventResizeTableSpaceUpdate = false; if (draggedColumn) { - savedColumnWidths.set(draggedColumn.innerText, savedColumnWidthBeforeReorder); + savedColumnWidths.set(draggedColumn.innerText.trim(), savedColumnWidthBeforeReorder); draggedColumn.style.left = ''; draggedColumn.style.top = ''; draggedColumn.classList.remove('dragging'); @@ -382,47 +383,55 @@ }, 100); const updateTableSpace = function (): void { + if (isResizeDragging) return; // Don't update while resizing + const newPossibleSpace = tableContainer.clientWidth; const utilitySpace = borderColumnWidth * 2; // 2 border columns let beforeWidth = utilitySpace; for (const width of savedColumnWidths.values()) { - if (width === 0) { + if (width !== 0) { + beforeWidth += width; } - beforeWidth += width; } + console.log('newPossibleSpace', newPossibleSpace); + console.log('beforeWidth', beforeWidth); + console.log(headerElements.map((column) => column.innerText.trim())); + if (newPossibleSpace > beforeWidth) { // Extend all column widths proportionally with new space for (const column of headerElements) { + const columnName = column.innerText.trim(); const newWidth = column.offsetWidth + (newPossibleSpace - beforeWidth) / headerElements.length; column.style.width = newWidth + 'px'; - savedColumnWidths.set(column.innerText, newWidth); + savedColumnWidths.set(columnName, newWidth); } } else { // Shrink all column widths proportionally with new space if not below minimum width dedicated by a: width by header text or b: with by manual resize for (const column of headerElements) { + const columnName = column.innerText.trim(); const newWidth = column.offsetWidth - (beforeWidth - newPossibleSpace) / headerElements.length; - if (resizeWidthMap.has(column.innerText)) { + if (resizeWidthMap.has(columnName)) { // User resized manually, so don't shrink below that - if (resizeWidthMap.get(column.innerText)! <= newWidth) { + if (resizeWidthMap.get(columnName)! <= newWidth) { column.style.width = newWidth + 'px'; - savedColumnWidths.set(column.innerText, newWidth); - } else if (column.offsetWidth !== resizeWidthMap.get(column.innerText)!) { + savedColumnWidths.set(columnName, newWidth); + } else if (column.offsetWidth !== resizeWidthMap.get(columnName)!) { // To update even on fast resize - column.style.width = resizeWidthMap.get(column.innerText)! + 'px'; - savedColumnWidths.set(column.innerText, resizeWidthMap.get(column.innerText)!); + column.style.width = resizeWidthMap.get(columnName)! + 'px'; + savedColumnWidths.set(columnName, resizeWidthMap.get(columnName)!); } } else { // Use the minimum width dedicated by the header text - const minWidth = getColumnWidthFreshNumber(column.innerText); + const minWidth = getColumnWidthFreshNumber(columnName); if (minWidth <= newWidth) { column.style.width = newWidth + 'px'; - savedColumnWidths.set(column.innerText, newWidth); + savedColumnWidths.set(columnName, newWidth); } else if (column.clientWidth !== minWidth) { // To update even on fast resize column.style.width = minWidth + 'px'; - savedColumnWidths.set(column.innerText, minWidth); + savedColumnWidths.set(columnName, minWidth); } } } @@ -465,7 +474,7 @@ let filterContextMenuElement: HTMLElement; const handleFilterContextMenu = function (event: MouseEvent, columnIndex: number): void { - if (event.button !== 0) return; + if (event.button !== 0 || $preventClicks) return; // Logic for what happens when a filter icon is clicked event.stopPropagation(); @@ -609,16 +618,18 @@ } }; - const hasProfilingErrors = function (): boolean { + // As store to update on profiling changes + const hasProfilingErrors = derived(currentState, ($currentState) => { + if (!$currentState.table) return false; for (const column of $currentState.table!.columns) { - for (const profilingItem of column[1].profiling.top) { + for (const profilingItem of column[1].profiling.top.concat(column[1].profiling.bottom)) { if (profilingItem.type === 'numerical' && profilingItem.interpretation === 'error') { return true; } } } return false; - }; + }); const getPosiibleColumnFilters = function (columnIndex: number): PossibleColumnFilter[] { if (!$currentState.table) return []; @@ -639,7 +650,7 @@ if (profilingCategories.length > 0) { possibleColumnFilters.push({ type: 'specificValue', - values: profilingCategories.map((profilingItem) => profilingItem.value.replace('%', '')), + values: ['-'].concat(profilingCategories.map((profilingItem) => profilingItem.name)), columnName: column.name, }); } else { @@ -707,11 +718,7 @@ {#each $currentState.table.columns as column, index}
@@ -788,7 +797,7 @@ >
{showProfiling ? 'Hide Profiling' : 'Show Profiling'} - {#if hasProfilingErrors()} + {#if $hasProfilingErrors}
@@ -799,12 +808,14 @@
{#each $currentState.table.columns as _column, i} - + {#if i !== $currentState.table.columns.length - 1} + + {/if} {/each}
handleColumnInteractionStart(event, index)} on:mousemove={(event) => throttledHandleReorderDragOver(event, index)} @@ -738,6 +745,8 @@ {/each} throttledHandleReorderDragOver(event, $currentState.table?.columns.length ?? 0)}># throttledHandleReorderDragOver(event, i + 1)} - > - throttledHandleReorderDragOver(event, i + 1)} + > + | undefined; constructor(services: SafeDsServices, pipelinePath: vscode.Uri, pipelineName: string) { this.services = services; this.pipelinePath = pipelinePath; this.pipelineName = pipelineName; + getPipelineDocument(this.pipelinePath).then((doc) => { + // Get here to avoid issues because of chanigng file + this.baseDocument = doc; + }); } private async addToAndExecutePipeline(pipelineExecutionId: string, addedLines: string): Promise { return new Promise(async (resolve, reject) => { - const baseDocument = await getPipelineDocument(this.pipelinePath); - if (!baseDocument) { - reject('Pipeline not found'); + if (!this.baseDocument) { + reject('Document not found'); return; } - const documentText = baseDocument.textDocument.getText(); + const documentText = this.baseDocument.textDocument.getText(); // Find pattern "pipeline {" and add the SDS code before the closing bracket of it const pipelineStart = documentText.indexOf('pipeline ' + this.pipelineName); @@ -203,7 +208,7 @@ export class RunnerApi { let sdsStrings = ''; const columnNameToPlaceholderMVNameMap = new Map(); // Mapping random placeholder name for missing value ratio back to column name - const missingValueRatioMap = new Map(); // Saved by random placeholder name + const missingValueRatioMap = new Map(); // Saved by random placeholder name const columnNameToPlaceholderIDnessNameMap = new Map(); // Mapping random placeholder name for IDness back to column name const idnessMap = new Map(); // Saved by random placeholder name @@ -214,7 +219,7 @@ export class RunnerApi { // Generate SDS code to get missing value ratio for each column for (let i = 0; i < columns.length; i++) { const newMvPlaceholderName = this.randomPlaceholderName(); - missingValueRatioMap.set(newMvPlaceholderName, 'null'); + missingValueRatioMap.set(newMvPlaceholderName, 0); columnNameToPlaceholderMVNameMap.set(columns[i]![1].name, newMvPlaceholderName); sdsStrings += this.sdsStringForMissingValueRatioByColumnName( @@ -254,7 +259,7 @@ export class RunnerApi { for (const [placeholderName] of missingValueRatioMap) { const missingValueRatio = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); if (missingValueRatio) { - missingValueRatioMap.set(placeholderName, missingValueRatio as string); + missingValueRatioMap.set(placeholderName, missingValueRatio as number); } } @@ -278,7 +283,7 @@ export class RunnerApi { const profiling: { columnName: string; profiling: Profiling }[] = []; for (const column of columns) { const missingValuesRatio = - parseFloat(missingValueRatioMap.get(columnNameToPlaceholderMVNameMap.get(column[1].name)!)!) * 100; + missingValueRatioMap.get(columnNameToPlaceholderMVNameMap.get(column[1].name)!)! * 100; const validRatio: ProfilingDetailStatistical = { type: 'numerical', @@ -341,7 +346,19 @@ export class RunnerApi { { type: 'name', name: 'Categorical', interpretation: 'bold' }, { type: 'name', - name: uniqueValues + ' Uniques', + name: uniqueValues + ' Distincts', + interpretation: 'default', + }, + { + type: 'name', + name: + Math.round( + column[1].values.length * + (1 - + missingValueRatioMap.get( + columnNameToPlaceholderMVNameMap.get(column[1].name)!, + )!), + ) + ' Total', interpretation: 'default', }, ], diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index b148e8ab0..365155777 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -129,32 +129,32 @@ export class EDAPanel { panel.tableName = tableName; EDAPanel.panelsMap.set(tableIdentifier, panel); - // Have to update and construct state as table placeholder could've changed in code - panel.updateHtmlDone = false; - panel._update(); - await panel.waitForUpdateHtmlDone(10000); - - // Get and send state - const stateInfo = await panel.constructCurrentState(); - webviewApi.postMessage(panel!.panel.webview, { - command: 'setWebviewState', - value: stateInfo.state, - }); - - // If not present, get and send profiling - if ( - !stateInfo.fromExisting || - !stateInfo.state.table || - !stateInfo.state - .table!.columns.map((c) => c[1].profiling) - .find((p) => p.bottom.length > 0 || p.top.length > 0) - ) { - const profiling = await panel.runnerApi.getProfiling(stateInfo.state.table!); - webviewApi.postMessage(panel!.panel.webview, { - command: 'setProfiling', - value: profiling, - }); - } + // // Have to update and construct state as table placeholder could've changed in code + // panel.updateHtmlDone = false; + // panel._update(); + // await panel.waitForUpdateHtmlDone(10000); + + // // Get and send state + // const stateInfo = await panel.constructCurrentState(); + // webviewApi.postMessage(panel!.panel.webview, { + // command: 'setWebviewState', + // value: stateInfo.state, + // }); + + // // If not present, get and send profiling + // if ( + // !stateInfo.fromExisting || + // !stateInfo.state.table || + // !stateInfo.state + // .table!.columns.map((c) => c[1].profiling) + // .find((p) => p.bottom.length > 0 || p.top.length > 0) + // ) { + // const profiling = await panel.runnerApi.getProfiling(stateInfo.state.table!); + // webviewApi.postMessage(panel!.panel.webview, { + // command: 'setProfiling', + // value: profiling, + // }); + // } return; } else { // Otherwise, create a new panel. From 95734a4e082bbe02bef09e7a9c8a549a0074ea45 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Fri, 29 Mar 2024 15:27:06 +0100 Subject: [PATCH 20/33] fix: webview reload and profiling semantic --- packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts | 2 +- packages/safe-ds-vscode/src/extension/mainClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 16a5c971d..6d3b0ede6 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -358,7 +358,7 @@ export class RunnerApi { missingValueRatioMap.get( columnNameToPlaceholderMVNameMap.get(column[1].name)!, )!), - ) + ' Total', + ) + ' Total Valids', interpretation: 'default', }, ], diff --git a/packages/safe-ds-vscode/src/extension/mainClient.ts b/packages/safe-ds-vscode/src/extension/mainClient.ts index 85346f835..1884b8216 100644 --- a/packages/safe-ds-vscode/src/extension/mainClient.ts +++ b/packages/safe-ds-vscode/src/extension/mainClient.ts @@ -346,7 +346,7 @@ const registerVSCodeCommands = function (context: vscode.ExtensionContext) { vscode.window.showErrorMessage('No EDA Panel to refresh!'); return; } - EDAPanel.kill(lastSuccessfulPipelineName!); + EDAPanel.kill(lastSuccessfulPipelineName! + '.' + lastSuccessfulTableName!); setTimeout(() => { EDAPanel.createOrShow( context.extensionUri, From 44b385008587ee2d77a3af9376557091bd30368b Mon Sep 17 00:00:00 2001 From: Jonas B Date: Fri, 29 Mar 2024 16:23:55 +0100 Subject: [PATCH 21/33] fix: no manual updateTableSpace & colWidths svelte store & head has bg - got rid of an excess profiling banner div: - Now apparently now methods for table resize/TableSpace needed anymore, all handled by html itself - Meaning I also cannot change much about that mechanism, other than the min width, set by setting width on startup of the elements - Also means a lot less code and complexity! - savedColumnWidths now is a svelte store, that the table subscribes to, meaning it updates correctly, only needed on resize and reorder (when letting the col go); also now no manual setting of stlyle for this anymore - the automatic handling by html meant that reordering did not properly take out of table, so now a "reorderPrototype" of a column header that is used for the under cursor display and updates with relevant data, while column is made "display: none" in table - Min width maybe as initial width if it is being streched, then increasing size of a col will not result in others shrinking, but how to decide if in full view or not? - Also full view makes scrolling for fixed stuff lag?? Mabye visible scroll bar or extra tiny div - Fixed that full view makes fixed stuff lag by making table width 100.1% instead of 100%, so always tiny bit out of view that causes scroll to exist - increased scroll buffer a bit to make more fluent - now you cannot see the table text through the borders of the headers/profiling anymore if table scrolled - have an absolute div at 100% with at top that is bright bg color normal height = 2 * rowHeight - if profiling expanded then delayed (since height animation of profilingInfo) setting of height to 2 * rowHeight + profilingInfo height, not complete height as not including for example borders but enough to cover all bg space that let's text through - top prop of this also = scrollTop --- packages/safe-ds-eda/src/App.svelte | 1 - .../src/components/TableView.svelte | 233 ++++++++---------- 2 files changed, 101 insertions(+), 133 deletions(-) diff --git a/packages/safe-ds-eda/src/App.svelte b/packages/safe-ds-eda/src/App.svelte index 0ab3960fb..322e16e30 100644 --- a/packages/safe-ds-eda/src/App.svelte +++ b/packages/safe-ds-eda/src/App.svelte @@ -68,7 +68,6 @@ .tableWrapper { flex: 1; - overflow: scroll; } .resizer { diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index 9da25fff4..f7890c8cc 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -13,21 +13,16 @@ } from '../../types/state'; import ProfilingInfo from './profiling/ProfilingInfo.svelte'; import ColumnFilters from './column-filters/ColumnFilters.svelte'; - import { derived } from 'svelte/store'; + import { derived, writable, type Readable, get } from 'svelte/store'; export let sidebarWidth: number; - $: if (sidebarWidth && tableContainer) { - updateTableSpace(); - } - let showProfiling = false; let minTableWidth = 0; let numRows = 0; - const borderColumnWidth = 45; // Set in CSS, change here if changes in css const headerElements: HTMLElement[] = []; let maxProfilingItemCount = 0; - const savedColumnWidths: Map = new Map(); + let savedColumnWidths = writable(new Map()); $: { if ($currentState.table) { @@ -55,29 +50,28 @@ } } - const getColumnWidth = function (columnName: string): number { - if (savedColumnWidths.has(columnName)) { - return savedColumnWidths.get(columnName)!; - } - const baseWidth = 35; // Minimum width - const scale = 55; - - // Use the logarithm of the character count, and scale it - const width = baseWidth + Math.log(columnName.length + 1) * scale; + $: if (headerElements.length > 0) { + // Is svelte reactive but so far only runs once which is what we want, consideration to have loop in onMount that waits until headerElements is filled and then runs this code once + for (const column of headerElements) { + const columnName = column.innerText.trim(); + if (get(savedColumnWidths).has(columnName)) continue; // Only set intital width if not already set - // Save the width for future use - savedColumnWidths.set(columnName, width); + const baseWidth = 35; // Minimum width + const scale = 55; - return width; - }; + // Use the logarithm of the character count, and scale it + const width = baseWidth + Math.log(columnName.length + 1) * scale; - const getColumnWidthFreshNumber = function (columnName: string): number { - const baseWidth = 35; // Minimum width - const scale = 55; + // Save the width for future use + savedColumnWidths.update((map) => { + map.set(columnName, width); + return map; + }); + console.log(columnName, width); + } - // Use the logarithm of the character count, and scale it - return baseWidth + Math.log(columnName.length + 1) * scale; - }; + lastHeight = tableContainer.clientHeight; // For recalculateVisibleRowCount + } const handleMainCellClick = function (): void { if (!$preventClicks) { @@ -98,10 +92,12 @@ const currentWidth = startWidth + event.clientX - startX; requestAnimationFrame(() => { targetColumn.style.width = `${currentWidth}px`; - savedColumnWidths.set(targetColumn.innerText.trim(), currentWidth); + savedColumnWidths.update((map) => { + map.set(targetColumn.innerText.trim(), currentWidth); + return map; + }); + resizeWidthMap.set(targetColumn.innerText.trim(), currentWidth); }); - resizeWidthMap.set(targetColumn.innerText.trim(), currentWidth); - updateTableSpace(); } }; @@ -128,20 +124,20 @@ let isReorderDragging = false; let dragStartIndex: number | null = null; let dragCurrentIndex: number | null = null; - let draggedColumn: HTMLElement | null = null; + let draggedColumnName: string | null = null; + let reorderPrototype: HTMLElement; let savedColumnWidthBeforeReorder = 0; - let preventResizeTableSpaceUpdate = false; let holdTimeout: NodeJS.Timeout; let isClick = true; // Flag to distinguish between click and hold let currentMouseUpHandler: ((event: MouseEvent) => void) | null = null; // For being able to properly remove the mouseup listener when col clicked and not held const handleReorderDragOver = function (event: MouseEvent, columnIndex: number): void { - if (isReorderDragging && dragStartIndex !== null && draggedColumn) { + if (isReorderDragging && dragStartIndex !== null && draggedColumnName) { dragCurrentIndex = columnIndex; requestAnimationFrame(() => { - draggedColumn!.style.left = event.clientX + tableContainer.scrollLeft - sidebarWidth + 'px'; - draggedColumn!.style.top = event.clientY + 'px'; + reorderPrototype!.style.left = event.clientX + tableContainer.scrollLeft - sidebarWidth + 'px'; + reorderPrototype!.style.top = event.clientY + scrollTop + 'px'; }); } }; @@ -166,17 +162,14 @@ isClick = true; // Assume it's a click initially holdTimeout = setTimeout(() => { + // Reorder drag start isClick = false; // If timeout completes, it's a hold document.addEventListener('mouseup', handleReorderDragEnd); - savedColumnWidthBeforeReorder = savedColumnWidths.get(headerElements[columnIndex].innerText.trim())!; - preventResizeTableSpaceUpdate = true; // To not add the new space to current dragged column + savedColumnWidthBeforeReorder = get(savedColumnWidths).get(headerElements[columnIndex].innerText.trim())!; + draggedColumnName = headerElements[columnIndex].innerText.trim(); isReorderDragging = true; dragStartIndex = columnIndex; dragCurrentIndex = columnIndex; - draggedColumn = headerElements[columnIndex]; - draggedColumn.classList.add('dragging'); - savedColumnWidths.set(draggedColumn.innerText.trim(), 0); - updateTableSpace(); selectedColumnIndexes = []; // Clear so reordering doesn't interfere with selection }, 300); // milliseconds delay for hold detection @@ -200,13 +193,12 @@ const handleReorderDragEnd = function (): void { if (isReorderDragging && dragStartIndex !== null && dragCurrentIndex !== null) { - preventResizeTableSpaceUpdate = false; - if (draggedColumn) { - savedColumnWidths.set(draggedColumn.innerText.trim(), savedColumnWidthBeforeReorder); - draggedColumn.style.left = ''; - draggedColumn.style.top = ''; - draggedColumn.classList.remove('dragging'); - draggedColumn = null; + if (draggedColumnName) { + savedColumnWidths.update((map) => { + map.set(draggedColumnName!, savedColumnWidthBeforeReorder); + return map; + }); + draggedColumnName = null; } // Reset the z-index of all headers headerElements.forEach((header) => { @@ -229,8 +221,6 @@ isReorderDragging = false; dragStartIndex = null; dragCurrentIndex = null; - updateTableSpace(); - updateTableSpace(); // Have to somehow call twice, first time it thinks the window is around 10px bigger than it is } }; @@ -342,7 +332,7 @@ // --- Scroll loading --- let tableContainer: HTMLElement; // Reference to the table container const rowHeight = 33; // Adjust based on your row height - const buffer = 25; // Number of rows to render outside the viewport + const buffer = 40; // Number of rows to render outside the viewport let visibleStart = 0; let visibleEnd = 0; let visibleRowCount = 10; @@ -351,7 +341,7 @@ const updateVisibleRows = function (): void { visibleStart = Math.max(0, Math.floor(scrollTop / rowHeight) - buffer); - visibleEnd = visibleStart + visibleRowCount; + visibleEnd = visibleStart + visibleRowCount + buffer; }; const throttledUpdateVisibleRows = throttle(updateVisibleRows, 40); @@ -375,75 +365,6 @@ const throttledRecalculateVisibleRowCount = throttle(recalculateVisibleRowCount, 20); - // --- Min Table with --- - const throttledUpdateTableSpace = throttle(() => { - if (!preventResizeTableSpaceUpdate) { - updateTableSpace(); - } - }, 100); - - const updateTableSpace = function (): void { - if (isResizeDragging) return; // Don't update while resizing - - const newPossibleSpace = tableContainer.clientWidth; - - const utilitySpace = borderColumnWidth * 2; // 2 border columns - let beforeWidth = utilitySpace; - for (const width of savedColumnWidths.values()) { - if (width !== 0) { - beforeWidth += width; - } - } - - console.log('newPossibleSpace', newPossibleSpace); - console.log('beforeWidth', beforeWidth); - console.log(headerElements.map((column) => column.innerText.trim())); - - if (newPossibleSpace > beforeWidth) { - // Extend all column widths proportionally with new space - for (const column of headerElements) { - const columnName = column.innerText.trim(); - const newWidth = column.offsetWidth + (newPossibleSpace - beforeWidth) / headerElements.length; - column.style.width = newWidth + 'px'; - savedColumnWidths.set(columnName, newWidth); - } - } else { - // Shrink all column widths proportionally with new space if not below minimum width dedicated by a: width by header text or b: with by manual resize - for (const column of headerElements) { - const columnName = column.innerText.trim(); - const newWidth = column.offsetWidth - (beforeWidth - newPossibleSpace) / headerElements.length; - if (resizeWidthMap.has(columnName)) { - // User resized manually, so don't shrink below that - if (resizeWidthMap.get(columnName)! <= newWidth) { - column.style.width = newWidth + 'px'; - savedColumnWidths.set(columnName, newWidth); - } else if (column.offsetWidth !== resizeWidthMap.get(columnName)!) { - // To update even on fast resize - column.style.width = resizeWidthMap.get(columnName)! + 'px'; - savedColumnWidths.set(columnName, resizeWidthMap.get(columnName)!); - } - } else { - // Use the minimum width dedicated by the header text - const minWidth = getColumnWidthFreshNumber(columnName); - if (minWidth <= newWidth) { - column.style.width = newWidth + 'px'; - savedColumnWidths.set(columnName, newWidth); - } else if (column.clientWidth !== minWidth) { - // To update even on fast resize - column.style.width = minWidth + 'px'; - savedColumnWidths.set(columnName, minWidth); - } - } - } - } - }; - - $: if (headerElements.length > 0) { - // Is svelte reactive but so far only runs once which is what we want, consideration to have loop in onMount that waits until headerElements is filled and then runs this code once - lastHeight = tableContainer.clientHeight; - updateTableSpace(); - } - // --- Right clicks --- let currentContextMenu: HTMLElement | null = null; @@ -591,8 +512,20 @@ }; // --- Profiling --- + let fullHeadBackground: HTMLElement; + let profilingInfo: HTMLElement; + const toggleProfiling = function (): void { - if (!$preventClicks) showProfiling = !showProfiling; + if (!$preventClicks) { + showProfiling = !showProfiling; + if (showProfiling) { + setTimeout(() => { + fullHeadBackground.style.height = 2 * rowHeight + profilingInfo.clientHeight + 'px'; + }, 700); + } else { + fullHeadBackground.style.height = rowHeight * 2 + 'px'; + } + } }; const getOptionalProfilingHeight = function (profiling: Profiling): string { @@ -689,14 +622,12 @@ tableContainer.addEventListener('scroll', throttledUpdateVisibleRows); tableContainer.addEventListener('scroll', updateScrollTop); window.addEventListener('resize', throttledRecalculateVisibleRowCount); - window.addEventListener('resize', throttledUpdateTableSpace); interval = setInterval(updateVisibleRows, 500); // To catch cases of fast scroll bar scrolling that leave table blank return () => { tableContainer.removeEventListener('scroll', throttledUpdateVisibleRows); tableContainer.addEventListener('scroll', updateScrollTop); window.removeEventListener('resize', throttledRecalculateVisibleRowCount); - window.removeEventListener('resize', throttledUpdateTableSpace); clearInterval(interval); }; }); @@ -706,6 +637,11 @@ {#if !$currentState.table} Loading ... {:else} +
@@ -719,7 +655,11 @@ - + - {#each $currentState.table.columns as _column, i} - {#if i !== $currentState.table.columns.length - 1} + {#each $currentState.table.columns as _column, index} + {#if index !== $currentState.table.columns.length - 1} {/if} @@ -836,6 +777,7 @@ > {#each $currentState.table.columns as column, index} + {/if} {#if showingColumnHeaderRightClickMenu}
@@ -910,10 +860,21 @@ background-color: var(--bg-dark); } + .fullHeadBackground { + position: absolute; + top: 0; + left: 0; + right: 0; + width: 100%; + background-color: white; + z-index: 2; + } + table { table-layout: fixed; - width: 100%; + width: 100.1%; /* To prevent lagging on vert scroll */ background-color: var(--bg-dark); + overflow-x: scroll; } .headerRow { @@ -921,6 +882,7 @@ top: 0; z-index: 1000; } + thead tr:hover { background-color: transparent; } @@ -948,12 +910,13 @@ } tbody { border-left: 3px solid var(--bg-bright); + z-index: 1; } - table tr { + tr { border-bottom: 2px solid var(--bg-dark); background-color: var(--bg-bright); } - table tr:hover { + tr:hover { background-color: var(--primary-color-desaturated); } @@ -1061,6 +1024,12 @@ pointer-events: none; /* Make it non-interactive */ z-index: 1000; /* Ensure it's on top */ border: 3px solid var(--bg-dark); + display: flex; + flex-direction: column; + align-items: start; + justify-content: center; + padding: 12px 8px; + text-overflow: ellipsis; } .contextMenu { From 81265612ca60281429242717249751a189f317a9 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Fri, 29 Mar 2024 16:44:07 +0100 Subject: [PATCH 22/33] fix: change state profiling semantics - changed profiling to always include value, not name, as we display values - thus image string also as "value" as well as string when just using "text" type (prev. "name" type) --- .../components/profiling/ProfilingInfo.svelte | 9 ++++---- packages/safe-ds-eda/types/state.d.ts | 9 ++++---- .../src/extension/eda/apis/runnerApi.ts | 22 ++++++------------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte index 8da3ab716..02a4e8465 100644 --- a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte +++ b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte @@ -25,9 +25,9 @@
{#each profiling.top as profilingTopItem} - {#if profilingTopItem.type === 'name'} + {#if profilingTopItem.type === 'text'}
- {profilingTopItem.name} + {profilingTopItem.value}
{:else if profilingTopItem.type === 'numerical'}
@@ -41,9 +41,10 @@
{#each profiling.bottom as profilingBottomItem} - {#if profilingBottomItem.type === 'name'} + {#if profilingBottomItem.type === 'text'}
- {profilingBottomItem.name} + {profilingBottomItem.value}
{:else if profilingBottomItem.type === 'numerical'}
diff --git a/packages/safe-ds-eda/types/state.d.ts b/packages/safe-ds-eda/types/state.d.ts index b60d543f9..c2c860d28 100644 --- a/packages/safe-ds-eda/types/state.d.ts +++ b/packages/safe-ds-eda/types/state.d.ts @@ -77,7 +77,7 @@ export interface Profiling { export interface ProfilingDetailBase { type: 'numerical' | 'image' | 'name'; - name: string; + value: string; interpretation: 'warn' | 'error' | 'default' | 'bold' | 'good'; } @@ -90,13 +90,12 @@ export interface ProfilingDetailStatistical extends ProfilingDetailBase { export interface ProfilingDetailImage extends ProfilingDetailBase { type: 'image'; - name: string; - encodedImage: string; + value: string; } export interface ProfilingDetailName extends ProfilingDetailBase { - type: 'name'; - name: string; + type: 'text'; + value: string; } export type ProfilingDetail = ProfilingDetailStatistical | ProfilingDetailImage | ProfilingDetailName; 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 6d3b0ede6..b99bb992d 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -119,14 +119,6 @@ export class RunnerApi { private randomPlaceholderName(): string { const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; // __gen, code gen prefix (konstante importieren) - const charactersLength = characters.length; - const randomArray = new Uint8Array(20); - crypto.getRandomValues(randomArray); - let result = ''; - randomArray.forEach((value) => { - result += characters.charAt(value % charactersLength); - }); - return CODEGEN_PREFIX + result; } private async getPlaceholderValue(placeholder: string, pipelineExecutionId: string): Promise { @@ -331,7 +323,7 @@ export class RunnerApi { profiling: { top: [validRatio, missingRatio], bottom: [ - { type: 'name', name: 'Categorical', interpretation: 'bold' }, + { type: 'text', value: 'Categorical', interpretation: 'bold' }, ...uniqueProfilings, ], }, @@ -343,15 +335,15 @@ export class RunnerApi { profiling: { top: [validRatio, missingRatio], bottom: [ - { type: 'name', name: 'Categorical', interpretation: 'bold' }, + { type: 'text', value: 'Categorical', interpretation: 'bold' }, { - type: 'name', - name: uniqueValues + ' Distincts', + type: 'text', + value: uniqueValues + ' Distincts', interpretation: 'default', }, { - type: 'name', - name: + type: 'text', + value: Math.round( column[1].values.length * (1 - @@ -380,7 +372,7 @@ export class RunnerApi { } public async getColumnNames(tableIdentifier: string): Promise { - const newPlaceholderName = this.randomPlaceholderName(); + const newPlaceholderName = this.genPlaceholderName(); const columnNamesSdsCode = this.sdsStringForColumnNames(tableIdentifier, newPlaceholderName); const pipelineExecutionId = crypto.randomUUID(); await this.addToAndExecutePipeline(pipelineExecutionId, columnNamesSdsCode); From 608cce203fa67399b4acd17221fad75c7dfad6fe Mon Sep 17 00:00:00 2001 From: Jonas B Date: Fri, 29 Mar 2024 16:44:58 +0100 Subject: [PATCH 23/33] fix: placeholder generation now incr number - profiling placeholder name gen now not random but with codegen prefix + incr counter number --- .../src/extension/eda/apis/runnerApi.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 b99bb992d..2240120ea 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -12,6 +12,7 @@ export class RunnerApi { pipelinePath: vscode.Uri; pipelineName: string; baseDocument: LangiumDocument | undefined; + placeholderCounter = 0; constructor(services: SafeDsServices, pipelinePath: vscode.Uri, pipelineName: string) { this.services = services; @@ -117,8 +118,8 @@ export class RunnerApi { ); } - private randomPlaceholderName(): string { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; // __gen, code gen prefix (konstante importieren) + private genPlaceholderName(): string { + return CODEGEN_PREFIX + this.placeholderCounter++; } private async getPlaceholderValue(placeholder: string, pipelineExecutionId: string): Promise { @@ -210,7 +211,7 @@ export class RunnerApi { // Generate SDS code to get missing value ratio for each column for (let i = 0; i < columns.length; i++) { - const newMvPlaceholderName = this.randomPlaceholderName(); + const newMvPlaceholderName = this.genPlaceholderName(); missingValueRatioMap.set(newMvPlaceholderName, 0); columnNameToPlaceholderMVNameMap.set(columns[i]![1].name, newMvPlaceholderName); @@ -220,7 +221,7 @@ export class RunnerApi { newMvPlaceholderName, ); - const newHistogramPlaceholderName = this.randomPlaceholderName(); + const newHistogramPlaceholderName = this.genPlaceholderName(); histogramMap.set(newHistogramPlaceholderName, 'null'); columnNameToPlaceholderHistogramNameMap.set(columns[i]![1].name, newHistogramPlaceholderName); @@ -232,7 +233,7 @@ export class RunnerApi { // Only need to check IDness for non-numerical columns if (columns[i]![1].type !== 'numerical') { - const newIDnessPlaceholderName = this.randomPlaceholderName(); + const newIDnessPlaceholderName = this.genPlaceholderName(); idnessMap.set(newIDnessPlaceholderName, 1); columnNameToPlaceholderIDnessNameMap.set(columns[i]![1].name, newIDnessPlaceholderName); sdsStrings += this.sdsStringForIDnessByColumnName( From a4fd31cacc8331032c64737709b83febd4cbe856 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Fri, 29 Mar 2024 20:32:52 +0100 Subject: [PATCH 24/33] feat: profiling histograms --- .../src/components/TableView.svelte | 16 +++-- .../components/profiling/ProfilingInfo.svelte | 19 ++++++ packages/safe-ds-eda/types/state.d.ts | 9 ++- .../src/extension/eda/apis/runnerApi.ts | 68 +++++++++++++------ 4 files changed, 85 insertions(+), 27 deletions(-) diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index f7890c8cc..452b709f4 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -17,6 +17,8 @@ export let sidebarWidth: number; + let profilingImageWidth = 200; + let showProfiling = false; let minTableWidth = 0; let numRows = 0; @@ -56,11 +58,13 @@ const columnName = column.innerText.trim(); if (get(savedColumnWidths).has(columnName)) continue; // Only set intital width if not already set - const baseWidth = 35; // Minimum width - const scale = 55; + // const baseWidth = 35; // Minimum width + // const scale = 55; + + // // Use the logarithm of the character count, and scale it + // const width = baseWidth + Math.log(columnName.length + 1) * scale; - // Use the logarithm of the character count, and scale it - const width = baseWidth + Math.log(columnName.length + 1) * scale; + const width = profilingImageWidth + 2 * 12; // Image width + 2 borders // Save the width for future use savedColumnWidths.update((map) => { @@ -545,7 +549,7 @@ const calcProfilingItemValue = function (profilingItem: ProfilingDetail): number { // To edit when Profiling type scales/changes if (profilingItem.type === 'image') { - return 3; // Bigger than normal text line, should be set to 3x line height + return 9; // Bigger than normal text line, should be set to 3x line height } else { return 1; } @@ -712,7 +716,7 @@ {#if column[1].profiling.top.length === 0 && column[1].profiling.bottom.length === 0}
Loading ...
{:else} - + {/if}
diff --git a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte index 02a4e8465..b2e92b7f1 100644 --- a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte +++ b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte @@ -2,6 +2,7 @@ import type { Profiling, ProfilingDetail } from '../../../types/state'; export let profiling: Profiling; + export let imageWidth: number = 200; const getProfilingItemColor = function (profilingItem: ProfilingDetail) { if (profilingItem.interpretation === 'good') { @@ -54,6 +55,18 @@ {profilingBottomItem.value}
+ {:else if profilingBottomItem.type === 'image'} +
+ profiling histogram +
{/if} {/each}
@@ -85,4 +98,10 @@ .profilingItemsBottom { width: 100%; } + + .profilingImage { + height: 150px; + object-fit: cover; + object-position: left; + } diff --git a/packages/safe-ds-eda/types/state.d.ts b/packages/safe-ds-eda/types/state.d.ts index c2c860d28..b399d0e77 100644 --- a/packages/safe-ds-eda/types/state.d.ts +++ b/packages/safe-ds-eda/types/state.d.ts @@ -90,7 +90,7 @@ export interface ProfilingDetailStatistical extends ProfilingDetailBase { export interface ProfilingDetailImage extends ProfilingDetailBase { type: 'image'; - value: string; + value: Base64Image; } export interface ProfilingDetailName extends ProfilingDetailBase { @@ -189,3 +189,10 @@ export interface ProfilingSettings extends ProfilingSettingsBase { sum: boolean; variance: boolean; } + +// ------------ Types for general objects ----------- + +export interface Base64Image { + format: string; + bytes: string; +} 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 2240120ea..c51fcaa0b 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -1,4 +1,4 @@ -import { Column, Profiling, ProfilingDetailStatistical, Table } from '@safe-ds/eda/types/state.js'; +import { Base64Image, Column, Profiling, ProfilingDetailStatistical, Table } from '@safe-ds/eda/types/state.js'; import { SafeDsServices, messages } from '@safe-ds/lang'; import { LangiumDocument, AstNode } from 'langium'; import { printOutputMessage } from '../../output.ts'; @@ -207,7 +207,7 @@ export class RunnerApi { const idnessMap = new Map(); // Saved by random placeholder name const columnNameToPlaceholderHistogramNameMap = new Map(); // Mapping random placeholder name for histogram back to column name - const histogramMap = new Map(); // Saved by random placeholder name + const histogramMap = new Map(); // Saved by random placeholder name // Generate SDS code to get missing value ratio for each column for (let i = 0; i < columns.length; i++) { @@ -221,27 +221,37 @@ export class RunnerApi { newMvPlaceholderName, ); - const newHistogramPlaceholderName = this.genPlaceholderName(); - histogramMap.set(newHistogramPlaceholderName, 'null'); - columnNameToPlaceholderHistogramNameMap.set(columns[i]![1].name, newHistogramPlaceholderName); - - sdsStrings += this.sdsStringForHistogramByColumnName( - columns[i]![1].name, - table.name, - newHistogramPlaceholderName, - ); - // Only need to check IDness for non-numerical columns if (columns[i]![1].type !== 'numerical') { const newIDnessPlaceholderName = this.genPlaceholderName(); idnessMap.set(newIDnessPlaceholderName, 1); columnNameToPlaceholderIDnessNameMap.set(columns[i]![1].name, newIDnessPlaceholderName); + sdsStrings += this.sdsStringForIDnessByColumnName( columns[i]![1].name, table.name, newIDnessPlaceholderName, ); + + let uniqueValues = new Set(); + for (let j = 0; j < columns[i]![1].values.length; j++) { + uniqueValues.add(columns[i]![1].values[j]); + } + if (uniqueValues.size <= 3 || uniqueValues.size > 10) { + // Must match conidtions below that choose to display histogram + continue; // This historam only generated if between 4-10 categorigal uniques or numerical type + } } + + const newHistogramPlaceholderName = this.genPlaceholderName(); + histogramMap.set(newHistogramPlaceholderName, undefined); + columnNameToPlaceholderHistogramNameMap.set(columns[i]![1].name, newHistogramPlaceholderName); + + sdsStrings += this.sdsStringForHistogramByColumnName( + columns[i]![1].name, + table.name, + newHistogramPlaceholderName, + ); } // Execute with generated SDS code @@ -264,13 +274,13 @@ export class RunnerApi { } } - // // Get histogram for each column - // for (const [placeholderName] of histogramMap) { - // const histogram = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); - // if (histogram) { - // histogramMap.set(placeholderName, histogram as string); - // } - // } + // Get histogram for each column + for (const [placeholderName] of histogramMap) { + const histogram = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); + if (histogram) { + histogramMap.set(placeholderName, histogram as Base64Image); + } + } // Create profiling data, interpret numerical values and color them const profiling: { columnName: string; profiling: Profiling }[] = []; @@ -329,6 +339,19 @@ export class RunnerApi { ], }, }); + } else if (uniqueValues <= 10) { + const histogram = histogramMap.get(columnNameToPlaceholderHistogramNameMap.get(column[1].name)!)!; + + profiling.push({ + columnName: column[1].name, + profiling: { + top: [validRatio, missingRatio], + bottom: [ + { type: 'text', value: 'Categorical', interpretation: 'bold' }, + { type: 'image', value: histogram, interpretation: 'default' }, + ], + }, + }); } else { // Display only the number of unique values profiling.push({ @@ -359,11 +382,16 @@ export class RunnerApi { }); } } else { + const histogram = histogramMap.get(columnNameToPlaceholderHistogramNameMap.get(column[1].name)!)!; + profiling.push({ columnName: column[1].name, profiling: { top: [validRatio, missingRatio], - bottom: [], + bottom: [ + { type: 'text', value: 'Numerical', interpretation: 'bold' }, + { type: 'image', value: histogram, interpretation: 'default' }, + ], }, }); } From 73069428fc4b431801fdb3425d5b43d4662093a4 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Sat, 30 Mar 2024 14:24:06 +0100 Subject: [PATCH 25/33] refactor: some comments and code, more readable --- .../src/components/TableView.svelte | 1 - .../src/extension/eda/apis/runnerApi.ts | 47 +++++++------------ 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index 452b709f4..dc9383b1f 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -71,7 +71,6 @@ map.set(columnName, width); return map; }); - console.log(columnName, width); } lastHeight = tableContainer.clientHeight; // For recalculateVisibleRowCount 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 c51fcaa0b..800aa718c 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -72,6 +72,8 @@ export class RunnerApi { }); } + // --- SDS code generation --- + private sdsStringForMissingValueRatioByColumnName( columnName: string, tablePlaceholder: string, @@ -92,10 +94,6 @@ export class RunnerApi { return 'val ' + newPlaceholderName + ' = ' + tableIdentifier + '.column_names; \n'; } - private sdsStringForColumns(tableIdentifier: string, newPlaceholderName: string): string { - return 'val ' + newPlaceholderName + ' = ' + tableIdentifier + '.to_columns(); \n'; - } - private sdsStringForIDnessByColumnName(columnName: string, tablePlaceholder: string, newPlaceholderName: string) { return ( 'val ' + newPlaceholderName + ' = ' + tablePlaceholder + '.get_column("' + columnName + '").idness(); \n' @@ -118,6 +116,8 @@ export class RunnerApi { ); } + // --- Placeholder handling --- + private genPlaceholderName(): string { return CODEGEN_PREFIX + this.placeholderCounter++; } @@ -212,9 +212,7 @@ export class RunnerApi { // Generate SDS code to get missing value ratio for each column for (let i = 0; i < columns.length; i++) { const newMvPlaceholderName = this.genPlaceholderName(); - missingValueRatioMap.set(newMvPlaceholderName, 0); columnNameToPlaceholderMVNameMap.set(columns[i]![1].name, newMvPlaceholderName); - sdsStrings += this.sdsStringForMissingValueRatioByColumnName( columns[i]![1].name, table.name, @@ -224,15 +222,14 @@ export class RunnerApi { // Only need to check IDness for non-numerical columns if (columns[i]![1].type !== 'numerical') { const newIDnessPlaceholderName = this.genPlaceholderName(); - idnessMap.set(newIDnessPlaceholderName, 1); columnNameToPlaceholderIDnessNameMap.set(columns[i]![1].name, newIDnessPlaceholderName); - sdsStrings += this.sdsStringForIDnessByColumnName( columns[i]![1].name, table.name, newIDnessPlaceholderName, ); + // Find unique values let uniqueValues = new Set(); for (let j = 0; j < columns[i]![1].values.length; j++) { uniqueValues.add(columns[i]![1].values[j]); @@ -243,10 +240,9 @@ export class RunnerApi { } } + // Histogram for numerical columns or categorical columns with 4-10 unique values const newHistogramPlaceholderName = this.genPlaceholderName(); - histogramMap.set(newHistogramPlaceholderName, undefined); columnNameToPlaceholderHistogramNameMap.set(columns[i]![1].name, newHistogramPlaceholderName); - sdsStrings += this.sdsStringForHistogramByColumnName( columns[i]![1].name, table.name, @@ -259,7 +255,7 @@ export class RunnerApi { await this.addToAndExecutePipeline(pipelineExecutionId, sdsStrings); // Get missing value ratio for each column - for (const [placeholderName] of missingValueRatioMap) { + for (const [, placeholderName] of columnNameToPlaceholderMVNameMap) { const missingValueRatio = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); if (missingValueRatio) { missingValueRatioMap.set(placeholderName, missingValueRatio as number); @@ -267,7 +263,7 @@ export class RunnerApi { } // Get IDness for each column - for (const [placeholderName] of idnessMap) { + for (const [, placeholderName] of columnNameToPlaceholderIDnessNameMap) { const idness = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); if (idness) { idnessMap.set(placeholderName, idness as number); @@ -275,16 +271,17 @@ export class RunnerApi { } // Get histogram for each column - for (const [placeholderName] of histogramMap) { + for (const [, placeholderName] of columnNameToPlaceholderHistogramNameMap) { const histogram = await this.getPlaceholderValue(placeholderName, pipelineExecutionId); if (histogram) { histogramMap.set(placeholderName, histogram as Base64Image); } } - // Create profiling data, interpret numerical values and color them + // Create profiling data const profiling: { columnName: string; profiling: Profiling }[] = []; for (const column of columns) { + // Base info for the top of the profiling const missingValuesRatio = missingValueRatioMap.get(columnNameToPlaceholderMVNameMap.get(column[1].name)!)! * 100; @@ -309,7 +306,7 @@ export class RunnerApi { if (uniqueValues <= 3) { // Can display each separate percentages of unique values - // Find all unique values + // Find all unique values and count them const uniqueValueCounts = new Map(); for (let i = 0; i < column[1].values.length; i++) { if (column[1].values[i]) @@ -340,6 +337,7 @@ export class RunnerApi { }, }); } else if (uniqueValues <= 10) { + // Display histogram for 4-10 unique values, has to match the condition above where histogram is generated const histogram = histogramMap.get(columnNameToPlaceholderHistogramNameMap.get(column[1].name)!)!; profiling.push({ @@ -353,7 +351,7 @@ export class RunnerApi { }, }); } else { - // Display only the number of unique values + // Display only the number of unique values vs total valid values profiling.push({ columnName: column[1].name, profiling: { @@ -371,9 +369,9 @@ export class RunnerApi { Math.round( column[1].values.length * (1 - - missingValueRatioMap.get( + (missingValueRatioMap.get( columnNameToPlaceholderMVNameMap.get(column[1].name)!, - )!), + ) || 0)), ) + ' Total Valids', interpretation: 'default', }, @@ -382,6 +380,7 @@ export class RunnerApi { }); } } else { + // Display histogram for numerical columns const histogram = histogramMap.get(columnNameToPlaceholderHistogramNameMap.get(column[1].name)!)!; profiling.push({ @@ -408,16 +407,4 @@ export class RunnerApi { const columnNames = await this.getPlaceholderValue(newPlaceholderName, pipelineExecutionId); return columnNames as string[]; } - - // Doesn't work as columns cannot be serialized yet by Runner - // public async getColumns(tableIdentifier: string): Promise<{ columns: any; placeholderName: string }> { - // const newPlaceholderName = this.randomPlaceholderName(); - // const columnsSdsCode = this.sdsStringForColumns(tableIdentifier, newPlaceholderName); - // const pipelineExecutionId = crypto.randomUUID(); - // await this.addToAndExecutePipeline(pipelineExecutionId, columnsSdsCode); - // const columns = await this.getPlaceholderValue(newPlaceholderName, pipelineExecutionId); - // // eslint-disable-next-line no-console - // console.log(columns); - // return { columns, placeholderName: newPlaceholderName }; - // } } From 6e3ab4f2ddb8532794b9e37600ad6631a98e2356 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Sat, 30 Mar 2024 14:35:17 +0100 Subject: [PATCH 26/33] fix: image in top of profiling --- .../src/components/profiling/ProfilingInfo.svelte | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte index b2e92b7f1..703a4d69b 100644 --- a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte +++ b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte @@ -37,6 +37,15 @@ > {profilingTopItem.value}
+ {:else if profilingTopItem.type === 'image'} +
+ profiling histogram +
{/if} {/each}
From 6c6259bfe7a377d9de8e2bc95492f2bdd4d7ced7 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Sat, 30 Mar 2024 15:40:11 +0100 Subject: [PATCH 27/33] refactor: comments for readability and future --- .../src/components/TableView.svelte | 2 +- .../src/extension/eda/apis/runnerApi.ts | 1 + .../src/extension/eda/edaPanel.ts | 35 ++++--------------- 3 files changed, 8 insertions(+), 30 deletions(-) diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index dc9383b1f..7a90f2ecf 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -524,7 +524,7 @@ if (showProfiling) { setTimeout(() => { fullHeadBackground.style.height = 2 * rowHeight + profilingInfo.clientHeight + 'px'; - }, 700); + }, 700); // 700ms is the transition time of the profiling info opening/incresing in height } else { fullHeadBackground.style.height = rowHeight * 2 + 'px'; } 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 800aa718c..cdf99fc12 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -20,6 +20,7 @@ export class RunnerApi { this.pipelineName = pipelineName; getPipelineDocument(this.pipelinePath).then((doc) => { // Get here to avoid issues because of chanigng file + // Make sure to create new instance of RunnerApi if pipeline execution of fresh pipeline is needed this.baseDocument = doc; }); } diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index 365155777..8a813b016 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -129,32 +129,7 @@ export class EDAPanel { panel.tableName = tableName; EDAPanel.panelsMap.set(tableIdentifier, panel); - // // Have to update and construct state as table placeholder could've changed in code - // panel.updateHtmlDone = false; - // panel._update(); - // await panel.waitForUpdateHtmlDone(10000); - - // // Get and send state - // const stateInfo = await panel.constructCurrentState(); - // webviewApi.postMessage(panel!.panel.webview, { - // command: 'setWebviewState', - // value: stateInfo.state, - // }); - - // // If not present, get and send profiling - // if ( - // !stateInfo.fromExisting || - // !stateInfo.state.table || - // !stateInfo.state - // .table!.columns.map((c) => c[1].profiling) - // .find((p) => p.bottom.length > 0 || p.top.length > 0) - // ) { - // const profiling = await panel.runnerApi.getProfiling(stateInfo.state.table!); - // webviewApi.postMessage(panel!.panel.webview, { - // command: 'setProfiling', - // value: profiling, - // }); - // } + // TODO: Display disclaimer that data can be outdated and show refresh button return; } else { // Otherwise, create a new panel. @@ -195,12 +170,14 @@ export class EDAPanel { value: stateInfo.state, }); + // TODO: if from existing state, show disclaimer that updated data is loading and execute pipeline + history + profiling and send + if ( !stateInfo.fromExisting || !stateInfo.state.table || - !stateInfo.state - .table!.columns.map((c) => c[1].profiling) - .find((p) => p.bottom.length > 0 || p.top.length > 0) + !stateInfo.state.table!.columns.find( + (c) => c[1].profiling.bottom.length > 0 || c[1].profiling.top.length > 0, + ) ) { const profiling = await EDAPanel.panelsMap .get(tableIdentifier)! From e8e792afc394bcfa7e6e2a1d4a5d8062bfb488ff Mon Sep 17 00:00:00 2001 From: Jonas B Date: Sat, 30 Mar 2024 15:53:00 +0100 Subject: [PATCH 28/33] fix: mega-linter issues --- .../safe-ds-eda/src/components/TableView.svelte | 14 +++++++------- .../components/column-filters/ColumnFilters.svelte | 2 +- .../src/extension/eda/apis/runnerApi.ts | 1 + 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index 7a90f2ecf..cc6897e76 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -13,7 +13,7 @@ } from '../../types/state'; import ProfilingInfo from './profiling/ProfilingInfo.svelte'; import ColumnFilters from './column-filters/ColumnFilters.svelte'; - import { derived, writable, type Readable, get } from 'svelte/store'; + import { derived, writable, get } from 'svelte/store'; export let sidebarWidth: number; @@ -442,15 +442,16 @@ if (event.target instanceof HTMLElement) { let element = event.target; - const hasParentWithClass = (element: HTMLElement, className: string) => { - while (element && element !== document.body) { - if (element.classList.contains(className)) { + const hasParentWithClass = (elementToScan: HTMLElement, className: string) => { + let currentElement: HTMLElement = elementToScan; + while (currentElement && currentElement !== document.body) { + if (currentElement.classList.contains(className)) { return true; } - if (!element.parentElement) { + if (!currentElement.parentElement) { return false; } - element = element.parentElement; + currentElement = currentElement.parentElement; } return false; }; @@ -666,7 +667,6 @@ on:mousedown={(event) => handleColumnInteractionStart(event, index)} on:mousemove={(event) => throttledHandleReorderDragOver(event, index)} >{column[1].name} -
{ - console.log((event.target as HTMLSelectElement).value); + // TODO: Implement }; 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 cdf99fc12..552756562 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -21,6 +21,7 @@ export class RunnerApi { getPipelineDocument(this.pipelinePath).then((doc) => { // Get here to avoid issues because of chanigng file // Make sure to create new instance of RunnerApi if pipeline execution of fresh pipeline is needed + // (e.g. launching of extension on table with existing state but no current panel) this.baseDocument = doc; }); } From ff8e14ec0ebd374f13a7e974425a3744a6e3693d Mon Sep 17 00:00:00 2001 From: Jonas B Date: Sat, 30 Mar 2024 15:57:38 +0100 Subject: [PATCH 29/33] fix: small annoying megalinter fix --- .../src/components/column-filters/ColumnFilters.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-ds-eda/src/components/column-filters/ColumnFilters.svelte b/packages/safe-ds-eda/src/components/column-filters/ColumnFilters.svelte index e0f33ab39..50a0cc14d 100644 --- a/packages/safe-ds-eda/src/components/column-filters/ColumnFilters.svelte +++ b/packages/safe-ds-eda/src/components/column-filters/ColumnFilters.svelte @@ -3,7 +3,7 @@ export let possibleFilters: PossibleColumnFilter[]; - const handleSpecificValueFilterChange = (event: Event) => { + const handleSpecificValueFilterChange = (_event: Event) => { // TODO: Implement }; From 0bac77d5615aa23483afef7a10109c993c770ef3 Mon Sep 17 00:00:00 2001 From: Jonas B Date: Tue, 2 Apr 2024 22:14:29 +0200 Subject: [PATCH 30/33] fix: pr comments - profiling interpretation is now applied as class, that sets the color of text - profiling not top and bottom anymore, but set validRatio and missingRatio and then bottom with "other", as this does not dictate representation but info thats not depend. on type of col vs other as depend. - profiling object now optional for col, so easier to check if profiling present or not - changed some inline stylings to style:property directives, so more readable, but not where style property to complex or too many - some refactoring - image no interpretation anymore, not needed --- packages/safe-ds-eda/src/App.svelte | 4 +- .../src/components/TableView.svelte | 60 ++++++---- .../ColumnFilters.svelte | 0 .../components/profiling/ProfilingInfo.svelte | 104 ++++++++---------- packages/safe-ds-eda/types/state.d.ts | 40 ++++--- .../src/extension/eda/apis/runnerApi.ts | 33 +++--- .../src/extension/eda/edaPanel.ts | 4 +- 7 files changed, 126 insertions(+), 119 deletions(-) rename packages/safe-ds-eda/src/components/{column-filters => columnFilters}/ColumnFilters.svelte (100%) diff --git a/packages/safe-ds-eda/src/App.svelte b/packages/safe-ds-eda/src/App.svelte index 322e16e30..59541c42e 100644 --- a/packages/safe-ds-eda/src/App.svelte +++ b/packages/safe-ds-eda/src/App.svelte @@ -37,9 +37,9 @@
-
+
-
+
diff --git a/packages/safe-ds-eda/src/components/TableView.svelte b/packages/safe-ds-eda/src/components/TableView.svelte index cc6897e76..ff5b262b0 100644 --- a/packages/safe-ds-eda/src/components/TableView.svelte +++ b/packages/safe-ds-eda/src/components/TableView.svelte @@ -6,14 +6,15 @@ import ErrorIcon from '../icons/Error.svelte'; import FilterIcon from '../icons/Filter.svelte'; import type { + Column, PossibleColumnFilter, Profiling, ProfilingDetail, ProfilingDetailStatistical, } from '../../types/state'; import ProfilingInfo from './profiling/ProfilingInfo.svelte'; - import ColumnFilters from './column-filters/ColumnFilters.svelte'; import { derived, writable, get } from 'svelte/store'; + import ColumnFilters from './columnFilters/ColumnFilters.svelte'; export let sidebarWidth: number; @@ -31,7 +32,7 @@ minTableWidth = 0; numRows = 0; maxProfilingItemCount = 0; - $currentState.table.columns.forEach((column) => { + $currentState.table.columns.forEach((column: [number, Column]) => { if (column[1].values.length > numRows) { numRows = column[1].values.length; } @@ -39,9 +40,11 @@ // Find which is the talles profiling type present in this table to adjust which profilings to give small height to, to have them adhere to good spacing // (cannot give to tallest one, as then it will all be small) - if (column[1].profiling.top.length > 0 || column[1].profiling.bottom.length > 0) { + if (column[1].profiling) { let profilingItemCount = 0; - for (const profilingItem of column[1].profiling.top.concat(column[1].profiling.bottom)) { + if (column[1].profiling.validRatio) profilingItemCount += 1; + if (column[1].profiling.missingRatio) profilingItemCount += 1; + for (const profilingItem of column[1].profiling.other) { profilingItemCount += calcProfilingItemValue(profilingItem); } if (profilingItemCount > maxProfilingItemCount) { @@ -535,7 +538,10 @@ const getOptionalProfilingHeight = function (profiling: Profiling): string { let profilingItemCount = 0; - for (const profilingItem of profiling.top.concat(profiling.bottom)) { + if (profiling.validRatio) profilingItemCount += 1; + if (profiling.missingRatio) profilingItemCount += 1; + + for (const profilingItem of profiling.other) { profilingItemCount += calcProfilingItemValue(profilingItem); } @@ -559,7 +565,14 @@ const hasProfilingErrors = derived(currentState, ($currentState) => { if (!$currentState.table) return false; for (const column of $currentState.table!.columns) { - for (const profilingItem of column[1].profiling.top.concat(column[1].profiling.bottom)) { + if (!column[1].profiling) return false; + if ( + column[1].profiling.missingRatio?.interpretation === 'error' || + column[1].profiling.validRatio?.interpretation === 'error' + ) { + return true; + } + for (const profilingItem of column[1].profiling.other) { if (profilingItem.type === 'numerical' && profilingItem.interpretation === 'error') { return true; } @@ -573,15 +586,15 @@ const column = $currentState.table.columns[columnIndex][1]; + if (!column.profiling) return []; + const possibleColumnFilters: PossibleColumnFilter[] = []; if (column.type === 'categorical') { - const profilingCategories: ProfilingDetailStatistical[] = column.profiling.bottom - .concat(column.profiling.top) - .filter( - (profilingItem) => - profilingItem.type === 'numerical' && profilingItem.interpretation === 'category', - ) as ProfilingDetailStatistical[]; + const profilingCategories: ProfilingDetailStatistical[] = column.profiling.other.filter( + (profilingItem: ProfilingDetail) => + profilingItem.type === 'numerical' && profilingItem.interpretation === 'category', + ) as ProfilingDetailStatistical[]; // If there is distinct categories in profiling, use those as filter options, else use search string if (profilingCategories.length > 0) { @@ -644,13 +657,14 @@
-
+
handleColumnInteractionStart(event, index)} on:mousemove={(event) => throttledHandleReorderDragOver(event, index)} >{column[1].name} @@ -753,7 +693,7 @@
throttledHandleReorderDragOver(event, 0)} @@ -765,7 +705,7 @@ style="height: {showProfiling && (column[1].profiling.top.length !== 0 || column[1].profiling.bottom.length !== 0) ? getOptionalProfilingHeight(column[1].profiling) - : ''};" + : ''}; {isReorderDragging && dragStartIndex === index ? 'display: none;' : ''}" on:mousemove={(event) => throttledHandleReorderDragOver(event, index)} >
@@ -807,12 +747,13 @@
throttledHandleReorderDragOver(event, i + 1)} + on:mousemove={(event) => throttledHandleReorderDragOver(event, index + 1)} > throttledHandleReorderDragOver(event, index)} class:selectedColumn={selectedColumnIndexes.includes(index) || @@ -865,6 +807,14 @@ No data {/if} + {#if draggedColumnName} + {draggedColumnName} +
- + - + - + {#each Array(Math.min(visibleEnd, numRows) - visibleStart) as _, i} - + diff --git a/packages/safe-ds-eda/src/components/column-filters/ColumnFilters.svelte b/packages/safe-ds-eda/src/components/columnFilters/ColumnFilters.svelte similarity index 100% rename from packages/safe-ds-eda/src/components/column-filters/ColumnFilters.svelte rename to packages/safe-ds-eda/src/components/columnFilters/ColumnFilters.svelte diff --git a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte index 703a4d69b..e1abe176d 100644 --- a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte +++ b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte @@ -3,77 +3,37 @@ export let profiling: Profiling; export let imageWidth: number = 200; - - const getProfilingItemColor = function (profilingItem: ProfilingDetail) { - if (profilingItem.interpretation === 'good') { - return 'var(--primary-color)'; - } else if (profilingItem.interpretation === 'error') { - return 'var(--error-color)'; - } else if (profilingItem.interpretation === 'warn') { - return 'var(--warn-color)'; - } else if (profilingItem.interpretation === 'bold') { - return 'var(--font-dark)'; - } else if (profilingItem.interpretation === 'default') { - return 'var(--font-light)'; - } else if (profilingItem.interpretation === 'category') { - return 'var(--font-light)'; - } else { - return 'var(--font-light)'; - } - };
- {#each profiling.top as profilingTopItem} - {#if profilingTopItem.type === 'text'} -
- {profilingTopItem.value} -
- {:else if profilingTopItem.type === 'numerical'} -
- {profilingTopItem.name}: - {profilingTopItem.value} -
- {:else if profilingTopItem.type === 'image'} -
- profiling histogram -
- {/if} - {/each} +
+ {profiling.validRatio.name}: + {profiling.validRatio.value} +
+
+ {profiling.missingRatio.name}: + {profiling.missingRatio.value} +
- {#each profiling.bottom as profilingBottomItem} - {#if profilingBottomItem.type === 'text'} + {#each profiling.other as profilingItem} + {#if profilingItem.type === 'text'}
- {profilingBottomItem.value} + {profilingItem.value}
- {:else if profilingBottomItem.type === 'numerical'} + {:else if profilingItem.type === 'numerical'}
- {profilingBottomItem.name}: - {profilingBottomItem.value} + {profilingItem.name}: + {profilingItem.value}
- {:else if profilingBottomItem.type === 'image'} + {:else if profilingItem.type === 'image'}
profiling histogram
{/if} @@ -95,7 +55,7 @@ margin-bottom: 1px; } - .profilingItemFirst { + .profilingItemKey { margin-right: 10px; } @@ -109,8 +69,32 @@ } .profilingImage { - height: 150px; + height: 150px; /* default height, profiling height calculation works off this value */ object-fit: cover; object-position: left; } + + .good { + color: var(--primary-color); + } + + .error { + color: var(--error-color); + } + + .warn { + color: var(--warn-color); + } + + .important { + color: var(--font-dark); + } + + .default { + color: var(--font-light); + } + + .category { + color: var(--font-light); + } diff --git a/packages/safe-ds-eda/types/state.d.ts b/packages/safe-ds-eda/types/state.d.ts index b399d0e77..4c42b8907 100644 --- a/packages/safe-ds-eda/types/state.d.ts +++ b/packages/safe-ds-eda/types/state.d.ts @@ -71,21 +71,25 @@ export interface Table { // ------------ Types for the Profiling ----------- export interface Profiling { - top: ProfilingDetail[]; - bottom: ProfilingDetail[]; + validRatio: ProfilingDetailStatistical; + missingRatio: ProfilingDetailStatistical; + other: ProfilingDetail[]; } export interface ProfilingDetailBase { type: 'numerical' | 'image' | 'name'; value: string; - interpretation: 'warn' | 'error' | 'default' | 'bold' | 'good'; } -export interface ProfilingDetailStatistical extends ProfilingDetailBase { +interface ProfilingDetailText extends ProfilingDetailBase { + interpretation: 'warn' | 'error' | 'default' | 'important' | 'good'; +} + +export interface ProfilingDetailStatistical extends ProfilingDetailText { type: 'numerical'; name: string; value: string; - interpretation: ProfilingDetailBase['interpretation'] | 'category'; // 'category' needed for filters, to show distinct values + interpretation: ProfilingDetailText['interpretation'] | 'category'; // 'category' needed for filters, to show distinct values } export interface ProfilingDetailImage extends ProfilingDetailBase { @@ -93,7 +97,7 @@ export interface ProfilingDetailImage extends ProfilingDetailBase { value: Base64Image; } -export interface ProfilingDetailName extends ProfilingDetailBase { +export interface ProfilingDetailName extends ProfilingDetailText { type: 'text'; value: string; } @@ -108,7 +112,7 @@ export interface ColumnBase { hidden: boolean; highlighted: boolean; appliedSort: 'asc' | 'desc' | null; - profiling: Profiling; + profiling?: Profiling; } export interface NumericalColumn extends ColumnBase { @@ -134,24 +138,23 @@ export interface ColumnFilterBase extends FilterBase { columnName: string; } -export interface SearchStringFilter extends ColumnFilterBase { +export interface PossibleSearchStringFilter extends ColumnFilterBase { type: 'searchString'; - searchString: string; } -export interface PossibleSearchStringFilter extends ColumnFilterBase { - type: 'searchString'; +export interface SearchStringFilter extends PossibleSearchStringFilter { + searchString: string; } -export interface ValueRangeFilter extends ColumnFilterBase { +export interface PossibleValueRangeFilter extends ColumnFilterBase { type: 'valueRange'; min: number; max: number; } -export interface SpecificValueFilter extends ColumnFilterBase { - type: 'specificValue'; - value: string; +export interface ValueRangeFilter extends PossibleValueRangeFilter { + currentMin: number; + currentMax: number; } export interface PossibleSpecificValueFilter extends ColumnFilterBase { @@ -159,10 +162,15 @@ export interface PossibleSpecificValueFilter extends ColumnFilterBase { values: string[]; } +export interface SpecificValueFilter extends ColumnFilterBase { + type: 'specificValue'; + value: string; +} + export type NumericalFilter = ValueRangeFilter; export type CategoricalFilter = SearchStringFilter | SpecificValueFilter; -export type PossibleColumnFilter = ValueRangeFilter | PossibleSearchStringFilter | PossibleSpecificValueFilter; +export type PossibleColumnFilter = PossibleValueRangeFilter | PossibleSearchStringFilter | PossibleSpecificValueFilter; export interface TableFilter extends FilterBase { type: 'hideMissingValueColumns' | 'hideNonNumericalColumns' | 'hideDuplicateRows' | 'hideRowsWithOutliers'; 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 552756562..13814c1b9 100644 --- a/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts +++ b/packages/safe-ds-vscode/src/extension/eda/apis/runnerApi.ts @@ -183,7 +183,6 @@ export class RunnerApi { highlighted: false, appliedFilters: [], appliedSort: null, - profiling: { top: [], bottom: [] }, coloredHighLow: false, }; table.columns.push([i++, column]); @@ -331,9 +330,10 @@ export class RunnerApi { profiling.push({ columnName: column[1].name, profiling: { - top: [validRatio, missingRatio], - bottom: [ - { type: 'text', value: 'Categorical', interpretation: 'bold' }, + validRatio, + missingRatio, + other: [ + { type: 'text', value: 'Categorical', interpretation: 'important' }, ...uniqueProfilings, ], }, @@ -345,10 +345,11 @@ export class RunnerApi { profiling.push({ columnName: column[1].name, profiling: { - top: [validRatio, missingRatio], - bottom: [ - { type: 'text', value: 'Categorical', interpretation: 'bold' }, - { type: 'image', value: histogram, interpretation: 'default' }, + validRatio, + missingRatio, + other: [ + { type: 'text', value: 'Categorical', interpretation: 'important' }, + { type: 'image', value: histogram }, ], }, }); @@ -357,9 +358,10 @@ export class RunnerApi { profiling.push({ columnName: column[1].name, profiling: { - top: [validRatio, missingRatio], - bottom: [ - { type: 'text', value: 'Categorical', interpretation: 'bold' }, + validRatio, + missingRatio, + other: [ + { type: 'text', value: 'Categorical', interpretation: 'important' }, { type: 'text', value: uniqueValues + ' Distincts', @@ -388,10 +390,11 @@ export class RunnerApi { profiling.push({ columnName: column[1].name, profiling: { - top: [validRatio, missingRatio], - bottom: [ - { type: 'text', value: 'Numerical', interpretation: 'bold' }, - { type: 'image', value: histogram, interpretation: 'default' }, + validRatio, + missingRatio, + other: [ + { type: 'text', value: 'Numerical', interpretation: 'important' }, + { type: 'image', value: histogram }, ], }, }); diff --git a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts index 8a813b016..257efd133 100644 --- a/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts +++ b/packages/safe-ds-vscode/src/extension/eda/edaPanel.ts @@ -175,9 +175,7 @@ export class EDAPanel { if ( !stateInfo.fromExisting || !stateInfo.state.table || - !stateInfo.state.table!.columns.find( - (c) => c[1].profiling.bottom.length > 0 || c[1].profiling.top.length > 0, - ) + !stateInfo.state.table!.columns.find((c) => !c[1].profiling) ) { const profiling = await EDAPanel.panelsMap .get(tableIdentifier)! From 4d37f5f9754a806b1baca1dec244d56e58e68f3e Mon Sep 17 00:00:00 2001 From: Jonas B Date: Tue, 2 Apr 2024 22:18:06 +0200 Subject: [PATCH 31/33] fix: megalinter --- .../safe-ds-eda/src/components/profiling/ProfilingInfo.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte index e1abe176d..d4d3b4056 100644 --- a/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte +++ b/packages/safe-ds-eda/src/components/profiling/ProfilingInfo.svelte @@ -1,5 +1,5 @@
throttledHandleReorderDragOver(event, 0)}>#
throttledHandleReorderDragOver(event, 0)} @@ -705,14 +719,13 @@ throttledHandleReorderDragOver(event, index)} >
- {#if column[1].profiling.top.length === 0 && column[1].profiling.bottom.length === 0} + {#if !column[1].profiling}
Loading ...
{:else} @@ -727,7 +740,7 @@ >
throttledHandleReorderDragOver(event, 0)} @@ -770,7 +783,7 @@
throttledHandleReorderDragOver(event, 0)} @@ -780,7 +793,7 @@ > {#each $currentState.table.columns as column, index} throttledHandleReorderDragOver(event, index)} class:selectedColumn={selectedColumnIndexes.includes(index) || @@ -813,7 +826,8 @@ {#if draggedColumnName} {draggedColumnName}