From ab409febf5bb24271a0cfb446af4e3eb7a5fe9d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Gr=C3=BCnder?= Date: Sun, 15 Feb 2026 16:53:05 +0100 Subject: [PATCH 01/19] add index db and review to step 5 --- src/app/canvas/glyph-canvas.component.ts | 4 +- src/app/menubar/menubar.component.html | 17 +- src/app/menubar/menubar.component.scss | 29 + src/app/menubar/menubar.component.ts | 18 + .../preprocessing-wizard.component.html | 5 +- .../preprocessing-wizard.component.ts | 13 +- .../services/preprocessing.service.ts | 6 +- .../shared/constants/step-info.ts | 6 +- ...tep4-visualization-settings.component.html | 1169 +++++++---------- ...tep4-visualization-settings.component.scss | 463 ------- .../step4-visualization-settings.component.ts | 466 +------ .../step5-review-processing.component.html | 264 ++++ .../step5-review-processing.component.scss | 418 ++++++ .../step5-review-processing.component.ts | 423 ++++++ src/app/services/dataprovider.service.ts | 147 ++- src/app/services/dataset-storage.service.ts | 102 ++ 16 files changed, 1905 insertions(+), 1645 deletions(-) create mode 100644 src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.html create mode 100644 src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.scss create mode 100644 src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.ts create mode 100644 src/app/services/dataset-storage.service.ts diff --git a/src/app/canvas/glyph-canvas.component.ts b/src/app/canvas/glyph-canvas.component.ts index 59d493b..30e9c5f 100644 --- a/src/app/canvas/glyph-canvas.component.ts +++ b/src/app/canvas/glyph-canvas.component.ts @@ -880,8 +880,10 @@ export class GlyphCanvasComponent implements OnInit, AfterViewInit, OnDestroy { }, 1000); if (this.renderGlyphsCallCount > this.MAX_RENDER_CALLS_PER_SECOND) { - console.error(`Infinite render loop detected (${this.renderGlyphsCallCount} calls/sec). Breaking loop.`); + console.warn(`Rapid render calls detected (${this.renderGlyphsCallCount} calls/sec). Deferring render.`); this.renderGlyphsCallCount = 0; + // Defer a final render so the canvas doesn't stay blank + setTimeout(() => this.renderGlyphs(true), 200); return; } diff --git a/src/app/menubar/menubar.component.html b/src/app/menubar/menubar.component.html index 57dbdf8..3e2100a 100644 --- a/src/app/menubar/menubar.component.html +++ b/src/app/menubar/menubar.component.html @@ -22,9 +22,20 @@
Data - +
+ + +
diff --git a/src/app/menubar/menubar.component.scss b/src/app/menubar/menubar.component.scss index 5ae0e7c..4a75468 100644 --- a/src/app/menubar/menubar.component.scss +++ b/src/app/menubar/menubar.component.scss @@ -71,6 +71,35 @@ } } +.dataset-selector { + display: flex; + align-items: center; + gap: 4px; +} + +.delete-btn { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + background: transparent; + border-radius: 8px; + cursor: pointer; + color: #999; + padding: 0; + + &:hover { + background: #fee2e2; + color: #dc2626; + } + + .material-icons { + font-size: 18px; + } +} + .data-control { display: flex; align-items: center; diff --git a/src/app/menubar/menubar.component.ts b/src/app/menubar/menubar.component.ts index 77b9703..8e7a866 100644 --- a/src/app/menubar/menubar.component.ts +++ b/src/app/menubar/menubar.component.ts @@ -23,6 +23,7 @@ export class MenuBarComponent implements OnInit, OnDestroy { showWizard = false; hasData = false; datasetNames: string[] = []; + datasetEntries: { name: string; source: string }[] = []; selectedDataset: string | null = null; private dataSub = new Subscription(); @@ -37,6 +38,10 @@ export class MenuBarComponent implements OnInit, OnDestroy { this.dataProvider.dataSetCollectionSubject$.subscribe(collection => { this.hasData = !!collection && collection.length > 0 && collection.at(0)?.dataset != ""; this.datasetNames = collection.map(entry => entry.dataset); + this.datasetEntries = collection.map(entry => ({ + name: entry.dataset, + source: entry.source + })); })); this.dataSub.add( this.configService.loadedDataSubject$.subscribe(loaded => { @@ -78,4 +83,17 @@ export class MenuBarComponent implements OnInit, OnDestroy { fitAll() { this.configService.toggleFitToScreen(); } + + isUserDataset(name: string | null): boolean { + if (!name) return false; + const entry = this.datasetEntries.find(e => e.name === name); + return !!entry && entry.source !== 'local'; + } + + async onDeleteDataset(name: string | null): Promise { + if (!name || !this.isUserDataset(name)) return; + if (confirm(`Delete dataset "${name}"? This cannot be undone.`)) { + await this.dataProvider.deleteDataset(name); + } + } } diff --git a/src/app/preprocessing-wizard/preprocessing-wizard.component.html b/src/app/preprocessing-wizard/preprocessing-wizard.component.html index 239702d..75a6262 100644 --- a/src/app/preprocessing-wizard/preprocessing-wizard.component.html +++ b/src/app/preprocessing-wizard/preprocessing-wizard.component.html @@ -42,7 +42,10 @@

Data Preprocessing Wizard

} @case (3) { - + + } + @case (4) { + } } diff --git a/src/app/preprocessing-wizard/preprocessing-wizard.component.ts b/src/app/preprocessing-wizard/preprocessing-wizard.component.ts index 96c00b0..efb7711 100644 --- a/src/app/preprocessing-wizard/preprocessing-wizard.component.ts +++ b/src/app/preprocessing-wizard/preprocessing-wizard.component.ts @@ -7,6 +7,7 @@ import { Step1UploadComponent } from './steps/step1-upload/step1-upload.componen import { Step2ColumnSelectionComponent } from './steps/step2-column-selection/step2-column-selection.component'; import { Step3ConfigureDataFeaturesComponent } from './steps/step3-configure-data-features/step3-configure-data-features.component'; import { Step4VisualizationSettingsComponent } from './steps/step4-visualization-settings/step4-visualization-settings.component'; +import { Step5ReviewProcessingComponent } from './steps/step5-review-processing/step5-review-processing.component'; import { DataProfile } from './models/column-statistics'; @Component({ @@ -18,7 +19,8 @@ import { DataProfile } from './models/column-statistics'; Step1UploadComponent, Step2ColumnSelectionComponent, Step3ConfigureDataFeaturesComponent, - Step4VisualizationSettingsComponent + Step4VisualizationSettingsComponent, + Step5ReviewProcessingComponent ], templateUrl: './preprocessing-wizard.component.html', styleUrl: './preprocessing-wizard.component.scss' @@ -38,7 +40,8 @@ export class PreprocessingWizardComponent implements OnInit, OnDestroy { { label: 'Upload Data', completed: false }, { label: 'Select Columns', completed: false }, { label: 'Configure Data & Features', completed: false }, - { label: 'Visualization Settings', completed: false } + { label: 'Visualization Settings', completed: false }, + { label: 'Review & Process', completed: false } ]; constructor(private preprocessingService: PreprocessingService) { } @@ -95,8 +98,8 @@ export class PreprocessingWizardComponent implements OnInit, OnDestroy { this.steps[0].completed = state.dataProfile !== null; this.steps[1].completed = state.columnConfigs.size > 0 && this.highestStepVisited > 1; this.steps[2].completed = this.highestStepVisited > 2; - // Step 3 (index 3) is completed either after processing OR if user has visited it - this.steps[3].completed = state.processedDataset !== null || this.highestStepVisited >= 3; + this.steps[3].completed = this.highestStepVisited > 3; + this.steps[4].completed = state.processedDataset !== null; } onStepClick(step: number): void { @@ -127,6 +130,8 @@ export class PreprocessingWizardComponent implements OnInit, OnDestroy { case 2: // Configure Data & Features return true; case 3: // Visualization Settings + return true; + case 4: // Review & Process return false; // Final step - processing happens here default: return false; diff --git a/src/app/preprocessing-wizard/services/preprocessing.service.ts b/src/app/preprocessing-wizard/services/preprocessing.service.ts index 67445a9..64b9379 100644 --- a/src/app/preprocessing-wizard/services/preprocessing.service.ts +++ b/src/app/preprocessing-wizard/services/preprocessing.service.ts @@ -77,16 +77,16 @@ export class PreprocessingService { return this.stateSubject.getValue(); } - // Step navigation (4 steps: 0-3) + // Step navigation (5 steps: 0-4) public goToStep(step: number): void { - if (step >= 0 && step <= 3) { + if (step >= 0 && step <= 4) { this.updateState({ currentStep: step }); } } public nextStep(): void { const current = this.currentState.currentStep; - if (current < 3) { + if (current < 4) { this.goToStep(current + 1); } } diff --git a/src/app/preprocessing-wizard/shared/constants/step-info.ts b/src/app/preprocessing-wizard/shared/constants/step-info.ts index 903ce05..ef7a6de 100644 --- a/src/app/preprocessing-wizard/shared/constants/step-info.ts +++ b/src/app/preprocessing-wizard/shared/constants/step-info.ts @@ -23,6 +23,10 @@ export const STEP_INFO: Record = { }, 3: { title: 'Visualization Settings', - purpose: 'Select color and glyph features, choose projection methods, review your configuration, and process your data.' + purpose: 'Select color and glyph features, and choose projection methods.' + }, + 4: { + title: 'Review & Process', + purpose: 'Review your configuration summary and process the data for visualization.' } }; diff --git a/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.html b/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.html index 0358506..29c6817 100644 --- a/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.html +++ b/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.html @@ -4,802 +4,519 @@

{{ stepInfo.title }}

{{ stepInfo.purpose }}

- -
- - -
- - @if (activeSection === 'configure') { -
- -
-

- palette - Color Feature (Optional) - -

-

Select which feature will be used to color data points in visualizations

+
+ +
+

+ palette + Color Feature (Optional) + +

+

Select which feature will be used to color data points in visualizations

+ +
+ @for (column of columns; track column.name) { +
+ + +
+ } +
+
-
- @for (column of columns; track column.name) { -
- - -
- } + +
+

+ star_outline + Glyph Features (3-12 Required) + +

+

+ Select 3-12 features to visualize as glyph axes. Drag to reorder. +

+ + +
+ {{ selectedGlyphFeatures.length }} features selected (3-12 required) +
+
- -
-

- star_outline - Glyph Features (3-12 Required) - -

-

- Select 3-12 features to visualize as glyph axes. Drag to reorder. -

+ +
+ + +
+ + +
+

Selected Glyph Features ({{ selectedGlyphFeatures.length }})

- -
- {{ selectedGlyphFeatures.length }} features selected (3-12 required) -
-
+ @if (selectedGlyphFeatures.length === 0) { +
+ info +

No features selected. Click "Use Smart Suggestions" or drag features from below.

-
+ } @else { +
+ @for (feature of selectedGlyphFeatures; track feature; let i = $index) { +
+ {{ i + 1 }} + {{ feature }} + +
+ } +
+ } - -
- - -
+ @if (selectedGlyphFeatures.length < 3) { +
+ warning + At least {{ 3 - selectedGlyphFeatures.length }} more feature(s) required +
+ } + @if (selectedGlyphFeatures.length > 12) { +
+ warning + Too many features. Remove {{ selectedGlyphFeatures.length - 12 }} +
+ } +
- -
-

Selected Glyph Features ({{ selectedGlyphFeatures.length }})

+ +
+

Available Features (After Encoding)

+

Drag features to the selection area above or click to add.

- @if (selectedGlyphFeatures.length === 0) { -
- info -

No features selected. Click "Use Smart Suggestions" or drag features from below.

-
- } @else { -
- @for (feature of selectedGlyphFeatures; track feature; let i = $index) { -
- {{ i + 1 }} - {{ feature }} - -
+
+ @for (feature of availableFeatures; track feature) { +
+ {{ feature }} + @if (getFeatureVariance(feature) !== null) { + σ²={{ getFeatureVariance(feature)?.toFixed(3) }} + } + @if (isFeatureSelected(feature)) { + check_circle } -
- } - - @if (selectedGlyphFeatures.length < 3) { -
- warning - At least {{ 3 - selectedGlyphFeatures.length }} more feature(s) required -
- } - @if (selectedGlyphFeatures.length > 12) { -
- warning - Too many features. Remove {{ selectedGlyphFeatures.length - 12 }}
}
+
+
- -
-

Available Features (After Encoding)

-

Drag features to the selection area above or click to add.

- -
- @for (feature of availableFeatures; track feature) { -
- {{ feature }} - @if (getFeatureVariance(feature) !== null) { - σ²={{ getFeatureVariance(feature)?.toFixed(3) }} - } - @if (isFeatureSelected(feature)) { - check_circle - } -
- } + +
+

+ settings + Projection Methods + +

+

+ FastMap runs as the primary projection and loads immediately. + Enable additional methods below to compute in the background. +

+ + +
+
+ bolt +
+
+
+
+

FastMap

+ Primary +
+ check_circle
+

Fast distance-preserving projection - O(n) complexity, ideal for large datasets (40K+)

- -
-

- settings - Projection Methods - -

-

- FastMap runs as the primary projection and loads immediately. - Enable additional methods below to compute in the background. -

- - -
-
- bolt -
-
-
-
-

FastMap

- Primary +

Additional Projections (Background)

+ +
+ @for (method of projectionMethods; track method.key) { +
+
+ {{ method.icon }} +
+
+
+
+

{{ method.name }}

+ @if (method.badge) { + {{ method.badge }} + } + @if (shouldShowLargeDatasetWarning(method)) { + + warning + + } +
+
- check_circle +

{{ method.description }}

+ @if (shouldShowLargeDatasetWarning(method) && projectionConfig[method.key]) { +

May be slow or cause memory issues with {{ getDatasetRowCount() | number }} items

+ }
-

Fast distance-preserving projection - O(n) complexity, ideal for large datasets (40K+)

-
- -

Additional Projections (Background)

+ } +
-
- @for (method of projectionMethods; track method.key) { -
-
- {{ method.icon }} + + @if (projectionConfig.enableTSNE || projectionConfig.enableUMAP || projectionConfig.enableIsoMap || projectionConfig.enableLLE || projectionConfig.enableLTSA || projectionConfig.enableTriMap) { +
+

Method Parameters

+ + + @if (projectionConfig.enableIsoMap) { +
+
+ auto_graph +
IsoMap Parameters
-
-
-
-

{{ method.name }}

- @if (method.badge) { - {{ method.badge }} - } - @if (shouldShowLargeDatasetWarning(method)) { - - warning - - } +
+
+
+ +
+ + +
-
-

{{ method.description }}

- @if (shouldShowLargeDatasetWarning(method) && projectionConfig[method.key]) { -

May be slow or cause memory issues with {{ getDatasetRowCount() | number }} items

- }
} -
- - - @if (projectionConfig.enableTSNE || projectionConfig.enableUMAP || projectionConfig.enableIsoMap || projectionConfig.enableLLE || projectionConfig.enableLTSA || projectionConfig.enableTriMap) { -
-

Method Parameters

- - @if (projectionConfig.enableIsoMap) { -
-
- auto_graph -
IsoMap Parameters
-
-
-
-
- -
- - -
-
-
-
+ + @if (projectionConfig.enableLLE) { +
+
+ blur_on +
LLE Parameters
- } - - - @if (projectionConfig.enableLLE) { -
-
- blur_on -
LLE Parameters
-
-
-
-
- -
- - -
+
+
+
+ +
+ +
- } +
+ } - - @if (projectionConfig.enableLTSA) { -
-
- waves -
LTSA Parameters
-
-
-
-
- -
- - -
+ + @if (projectionConfig.enableLTSA) { +
+
+ waves +
LTSA Parameters
+
+
+
+
+ +
+ +
- } +
+ } - - @if (projectionConfig.enableTriMap) { -
-
- timeline -
TriMap Parameters
-
-
-
-
- -
- - -
+ + @if (projectionConfig.enableTriMap) { +
+
+ timeline +
TriMap Parameters
+
+
+
+
+ +
+ +
- } +
+ } - - @if (projectionConfig.enableTSNE) { -
-
- bubble_chart -
t-SNE Parameters
-
-
-
-
- -
- - -
+ + @if (projectionConfig.enableTSNE) { +
+
+ bubble_chart +
t-SNE Parameters
+
+
+
+
+ +
+ +
+
-
- -
- - -
+
+ +
+ +
+
- @if (shouldShowTSNEWarning()) { -
- warning -
-

{{ getTSNETimeEstimate() }}

-

t-SNE computation will run in the background. You can explore your data with FastMap while waiting.

-
+ @if (shouldShowTSNEWarning()) { +
+ warning +
+

{{ getTSNETimeEstimate() }}

+

t-SNE computation will run in the background. You can explore your data with FastMap while waiting.

- } -
+
+ }
- } +
+ } - - @if (projectionConfig.enableUMAP) { -
-
- scatter_plot -
UMAP Parameters
-
-
-
-
- -
- - -
+ + @if (projectionConfig.enableUMAP) { +
+
+ scatter_plot +
UMAP Parameters
+
+
+
+
+ +
+ +
+
-
- -
- - -
+
+ +
+ +
- } -
- } -
- - -
- - -
- @if (!canProceedToReview()) { -
- warning - Please select 3-12 glyph features and at least one projection method to continue.
} - -
-
+ }
- } - - - - - @if (activeSection === 'review') { -
- -
-
-
view_column
-
- Columns Selected - {{ enabledColumns }} / {{ totalColumns }} -
-
- -
-
tune
-
- Projection Features - {{ projectionColumns }} -
-
- -
-
palette
-
- Color Feature - {{ colorFeature || 'None' }} -
-
- -
-
star_outline
-
- Glyph Features - {{ selectedGlyphFeatures.length }} -
-
-
-
settings
-
- Projection Methods - {{ enabledMethods.join(', ') }} + +
+ + +
+ @if (!canProceed()) { +
+ warning + Please select 3-12 glyph features and at least one projection method to continue.
-
-
- - -
-

- list - Column Configuration Details -

- -
-
- Column - Type - Encoding - Scaling - In Projection -
- - @for (config of columnConfigs; track config.name) { - @if (config.enabled) { -
- - {{ config.name }} - @if (config.isColorFeature) { - - palette - - } - - - - {{ getDataTypeLabel(config.targetType) }} - - - {{ getEncodingLabel(config.encodingMethod) }} - - @if (config.targetType === DataType.Numeric || config.targetType === DataType.Date) { - {{ getScalingLabel(config.scalingMethod) }} - } @else { - N/A - } - - - @if (config.includeInProjection) { - - check_circle - Yes - - } @else { - - cancel - No - - } - -
- } - } -
-
- - -
- info -
- Processing Info: Processing time depends on your dataset size and enabled projection methods. - FastMap loads immediately, allowing you to explore your data right away. Additional projections compute in the background. -
-
+ } - -
- - -
- -
- } - - - - - @if (activeSection === 'processing') { - @if (isProcessing) { - -
-
-
- sync -
- -

Processing Your Data

-

{{ processingStep || 'Applying your preprocessing configuration...' }}

- -
-
-
- -
{{ processingProgress }}%
-
-
- } - - @if (processingComplete && !isProcessing) { - -
-
-
- check_circle -
- -

Processing Complete!

-

Your dataset has been successfully processed and is ready for visualization.

- -
-
- check - {{ enabledColumns }} columns processed -
-
- check - FastMap projection ready -
-
- check - Data cleaning applied -
-
- - - @if (getBackgroundProjectionsArray().length > 0) { -
-

- schedule - Background Projections -

-
- @for (proj of getBackgroundProjectionsArray(); track proj.method) { -
-
- {{ proj.method.toUpperCase() }} - @if (proj.status === 'running') { - - sync - Running - - } - @if (proj.status === 'complete') { - - check_circle - Complete - - } - @if (proj.status === 'error') { - - error - Error - - } -
- @if (proj.status === 'running') { -
-
-
-

{{ proj.message }}

- } - @if (proj.status === 'complete') { -

Available in Glyphspace dropdown

- } -
- } -
-
- } -
-
- - -
- - -
- -
-
- } - - @if (error) { - -
- error -
- Error: {{ error }} -
-
- } - } +
diff --git a/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.scss b/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.scss index 6759736..2bb95f5 100644 --- a/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.scss +++ b/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.scss @@ -9,64 +9,6 @@ @include w.step-header; } - // Section Tabs - .section-tabs { - display: flex; - gap: 8px; - margin-bottom: 16px; - border-bottom: 2px solid v.$gray-300; - - .tab-button { - display: flex; - align-items: center; - gap: 6px; - padding: 10px 16px; - background: white; - border: 1px solid v.$gray-300; - border-bottom: none; - border-radius: 4px 4px 0 0; - font-size: 13px; - font-weight: 500; - color: v.$gray-700; - cursor: pointer; - transition: all 0.2s ease; - position: relative; - top: 2px; - - .material-icons { - font-size: 18px; - } - - .warning-icon { - font-size: 16px; - color: #E65100; - } - - &:hover:not(:disabled) { - background: v.$gray-100; - border-color: v.$gray-400; - } - - &.active { - background: white; - border-color: v.$gray-300; - border-bottom-color: white; - color: v.$active-color; - font-weight: 600; - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } - - &.can-proceed:not(.active) { - border-color: #4CAF50; - color: #388E3C; - } - } - } - // Config Panels .config-panel { background: white; @@ -649,402 +591,6 @@ } } - // Review Section - .review-section { - .summary-cards { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 12px; - margin-bottom: 16px; - - .summary-card { - display: flex; - gap: 12px; - padding: 14px; - background: white; - border: 1px solid v.$gray-300; - border-radius: 4px; - - .card-icon { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - background: rgba(0, 188, 212, 0.1); - border-radius: 50%; - - .material-icons { - font-size: 22px; - color: v.$active-color; - } - } - - .card-content { - display: flex; - flex-direction: column; - gap: 2px; - - .card-label { - font-size: 11px; - font-weight: 600; - color: v.$gray-600; - text-transform: uppercase; - } - - .card-value { - font-size: 16px; - font-weight: 700; - color: v.$gray-800; - } - } - } - } - - .details-section { - background: white; - border: 1px solid v.$gray-300; - border-radius: 4px; - padding: 16px; - margin-bottom: 16px; - - h3 { - display: flex; - align-items: center; - gap: 8px; - font-size: 15px; - font-weight: 600; - color: v.$gray-800; - margin-bottom: 12px; - - .material-icons { - font-size: 20px; - color: v.$active-color; - } - } - - .columns-table { - display: flex; - flex-direction: column; - border: 1px solid v.$gray-300; - border-radius: 4px; - max-height: 40vh; // Limit table height to enable scrolling with sticky header - overflow-y: auto; - overflow-x: hidden; - scrollbar-gutter: stable; // Reserve space for scrollbar to prevent content overlap - - .table-header, - .table-row { - display: grid; - grid-template-columns: 2fr 1fr 1.2fr 1fr 1fr; - gap: 10px; - padding: 10px 12px; - font-size: 13px; - align-items: center; - } - - .table-header { - background: v.$wizard-table-header-bg; - border-bottom: 2px solid v.$gray-300; - font-weight: 600; - color: v.$gray-700; - position: sticky; - top: 0; - z-index: 10; - } - - .table-row { - border-bottom: 1px solid v.$gray-200; - - &:last-child { - border-bottom: none; - } - - &:hover { - background: v.$gray-100; - } - - .column-name-text { - font-weight: 500; - color: v.$gray-800; - } - - .color-badge { - display: inline-flex; - align-items: center; - margin-left: 6px; - - .material-icons { - font-size: 14px; - color: v.$active-color; - } - } - - .data-type-badge { - @include w.badge; - } - - .text-muted { - font-size: 12px; - color: v.$gray-500; - font-style: italic; - } - - .status-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 8px; - border-radius: 3px; - font-size: 11px; - font-weight: 600; - - .material-icons { - font-size: 14px; - } - - &.status-yes { - background: rgba(76, 175, 80, 0.1); - color: #388E3C; - } - - &.status-no { - background: rgba(158, 158, 158, 0.1); - color: #616161; - } - } - } - } - } - - .warning-box { - @include w.warning-box; - } - - .info-box { - @include w.info-box; - } - } - - // Processing Section - .processing-view, - .success-view { - display: flex; - align-items: center; - justify-content: center; - min-height: 400px; - - .processing-card, - .success-card { - display: flex; - flex-direction: column; - align-items: center; - gap: 16px; - padding: 40px; - background: white; - border: 1px solid v.$gray-300; - border-radius: 8px; - max-width: 600px; - text-align: center; - - .processing-icon, - .success-icon { - display: flex; - align-items: center; - justify-content: center; - width: 80px; - height: 80px; - background: rgba(0, 188, 212, 0.1); - border-radius: 50%; - - .material-icons { - font-size: 48px; - - &.spinning { - color: v.$active-color; - animation: spin 1s linear infinite; - } - } - } - - .success-icon { - background: rgba(76, 175, 80, 0.1); - - .material-icons { - color: #4CAF50; - } - } - - h3 { - font-size: 20px; - font-weight: 700; - color: v.$gray-800; - margin: 0; - } - - .processing-message, - .success-message { - font-size: 14px; - color: v.$gray-600; - } - - .progress-bar { - @include w.progress-bar; - width: 100%; - max-width: 400px; - height: 8px; - } - - .progress-fill { - @include w.progress-fill; - } - - .progress-text { - font-size: 16px; - font-weight: 700; - color: v.$active-color; - } - - .success-stats { - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; - - .stat-item { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - font-size: 14px; - color: v.$gray-700; - - .material-icons { - font-size: 18px; - color: #4CAF50; - } - } - } - - .background-status-section { - width: 100%; - padding-top: 16px; - border-top: 1px solid v.$gray-300; - - h4 { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - font-size: 14px; - font-weight: 600; - color: v.$gray-700; - margin-bottom: 12px; - - .material-icons { - font-size: 18px; - } - } - - .projection-status-list { - display: flex; - flex-direction: column; - gap: 10px; - - .projection-status-item { - padding: 10px; - background: v.$gray-100; - border-radius: 4px; - - .status-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 6px; - - .method-name { - font-size: 13px; - font-weight: 600; - color: v.$gray-800; - } - - .status-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 8px; - border-radius: 3px; - font-size: 11px; - font-weight: 600; - - .material-icons { - font-size: 14px; - - &.spinning { - animation: spin 1s linear infinite; - } - } - - &.status-running { - background: rgba(0, 188, 212, 0.1); - color: v.$active-color; - } - - &.status-complete { - background: rgba(76, 175, 80, 0.1); - color: #388E3C; - } - - &.status-error { - background: rgba(244, 67, 54, 0.1); - color: #C62828; - } - } - } - - .progress-bar-small { - height: 4px; - background: v.$gray-200; - border-radius: 2px; - overflow: hidden; - margin-bottom: 4px; - - .progress-fill-small { - height: 100%; - background: v.$active-color; - transition: width 0.3s ease; - } - } - - .status-message { - font-size: 12px; - color: v.$gray-600; - margin: 0; - } - } - } - } - - .success-actions { - display: flex; - gap: 12px; - margin-top: 16px; - - .btn-secondary { - @include w.btn-secondary; - } - - .btn-primary { - @include w.btn-primary; - } - } - } - } - - .error-message { - @include w.message-error; - } - .warning-box { @include w.warning-box; } @@ -1075,12 +621,3 @@ } } } - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.ts b/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.ts index 5ce98a1..9d22718 100644 --- a/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.ts +++ b/src/app/preprocessing-wizard/steps/step4-visualization-settings/step4-visualization-settings.component.ts @@ -1,14 +1,10 @@ -import { Component, OnInit, OnDestroy, ChangeDetectorRef, NgZone, Output, EventEmitter, ElementRef } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; -import { Subscription } from 'rxjs'; import { PreprocessingService } from '../../services/preprocessing.service'; -import { DataProviderService } from '../../../services/dataprovider.service'; -import { ProjectionService, ProjectionResult } from '../../../services/projection.service'; -import { ToastService } from '../../../services/toast.service'; -import { ColumnConfig, ProjectionConfig } from '../../models/column-config'; +import { ProjectionConfig } from '../../models/column-config'; import { ColumnStatistics } from '../../models/column-statistics'; -import { DataType, EncodingMethod, ScalingMethod } from '../../models/data-type.enum'; +import { DataType } from '../../models/data-type.enum'; import { HelpTooltipComponent } from '../../shared/help-tooltip/help-tooltip.component'; import { HELP_TEXT } from '../../shared/constants/help-text'; import { STEP_INFO } from '../../shared/constants/step-info'; @@ -25,7 +21,7 @@ interface ProjectionMethodUI { icon: string; badge?: string; disabled?: boolean; - largeDatasetWarning?: boolean; // Show warning for datasets > 10k items (O(n²) memory/time) + largeDatasetWarning?: boolean; } @Component({ @@ -36,11 +32,6 @@ interface ProjectionMethodUI { styleUrl: './step4-visualization-settings.component.scss' }) export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { - @Output() finish = new EventEmitter(); - - // Tab control - activeSection: 'configure' | 'review' | 'processing' = 'configure'; - // Color feature selection columns: ColumnStatistics[] = []; colorFeature: string | null = null; @@ -79,12 +70,8 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { }; readonly TSNE_WARNING_THRESHOLD = 5000; - readonly LARGE_DATASET_THRESHOLD = 10000; // Show warnings for O(n²) methods above this + readonly LARGE_DATASET_THRESHOLD = 10000; - // FastMap is always the primary projection (runs immediately, not toggleable) - // These are optional background projections the user can enable - // Speed labels: Very Fast > Fast > Medium > Slow (relative to FastMap which is primary) - // largeDatasetWarning: methods with O(n²) memory/time that may fail or be very slow with >10k items projectionMethods: ProjectionMethodUI[] = [ { key: 'enablePCA', @@ -99,7 +86,7 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { description: 'Non-linear manifold learning - Preserves geodesic distances', icon: 'auto_graph', badge: 'Fast', - largeDatasetWarning: true // O(n²) distance matrix + largeDatasetWarning: true }, { key: 'enableMDS', @@ -107,7 +94,7 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { description: 'Classical Multidimensional Scaling - Distance preserving', icon: 'grid_on', badge: 'Fast', - largeDatasetWarning: true // O(n²) distance matrix + largeDatasetWarning: true }, { key: 'enableLLE', @@ -115,7 +102,7 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { description: 'Locally Linear Embedding - Preserves local geometry', icon: 'blur_on', badge: 'Medium', - largeDatasetWarning: true // O(n²) neighborhood graph + largeDatasetWarning: true }, { key: 'enableLTSA', @@ -123,7 +110,7 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { description: 'Local Tangent Space Alignment - Good for curved manifolds', icon: 'waves', badge: 'Medium', - largeDatasetWarning: true // O(n²) neighborhood graph + largeDatasetWarning: true }, { key: 'enableTSNE', @@ -131,7 +118,7 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { description: 'Preserves local structure - Good for clusters', icon: 'bubble_chart', badge: 'Slow', - largeDatasetWarning: true // O(n²) pairwise similarities + largeDatasetWarning: true }, { key: 'enableUMAP', @@ -139,7 +126,7 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { description: 'Balances local and global structure', icon: 'scatter_plot', badge: 'Slow', - largeDatasetWarning: true // O(n²) neighborhood search + largeDatasetWarning: true }, { key: 'enableTriMap', @@ -147,7 +134,6 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { description: 'Global structure preservation - Good for large datasets', icon: 'timeline', badge: 'Medium' - // TriMap is designed for large datasets, no warning needed }, { key: 'enableTopoMap', @@ -155,7 +141,7 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { description: 'Topology preserving projection', icon: 'terrain', badge: 'Medium', - largeDatasetWarning: true // O(n²) distance computations + largeDatasetWarning: true }, { key: 'enableSammon', @@ -163,50 +149,15 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { description: 'Sammon mapping - Preserves small distances', icon: 'hub', badge: 'Medium', - largeDatasetWarning: true // O(n²) distance matrix + largeDatasetWarning: true } ]; - // Processing state - isProcessing = false; - processingProgress = 0; - processingStep = ''; - processingComplete = false; - error: string | null = null; - - // Review/Summary data - totalColumns = 0; - enabledColumns = 0; - projectionColumns = 0; - enabledMethods: string[] = []; - columnConfigs: ColumnConfig[] = []; - - // Background projection status - backgroundProjections = new Map(); - - // Capture dataset info for background projections (survives wizard reset) - private backgroundDatasetName: string = ''; - private backgroundTimestamp: string = ''; - - // Expose enums - DataType = DataType; - EncodingMethod = EncodingMethod; - ScalingMethod = ScalingMethod; - readonly HELP_TEXT = HELP_TEXT; readonly stepInfo = STEP_INFO[3]; // Step 4 (index 3) - private progressSubscription?: Subscription; - private backgroundStatusSubscription?: Subscription; - constructor( - public preprocessingService: PreprocessingService, - private dataProvider: DataProviderService, - private projectionService: ProjectionService, - private toastService: ToastService, - private cdr: ChangeDetectorRef, - private ngZone: NgZone, - private elementRef: ElementRef + public preprocessingService: PreprocessingService ) {} ngOnInit(): void { @@ -225,7 +176,6 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { if (colorCol) { this.colorFeature = colorCol.name; } else if (this.columns.length > 0) { - // Default to first column and persist to service this.colorFeature = this.columns[0].name; this.preprocessingService.setColorFeature(this.columns[0].name); } else { @@ -245,55 +195,9 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { if (state.projectionConfig) { this.projectionConfig = { ...state.projectionConfig }; } - - // Prepare review data - this.prepareReviewData(); - } - - ngOnDestroy(): void { - if (this.progressSubscription) { - this.progressSubscription.unsubscribe(); - } - if (this.backgroundStatusSubscription) { - this.backgroundStatusSubscription.unsubscribe(); - } - } - - // ============================================================================ - // Section Navigation - // ============================================================================ - - goToSection(section: 'configure' | 'review' | 'processing'): void { - if (section === 'review' && !this.canProceedToReview()) { - return; - } - this.activeSection = section; - if (section === 'review') { - this.prepareReviewData(); - } - // Scroll to top when changing sections - this.scrollToTop(); - } - - private scrollToTop(): void { - // Use setTimeout to ensure scroll happens after Angular renders the new content - setTimeout(() => { - // Scroll the parent wizard content container - const wizardContent = document.querySelector('.wizard-content'); - if (wizardContent) { - wizardContent.scrollTop = 0; - } - // Also scroll the component itself if it has overflow - this.elementRef.nativeElement.scrollTop = 0; - }, 0); } - canProceedToReview(): boolean { - const glyphValid = this.selectedGlyphFeatures.length >= this.MIN_GLYPH_FEATURES && - this.selectedGlyphFeatures.length <= this.MAX_GLYPH_FEATURES; - const projectionValid = this.hasEnabledMethod(); - return glyphValid && projectionValid; - } + ngOnDestroy(): void {} // ============================================================================ // Color Feature Selection @@ -497,26 +401,9 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { } hasEnabledMethod(): boolean { - // FastMap is always enabled as the primary projection return true; } - getEnabledMethodsCount(): number { - // Start with 1 for FastMap (always enabled as primary) - let count = 1; - if (this.projectionConfig.enablePCA) count++; - if (this.projectionConfig.enableIsoMap) count++; - if (this.projectionConfig.enableMDS) count++; - if (this.projectionConfig.enableLLE) count++; - if (this.projectionConfig.enableLTSA) count++; - if (this.projectionConfig.enableTSNE) count++; - if (this.projectionConfig.enableUMAP) count++; - if (this.projectionConfig.enableTriMap) count++; - if (this.projectionConfig.enableTopoMap) count++; - if (this.projectionConfig.enableSammon) count++; - return count; - } - getDatasetRowCount(): number { return this.preprocessingService.currentState.dataProfile?.totalRows || 0; } @@ -535,328 +422,23 @@ export class Step4VisualizationSettingsComponent implements OnInit, OnDestroy { } // ============================================================================ - // Review/Summary - // ============================================================================ - - prepareReviewData(): void { - const state = this.preprocessingService.currentState; - - this.totalColumns = state.dataProfile?.columns.length || 0; - this.columnConfigs = Array.from(state.columnConfigs.values()); - this.enabledColumns = this.columnConfigs.filter(c => c.enabled).length; - this.projectionColumns = this.columnConfigs.filter(c => c.enabled && c.includeInProjection).length; - - // FastMap is always the primary projection - this.enabledMethods = ['FastMap (Primary)']; - if (this.projectionConfig.enablePCA) this.enabledMethods.push('PCA'); - if (this.projectionConfig.enableIsoMap) this.enabledMethods.push('IsoMap'); - if (this.projectionConfig.enableMDS) this.enabledMethods.push('MDS'); - if (this.projectionConfig.enableLLE) this.enabledMethods.push('LLE'); - if (this.projectionConfig.enableLTSA) this.enabledMethods.push('LTSA'); - if (this.projectionConfig.enableTSNE) this.enabledMethods.push('t-SNE'); - if (this.projectionConfig.enableUMAP) this.enabledMethods.push('UMAP'); - if (this.projectionConfig.enableTriMap) this.enabledMethods.push('TriMap'); - if (this.projectionConfig.enableTopoMap) this.enabledMethods.push('TopoMap'); - if (this.projectionConfig.enableSammon) this.enabledMethods.push('Sammon'); - } - - getEncodingLabel(method: EncodingMethod): string { - const labels = { - [EncodingMethod.None]: 'None', - [EncodingMethod.OneHot]: 'One-Hot', - [EncodingMethod.Label]: 'Label', - [EncodingMethod.Normalize]: 'Normalize', - [EncodingMethod.Standardize]: 'Standardize' - }; - return labels[method] || 'Unknown'; - } - - getScalingLabel(method: ScalingMethod): string { - const labels = { - [ScalingMethod.None]: 'None', - [ScalingMethod.Standard]: 'Standard', - [ScalingMethod.MinMax]: 'Min-Max', - [ScalingMethod.Robust]: 'Robust' - }; - return labels[method] || 'Unknown'; - } - - getDataTypeLabel(type: DataType): string { - const labels: Record = { - [DataType.Numeric]: 'Numeric', - [DataType.Categorical]: 'Categorical', - [DataType.Text]: 'Text', - [DataType.Date]: 'Date', - [DataType.Boolean]: 'Boolean', - [DataType.ID]: 'ID', - [DataType.Coordinate]: 'Coordinate', - [DataType.Unknown]: 'Unknown' - }; - return labels[type] || 'Unknown'; - } - - // ============================================================================ - // Processing + // Navigation // ============================================================================ - async startProcessing(): Promise { - this.activeSection = 'processing'; - this.isProcessing = true; - this.processingProgress = 0; - this.processingStep = 'Initializing...'; - this.error = null; - - this.progressSubscription = this.preprocessingService.processingProgress.subscribe({ - next: (progress) => { - this.processingStep = progress.message || progress.step; - this.processingProgress = Math.min(progress.progress, 70); - this.cdr.detectChanges(); - } - }); - - try { - await this.preprocessingService.processData(); - - this.ngZone.run(() => { - this.processingStep = 'Loading features for projections...'; - this.processingProgress = 70; - this.cdr.detectChanges(); - }); - - const csvText = await this.preprocessingService.getProcessedFeaturesCSV(); - const { features, ids } = this.projectionService.parseCSVFeatures(csvText); - - this.ngZone.run(() => { - this.processingStep = 'Computing FastMap projection...'; - this.processingProgress = 75; - this.cdr.detectChanges(); - }); - - // Use FastMap as the primary projection (fast distance-preserving, O(n) complexity, ideal for large datasets) - const fastmapResult = await this.projectionService.runFastMapSync(features, ids); - - this.ngZone.run(() => { - this.processingStep = 'Loading dataset with FastMap...'; - this.processingProgress = 90; - this.cdr.detectChanges(); - }); - - await this.preprocessingService.addProjectionPositions('fastmap', fastmapResult.positions); - - this.ngZone.run(() => { - this.processingProgress = 100; - this.processingStep = `Dataset loaded with FastMap (${fastmapResult.computeTime}ms)`; - this.processingComplete = true; - this.isProcessing = false; - this.cdr.detectChanges(); - }); - - this.startBackgroundProjections(features, ids); - - } catch (error: any) { - console.error('Processing failed:', error); - this.ngZone.run(() => { - this.error = error.message || 'Processing failed'; - this.isProcessing = false; - this.cdr.detectChanges(); - }); - } finally { - if (this.progressSubscription) { - this.progressSubscription.unsubscribe(); - this.progressSubscription = undefined; - } - } - } - - private async startBackgroundProjections(features: number[][], ids: (string|number)[]): Promise { - const config = this.projectionConfig; - - // Capture dataset info so background projections can add positions even after wizard reset - const state = this.preprocessingService.currentState; - this.backgroundDatasetName = state.datasetName; - this.backgroundTimestamp = state.timestamp; - - this.backgroundStatusSubscription = this.projectionService.backgroundStatusObservable.subscribe(statusMap => { - this.ngZone.run(() => { - this.backgroundProjections.clear(); - statusMap.forEach((status, method) => { - this.backgroundProjections.set(method, { - status: status.status, - progress: status.progress, - message: status.message - }); - }); - this.cdr.detectChanges(); - }); - }); - - // Run background projections (FastMap is primary) - if (config.enablePCA) { - this.runBackgroundProjection('PCA', () => this.projectionService.runPCABackground(features, ids)); - } - - if (config.enableIsoMap) { - this.runBackgroundProjection('IsoMap', () => this.projectionService.runIsoMap(features, ids, { - neighbors: config.isomapNeighbors - })); - } - - if (config.enableMDS) { - this.runBackgroundProjection('MDS', () => this.projectionService.runMDS(features, ids)); - } - - if (config.enableLLE) { - this.runBackgroundProjection('LLE', () => this.projectionService.runLLE(features, ids, { - neighbors: config.lleNeighbors - })); - } - - if (config.enableLTSA) { - this.runBackgroundProjection('LTSA', () => this.projectionService.runLTSA(features, ids, { - neighbors: config.ltsaNeighbors - })); - } - - if (config.enableTSNE) { - this.runBackgroundProjection('t-SNE', () => - this.projectionService.runTSNE(features, ids, { - perplexity: config.tsnePerplexity, - iterations: config.tsneIterations - }) - ); - } - - if (config.enableUMAP) { - this.runBackgroundProjection('UMAP', () => - this.projectionService.runUMAP(features, ids, { - neighbors: config.umapNeighbors, - minDist: config.umapMinDist - }) - ); - } - - if (config.enableTriMap) { - this.runBackgroundProjection('TriMap', () => this.projectionService.runTriMap(features, ids, { - weightAdj: config.trimapWeightAdj - })); - } - - if (config.enableTopoMap) { - this.runBackgroundProjection('TopoMap', () => this.projectionService.runTopoMap(features, ids)); - } - - if (config.enableSammon) { - this.runBackgroundProjection('Sammon', () => this.projectionService.runSammon(features, ids)); - } - } - - private async runBackgroundProjection(name: string, computeFn: () => Promise): Promise { - try { - const result = await computeFn(); - - // Convert positions to the format expected by DataProvider - const positionsForProvider = result.positions.map(p => ({ - id: p.id, - position: { x: p.x, y: p.y } - })); - - // Try to add to wizard state first (if still active) - await this.preprocessingService.addProjectionPositions(result.method, result.positions); - - const state = this.preprocessingService.currentState; - if (state.processedDataset) { - // Wizard still active - update via normal flow - const collection = state.processedDataset as any; - const datasetKey = collection.selectedDataset || (collection.datasets ? Object.keys(collection.datasets)[0] : null); - - if (datasetKey && collection.datasets) { - const dataset = collection.datasets[datasetKey]; - if (dataset) { - this.dataProvider.addProcessedDatasetToCollection(state.datasetName, state.timestamp, dataset); - this.dataProvider.loadProcessedDataset(dataset, state.datasetName, state.timestamp); - } - } - } else if (this.backgroundDatasetName && this.backgroundTimestamp) { - // Wizard was reset but dataset is already loaded in dashboard - // Add positions directly to the loaded dataset - this.dataProvider.addPositionsToLoadedDataset( - this.backgroundDatasetName, - this.backgroundTimestamp, - result.method, - positionsForProvider - ); - } - - this.ngZone.run(() => { - this.toastService.success(`${name} projection ready! (${(result.computeTime / 1000).toFixed(1)}s)`, 4000); - }); - - } catch (error: any) { - console.error(`${name} projection failed:`, error); - this.ngZone.run(() => { - this.toastService.error(`${name} projection failed: ${error.message}`, 6000); - }); - } + canProceed(): boolean { + const glyphValid = this.selectedGlyphFeatures.length >= this.MIN_GLYPH_FEATURES && + this.selectedGlyphFeatures.length <= this.MAX_GLYPH_FEATURES; + const projectionValid = this.hasEnabledMethod(); + return glyphValid && projectionValid; } - goToDashboard(): void { - const state = this.preprocessingService.currentState; - - if (state.processedDataset) { - const collection = state.processedDataset as any; - const datasetKey = collection.selectedDataset || (collection.datasets ? Object.keys(collection.datasets)[0] : null); - - if (!datasetKey || !collection.datasets) { - this.error = 'Invalid dataset structure. Please try processing again.'; - return; - } - - const dataset = collection.datasets[datasetKey]; - - if (dataset) { - this.dataProvider.addProcessedDatasetToCollection(state.datasetName, state.timestamp, dataset); - this.dataProvider.loadProcessedDataset(dataset, state.datasetName, state.timestamp); - } else { - this.error = 'Failed to load processed dataset'; - return; - } + continue(): void { + if (this.canProceed()) { + this.preprocessingService.nextStep(); } - - // Reset wizard state so it's ready for a new upload - this.preprocessingService.resetState(); - - this.finish.emit(); } goBack(): void { this.preprocessingService.previousStep(); } - - startOver(): void { - if (confirm('Are you sure you want to start over? All current configuration will be lost.')) { - // Terminate any running background projection workers - this.projectionService.terminateAllWorkers(); - this.projectionService.clearBackgroundStatuses(); - - // Clear local state - this.backgroundProjections.clear(); - this.processingComplete = false; - this.isProcessing = false; - this.processingProgress = 0; - this.processingStep = ''; - this.error = null; - - // Reset wizard state - this.preprocessingService.resetState(); - this.preprocessingService.goToStep(0); - } - } - - getBackgroundProjectionsArray(): Array<{ method: string; status: string; progress: number; message: string }> { - const result: Array<{ method: string; status: string; progress: number; message: string }> = []; - this.backgroundProjections.forEach((value, key) => { - result.push({ method: key, ...value }); - }); - return result; - } } diff --git a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.html b/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.html new file mode 100644 index 0000000..b0acf00 --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.html @@ -0,0 +1,264 @@ +
+
+

{{ stepInfo.title }}

+

{{ stepInfo.purpose }}

+
+ + + + + @if (!showProcessing) { +
+ +
+
+
view_column
+
+ Columns Selected + {{ enabledColumns }} / {{ totalColumns }} +
+
+ +
+
tune
+
+ Projection Features + {{ projectionColumns }} +
+
+ +
+
palette
+
+ Color Feature + {{ colorFeature || 'None' }} +
+
+ +
+
star_outline
+
+ Glyph Features + {{ selectedGlyphFeatures.length }} +
+
+ +
+
settings
+
+ Projection Methods + {{ enabledMethods.join(', ') }} +
+
+
+ + +
+

+ list + Column Configuration Details +

+ +
+
+ Column + Type + Encoding + Scaling + In Projection +
+ + @for (config of columnConfigs; track config.name) { + @if (config.enabled) { +
+ + {{ config.name }} + @if (config.isColorFeature) { + + palette + + } + + + + {{ getDataTypeLabel(config.targetType) }} + + + {{ getEncodingLabel(config.encodingMethod) }} + + @if (config.targetType === DataType.Numeric || config.targetType === DataType.Date) { + {{ getScalingLabel(config.scalingMethod) }} + } @else { + N/A + } + + + @if (config.includeInProjection) { + + check_circle + Yes + + } @else { + + cancel + No + + } + +
+ } + } +
+
+ + +
+ info +
+ Processing Info: Processing time depends on your dataset size and enabled projection methods. + FastMap loads immediately, allowing you to explore your data right away. Additional projections compute in the background. +
+
+ + +
+ + +
+ +
+
+
+ } + + + + + @if (showProcessing) { + @if (isProcessing) { + +
+
+
+ sync +
+ +

Processing Your Data

+

{{ processingStep || 'Applying your preprocessing configuration...' }}

+ +
+
+
+ +
{{ processingProgress }}%
+
+
+ } + + @if (processingComplete && !isProcessing) { + +
+
+
+ check_circle +
+ +

Processing Complete!

+

Your dataset has been successfully processed and is ready for visualization.

+ +
+
+ check + {{ enabledColumns }} columns processed +
+
+ check + FastMap projection ready +
+
+ check + Data cleaning applied +
+
+ + + @if (getBackgroundProjectionsArray().length > 0) { +
+

+ schedule + Background Projections +

+
+ @for (proj of getBackgroundProjectionsArray(); track proj.method) { +
+
+ {{ proj.method.toUpperCase() }} + @if (proj.status === 'running') { + + sync + Running + + } + @if (proj.status === 'complete') { + + check_circle + Complete + + } + @if (proj.status === 'error') { + + error + Error + + } +
+ @if (proj.status === 'running') { +
+
+
+

{{ proj.message }}

+ } + @if (proj.status === 'complete') { +

Available in Glyphspace dropdown

+ } +
+ } +
+
+ } +
+
+ + +
+ + +
+ +
+
+ } + + @if (error) { + +
+ error +
+ Error: {{ error }} +
+
+ } + } +
diff --git a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.scss b/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.scss new file mode 100644 index 0000000..b280b4d --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.scss @@ -0,0 +1,418 @@ +@use 'sass:color'; +@use '../../shared/wizard-shared' as w; +@use '../../../../styles/variables' as v; + +.step5-review-processing { + @include w.step-container; + + .step-header { + @include w.step-header; + } + + // Review Section + .review-section { + .summary-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; + margin-bottom: 16px; + + .summary-card { + display: flex; + gap: 12px; + padding: 14px; + background: white; + border: 1px solid v.$gray-300; + border-radius: 4px; + + .card-icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: rgba(0, 188, 212, 0.1); + border-radius: 50%; + + .material-icons { + font-size: 22px; + color: v.$active-color; + } + } + + .card-content { + display: flex; + flex-direction: column; + gap: 2px; + + .card-label { + font-size: 11px; + font-weight: 600; + color: v.$gray-600; + text-transform: uppercase; + } + + .card-value { + font-size: 16px; + font-weight: 700; + color: v.$gray-800; + } + } + } + } + + .details-section { + background: white; + border: 1px solid v.$gray-300; + border-radius: 4px; + padding: 16px; + margin-bottom: 16px; + + h3 { + display: flex; + align-items: center; + gap: 8px; + font-size: 15px; + font-weight: 600; + color: v.$gray-800; + margin-bottom: 12px; + + .material-icons { + font-size: 20px; + color: v.$active-color; + } + } + + .columns-table { + display: flex; + flex-direction: column; + border: 1px solid v.$gray-300; + border-radius: 4px; + max-height: 40vh; + overflow-y: auto; + overflow-x: hidden; + scrollbar-gutter: stable; + + .table-header, + .table-row { + display: grid; + grid-template-columns: 2fr 1fr 1.2fr 1fr 1fr; + gap: 10px; + padding: 10px 12px; + font-size: 13px; + align-items: center; + } + + .table-header { + background: v.$wizard-table-header-bg; + border-bottom: 2px solid v.$gray-300; + font-weight: 600; + color: v.$gray-700; + position: sticky; + top: 0; + z-index: 10; + } + + .table-row { + border-bottom: 1px solid v.$gray-200; + + &:last-child { + border-bottom: none; + } + + &:hover { + background: v.$gray-100; + } + + .column-name-text { + font-weight: 500; + color: v.$gray-800; + } + + .color-badge { + display: inline-flex; + align-items: center; + margin-left: 6px; + + .material-icons { + font-size: 14px; + color: v.$active-color; + } + } + + .data-type-badge { + @include w.badge; + } + + .text-muted { + font-size: 12px; + color: v.$gray-500; + font-style: italic; + } + + .status-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + + .material-icons { + font-size: 14px; + } + + &.status-yes { + background: rgba(76, 175, 80, 0.1); + color: #388E3C; + } + + &.status-no { + background: rgba(158, 158, 158, 0.1); + color: #616161; + } + } + } + } + } + + .info-box { + @include w.info-box; + } + } + + // Processing Section + .processing-view, + .success-view { + display: flex; + align-items: center; + justify-content: center; + min-height: 400px; + + .processing-card, + .success-card { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + padding: 40px; + background: white; + border: 1px solid v.$gray-300; + border-radius: 8px; + max-width: 600px; + text-align: center; + + .processing-icon, + .success-icon { + display: flex; + align-items: center; + justify-content: center; + width: 80px; + height: 80px; + background: rgba(0, 188, 212, 0.1); + border-radius: 50%; + + .material-icons { + font-size: 48px; + + &.spinning { + color: v.$active-color; + animation: spin 1s linear infinite; + } + } + } + + .success-icon { + background: rgba(76, 175, 80, 0.1); + + .material-icons { + color: #4CAF50; + } + } + + h3 { + font-size: 20px; + font-weight: 700; + color: v.$gray-800; + margin: 0; + } + + .processing-message, + .success-message { + font-size: 14px; + color: v.$gray-600; + } + + .progress-bar { + @include w.progress-bar; + width: 100%; + max-width: 400px; + height: 8px; + } + + .progress-fill { + @include w.progress-fill; + } + + .progress-text { + font-size: 16px; + font-weight: 700; + color: v.$active-color; + } + + .success-stats { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + + .stat-item { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 14px; + color: v.$gray-700; + + .material-icons { + font-size: 18px; + color: #4CAF50; + } + } + } + + .background-status-section { + width: 100%; + padding-top: 16px; + border-top: 1px solid v.$gray-300; + + h4 { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + font-size: 14px; + font-weight: 600; + color: v.$gray-700; + margin-bottom: 12px; + + .material-icons { + font-size: 18px; + } + } + + .projection-status-list { + display: flex; + flex-direction: column; + gap: 10px; + + .projection-status-item { + padding: 10px; + background: v.$gray-100; + border-radius: 4px; + + .status-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; + + .method-name { + font-size: 13px; + font-weight: 600; + color: v.$gray-800; + } + + .status-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 3px; + font-size: 11px; + font-weight: 600; + + .material-icons { + font-size: 14px; + + &.spinning { + animation: spin 1s linear infinite; + } + } + + &.status-running { + background: rgba(0, 188, 212, 0.1); + color: v.$active-color; + } + + &.status-complete { + background: rgba(76, 175, 80, 0.1); + color: #388E3C; + } + + &.status-error { + background: rgba(244, 67, 54, 0.1); + color: #C62828; + } + } + } + + .progress-bar-small { + height: 4px; + background: v.$gray-200; + border-radius: 2px; + overflow: hidden; + margin-bottom: 4px; + + .progress-fill-small { + height: 100%; + background: v.$active-color; + transition: width 0.3s ease; + } + } + + .status-message { + font-size: 12px; + color: v.$gray-600; + margin: 0; + } + } + } + } + } + } + + .error-message { + @include w.message-error; + } + + // Action Buttons + .action-buttons { + @include w.action-buttons-footer; + + .btn-secondary { + @include w.btn-secondary; + } + + .buttons-right { + display: flex; + align-items: center; + gap: 12px; + margin-left: auto; + } + + .btn-primary { + @include w.btn-primary; + } + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.ts b/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.ts new file mode 100644 index 0000000..01b08b8 --- /dev/null +++ b/src/app/preprocessing-wizard/steps/step5-review-processing/step5-review-processing.component.ts @@ -0,0 +1,423 @@ +import { Component, OnInit, OnDestroy, ChangeDetectorRef, NgZone, Output, EventEmitter, ElementRef } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Subscription } from 'rxjs'; +import { PreprocessingService } from '../../services/preprocessing.service'; +import { DataProviderService } from '../../../services/dataprovider.service'; +import { ProjectionService, ProjectionResult } from '../../../services/projection.service'; +import { ToastService } from '../../../services/toast.service'; +import { ColumnConfig, ProjectionConfig } from '../../models/column-config'; +import { DataType, EncodingMethod, ScalingMethod } from '../../models/data-type.enum'; +import { STEP_INFO } from '../../shared/constants/step-info'; + +@Component({ + selector: 'app-step5-review-processing', + standalone: true, + imports: [CommonModule], + templateUrl: './step5-review-processing.component.html', + styleUrl: './step5-review-processing.component.scss' +}) +export class Step5ReviewProcessingComponent implements OnInit, OnDestroy { + @Output() finish = new EventEmitter(); + + // Review/Summary data + totalColumns = 0; + enabledColumns = 0; + projectionColumns = 0; + enabledMethods: string[] = []; + columnConfigs: ColumnConfig[] = []; + colorFeature: string | null = null; + selectedGlyphFeatures: string[] = []; + projectionConfig!: ProjectionConfig; + + // Processing state + isProcessing = false; + processingProgress = 0; + processingStep = ''; + processingComplete = false; + error: string | null = null; + showProcessing = false; + + // Background projection status + backgroundProjections = new Map(); + + // Capture dataset info for background projections (survives wizard reset) + private backgroundDatasetName: string = ''; + private backgroundTimestamp: string = ''; + + // Expose enums + DataType = DataType; + + readonly stepInfo = STEP_INFO[4]; // Step 5 (index 4) + + private progressSubscription?: Subscription; + private backgroundStatusSubscription?: Subscription; + + constructor( + public preprocessingService: PreprocessingService, + private dataProvider: DataProviderService, + private projectionService: ProjectionService, + private toastService: ToastService, + private cdr: ChangeDetectorRef, + private ngZone: NgZone, + private elementRef: ElementRef + ) {} + + ngOnInit(): void { + const state = this.preprocessingService.currentState; + + // Load color feature + const colorCol = Array.from(state.columnConfigs.values()).find(c => c.isColorFeature); + this.colorFeature = colorCol?.name ?? null; + + // Load glyph features + this.selectedGlyphFeatures = [...state.glyphFeatures]; + + // Load projection config + this.projectionConfig = { ...state.projectionConfig }; + + // Prepare review data + this.prepareReviewData(); + } + + ngOnDestroy(): void { + if (this.progressSubscription) { + this.progressSubscription.unsubscribe(); + } + if (this.backgroundStatusSubscription) { + this.backgroundStatusSubscription.unsubscribe(); + } + } + + // ============================================================================ + // Review/Summary + // ============================================================================ + + prepareReviewData(): void { + const state = this.preprocessingService.currentState; + + this.totalColumns = state.dataProfile?.columns.length || 0; + this.columnConfigs = Array.from(state.columnConfigs.values()); + this.enabledColumns = this.columnConfigs.filter(c => c.enabled).length; + this.projectionColumns = this.columnConfigs.filter(c => c.enabled && c.includeInProjection).length; + + // FastMap is always the primary projection + this.enabledMethods = ['FastMap (Primary)']; + if (this.projectionConfig.enablePCA) this.enabledMethods.push('PCA'); + if (this.projectionConfig.enableIsoMap) this.enabledMethods.push('IsoMap'); + if (this.projectionConfig.enableMDS) this.enabledMethods.push('MDS'); + if (this.projectionConfig.enableLLE) this.enabledMethods.push('LLE'); + if (this.projectionConfig.enableLTSA) this.enabledMethods.push('LTSA'); + if (this.projectionConfig.enableTSNE) this.enabledMethods.push('t-SNE'); + if (this.projectionConfig.enableUMAP) this.enabledMethods.push('UMAP'); + if (this.projectionConfig.enableTriMap) this.enabledMethods.push('TriMap'); + if (this.projectionConfig.enableTopoMap) this.enabledMethods.push('TopoMap'); + if (this.projectionConfig.enableSammon) this.enabledMethods.push('Sammon'); + } + + getEncodingLabel(method: EncodingMethod): string { + const labels = { + [EncodingMethod.None]: 'None', + [EncodingMethod.OneHot]: 'One-Hot', + [EncodingMethod.Label]: 'Label', + [EncodingMethod.Normalize]: 'Normalize', + [EncodingMethod.Standardize]: 'Standardize' + }; + return labels[method] || 'Unknown'; + } + + getScalingLabel(method: ScalingMethod): string { + const labels = { + [ScalingMethod.None]: 'None', + [ScalingMethod.Standard]: 'Standard', + [ScalingMethod.MinMax]: 'Min-Max', + [ScalingMethod.Robust]: 'Robust' + }; + return labels[method] || 'Unknown'; + } + + getDataTypeLabel(type: DataType): string { + const labels: Record = { + [DataType.Numeric]: 'Numeric', + [DataType.Categorical]: 'Categorical', + [DataType.Text]: 'Text', + [DataType.Date]: 'Date', + [DataType.Boolean]: 'Boolean', + [DataType.ID]: 'ID', + [DataType.Coordinate]: 'Coordinate', + [DataType.Unknown]: 'Unknown' + }; + return labels[type] || 'Unknown'; + } + + getDataTypeBadgeClass(dataType: DataType | undefined): string { + return `badge-${dataType}`; + } + + // ============================================================================ + // Processing + // ============================================================================ + + async startProcessing(): Promise { + this.showProcessing = true; + this.isProcessing = true; + this.processingProgress = 0; + this.processingStep = 'Initializing...'; + this.error = null; + + this.progressSubscription = this.preprocessingService.processingProgress.subscribe({ + next: (progress) => { + this.processingStep = progress.message || progress.step; + this.processingProgress = Math.min(progress.progress, 70); + this.cdr.detectChanges(); + } + }); + + try { + await this.preprocessingService.processData(); + + this.ngZone.run(() => { + this.processingStep = 'Loading features for projections...'; + this.processingProgress = 70; + this.cdr.detectChanges(); + }); + + const csvText = await this.preprocessingService.getProcessedFeaturesCSV(); + const { features, ids } = this.projectionService.parseCSVFeatures(csvText); + + this.ngZone.run(() => { + this.processingStep = 'Computing FastMap projection...'; + this.processingProgress = 75; + this.cdr.detectChanges(); + }); + + // Use FastMap as the primary projection + const fastmapResult = await this.projectionService.runFastMapSync(features, ids); + + this.ngZone.run(() => { + this.processingStep = 'Loading dataset with FastMap...'; + this.processingProgress = 90; + this.cdr.detectChanges(); + }); + + await this.preprocessingService.addProjectionPositions('fastmap', fastmapResult.positions); + + this.ngZone.run(() => { + this.processingProgress = 100; + this.processingStep = `Dataset loaded with FastMap (${fastmapResult.computeTime}ms)`; + this.processingComplete = true; + this.isProcessing = false; + this.cdr.detectChanges(); + }); + + this.startBackgroundProjections(features, ids); + + } catch (error: any) { + console.error('Processing failed:', error); + this.ngZone.run(() => { + this.error = error.message || 'Processing failed'; + this.isProcessing = false; + this.cdr.detectChanges(); + }); + } finally { + if (this.progressSubscription) { + this.progressSubscription.unsubscribe(); + this.progressSubscription = undefined; + } + } + } + + private async startBackgroundProjections(features: number[][], ids: (string|number)[]): Promise { + const config = this.projectionConfig; + + // Capture dataset info so background projections can add positions even after wizard reset + const state = this.preprocessingService.currentState; + this.backgroundDatasetName = state.datasetName; + this.backgroundTimestamp = state.timestamp; + + this.backgroundStatusSubscription = this.projectionService.backgroundStatusObservable.subscribe(statusMap => { + this.ngZone.run(() => { + this.backgroundProjections.clear(); + statusMap.forEach((status, method) => { + this.backgroundProjections.set(method, { + status: status.status, + progress: status.progress, + message: status.message + }); + }); + this.cdr.detectChanges(); + }); + }); + + if (config.enablePCA) { + this.runBackgroundProjection('PCA', () => this.projectionService.runPCABackground(features, ids)); + } + + if (config.enableIsoMap) { + this.runBackgroundProjection('IsoMap', () => this.projectionService.runIsoMap(features, ids, { + neighbors: config.isomapNeighbors + })); + } + + if (config.enableMDS) { + this.runBackgroundProjection('MDS', () => this.projectionService.runMDS(features, ids)); + } + + if (config.enableLLE) { + this.runBackgroundProjection('LLE', () => this.projectionService.runLLE(features, ids, { + neighbors: config.lleNeighbors + })); + } + + if (config.enableLTSA) { + this.runBackgroundProjection('LTSA', () => this.projectionService.runLTSA(features, ids, { + neighbors: config.ltsaNeighbors + })); + } + + if (config.enableTSNE) { + this.runBackgroundProjection('t-SNE', () => + this.projectionService.runTSNE(features, ids, { + perplexity: config.tsnePerplexity, + iterations: config.tsneIterations + }) + ); + } + + if (config.enableUMAP) { + this.runBackgroundProjection('UMAP', () => + this.projectionService.runUMAP(features, ids, { + neighbors: config.umapNeighbors, + minDist: config.umapMinDist + }) + ); + } + + if (config.enableTriMap) { + this.runBackgroundProjection('TriMap', () => this.projectionService.runTriMap(features, ids, { + weightAdj: config.trimapWeightAdj + })); + } + + if (config.enableTopoMap) { + this.runBackgroundProjection('TopoMap', () => this.projectionService.runTopoMap(features, ids)); + } + + if (config.enableSammon) { + this.runBackgroundProjection('Sammon', () => this.projectionService.runSammon(features, ids)); + } + } + + private async runBackgroundProjection(name: string, computeFn: () => Promise): Promise { + try { + const result = await computeFn(); + + // Convert positions to the format expected by DataProvider + const positionsForProvider = result.positions.map(p => ({ + id: p.id, + position: { x: p.x, y: p.y } + })); + + // Try to add to wizard state first (if still active) + await this.preprocessingService.addProjectionPositions(result.method, result.positions); + + const state = this.preprocessingService.currentState; + if (state.processedDataset) { + // Wizard still active - update via normal flow + const collection = state.processedDataset as any; + const datasetKey = collection.selectedDataset || (collection.datasets ? Object.keys(collection.datasets)[0] : null); + + if (datasetKey && collection.datasets) { + const dataset = collection.datasets[datasetKey]; + if (dataset) { + this.dataProvider.addProcessedDatasetToCollection(state.datasetName, state.timestamp, dataset); + this.dataProvider.loadProcessedDataset(dataset, state.datasetName, state.timestamp); + } + } + } else if (this.backgroundDatasetName && this.backgroundTimestamp) { + // Wizard was reset but dataset is already loaded in dashboard + this.dataProvider.addPositionsToLoadedDataset( + this.backgroundDatasetName, + this.backgroundTimestamp, + result.method, + positionsForProvider + ); + // Re-save to IndexedDB with the new projection included + this.dataProvider.saveDatasetToStorage(this.backgroundDatasetName, this.backgroundTimestamp); + } + + this.ngZone.run(() => { + this.toastService.success(`${name} projection ready! (${(result.computeTime / 1000).toFixed(1)}s)`, 4000); + }); + + } catch (error: any) { + console.error(`${name} projection failed:`, error); + this.ngZone.run(() => { + this.toastService.error(`${name} projection failed: ${error.message}`, 6000); + }); + } + } + + goToDashboard(): void { + const state = this.preprocessingService.currentState; + + if (state.processedDataset) { + const collection = state.processedDataset as any; + const datasetKey = collection.selectedDataset || (collection.datasets ? Object.keys(collection.datasets)[0] : null); + + if (!datasetKey || !collection.datasets) { + this.error = 'Invalid dataset structure. Please try processing again.'; + return; + } + + const dataset = collection.datasets[datasetKey]; + + if (dataset) { + this.dataProvider.addProcessedDatasetToCollection(state.datasetName, state.timestamp, dataset); + this.dataProvider.loadProcessedDataset(dataset, state.datasetName, state.timestamp); + // Persist to IndexedDB for cross-session survival + this.dataProvider.saveDatasetToStorage(state.datasetName, state.timestamp); + } else { + this.error = 'Failed to load processed dataset'; + return; + } + } + + // Reset wizard state so it's ready for a new upload + this.preprocessingService.resetState(); + + this.finish.emit(); + } + + goBack(): void { + this.preprocessingService.previousStep(); + } + + startOver(): void { + if (confirm('Are you sure you want to start over? All current configuration will be lost.')) { + // Terminate any running background projection workers + this.projectionService.terminateAllWorkers(); + this.projectionService.clearBackgroundStatuses(); + + // Clear local state + this.backgroundProjections.clear(); + this.processingComplete = false; + this.isProcessing = false; + this.processingProgress = 0; + this.processingStep = ''; + this.error = null; + this.showProcessing = false; + + // Reset wizard state + this.preprocessingService.resetState(); + this.preprocessingService.goToStep(0); + } + } + + getBackgroundProjectionsArray(): Array<{ method: string; status: string; progress: number; message: string }> { + const result: Array<{ method: string; status: string; progress: number; message: string }> = []; + this.backgroundProjections.forEach((value, key) => { + result.push({ method: key, ...value }); + }); + return result; + } +} diff --git a/src/app/services/dataprovider.service.ts b/src/app/services/dataprovider.service.ts index 65cdce3..d667b5a 100644 --- a/src/app/services/dataprovider.service.ts +++ b/src/app/services/dataprovider.service.ts @@ -13,6 +13,7 @@ import { GlyphFeature } from "../shared/interfaces/glyph-feature"; import { GlyphPosition } from "../shared/interfaces/glyph-position"; import { HttpClient } from "@angular/common/http"; import { DEFAULT_DATASETCOLLECTION } from "../../default-dataset"; +import { DatasetStorageService, StoredDataset } from "./dataset-storage.service"; @Injectable({ providedIn: 'root', @@ -29,9 +30,10 @@ export class DataProviderService { totalItems = 0; filteredItems = 0; - constructor(private http: HttpClient, private config: ConfigService, private dataProcessor: DataProcessorService) { + constructor(private http: HttpClient, private config: ConfigService, private dataProcessor: DataProcessorService, private datasetStorage: DatasetStorageService) { // TODO: Defer loading like WASM data sets this.loadDatasets(DEFAULT_DATASETCOLLECTION); + this.loadSavedDatasets(); } private loadDatasets(datasets: DatasetCollection) { @@ -92,6 +94,128 @@ export class DataProviderService { }); } + private async loadSavedDatasets(): Promise { + try { + const savedDatasets = await this.datasetStorage.getAllDatasets(); + + for (const saved of savedDatasets) { + const positionsMap = new Map(); + for (const [algo, posArr] of Object.entries(saved.positions)) { + positionsMap.set(algo, posArr); + } + + this.buildDataSet(saved.name, saved.timestamp, saved.schema, saved.meta, saved.features, positionsMap); + + const positionMapping: { [key: string]: string } = {}; + for (const algo of Object.keys(saved.positions)) { + positionMapping[algo] = `memory://${saved.name}/${saved.timestamp}/${algo}`; + } + + const entry: DatasetCollectionEntry = { + dataset: saved.name, + source: 'indexeddb', + items: [{ + time: saved.timestamp, + algorithms: { + schema: `memory://${saved.name}/${saved.timestamp}/schema`, + meta: `memory://${saved.name}/${saved.timestamp}/meta`, + feature: `memory://${saved.name}/${saved.timestamp}/features`, + position: positionMapping + } + }] + }; + + this.setDatasetCollection([entry]); + } + } catch (error) { + console.warn('[DataProvider] Failed to load saved datasets from IndexedDB:', error); + } + } + + public async saveDatasetToStorage(datasetName: string, timestamp: string): Promise { + try { + const schema = this.schemaCache.get(datasetName)?.get(timestamp); + const meta = this.metaCache.get(datasetName)?.get(timestamp); + if (!schema || !meta) { + console.warn('[DataProvider] Cannot save to IndexedDB - missing schema or meta for:', datasetName); + return; + } + + const glyphMap = this.glyphCache.get(datasetName); + if (!glyphMap) return; + + const features: GlyphFeature[] = []; + const positions: { [algo: string]: GlyphPosition[] } = {}; + + glyphMap.forEach((glyph) => { + features.push({ + id: glyph.id, + defaultcontext: String(glyph.defaultcontext), + features: glyph.features, + values: glyph.values ?? {} + }); + + if (glyph.positions[timestamp]) { + for (const [algo, pos] of Object.entries(glyph.positions[timestamp])) { + if (!positions[algo]) positions[algo] = []; + positions[algo].push({ + id: glyph.id, + position: pos as { x: number; y: number } + }); + } + } + }); + + const stored: StoredDataset = { + name: datasetName, + timestamp, + savedAt: Date.now(), + schema, + meta, + features, + positions + }; + + await this.datasetStorage.saveDataset(stored); + + // Update source to 'indexeddb' in collection + const collection = this.dataSetCollectionSubject.getValue(); + const entry = collection.find(c => c.dataset === datasetName); + if (entry) { + entry.source = 'indexeddb'; + this.dataSetCollectionSubject.next([...collection]); + } + + console.log(`[DataProvider] Dataset "${datasetName}" saved to IndexedDB`); + } catch (error) { + console.warn('[DataProvider] Failed to save dataset to IndexedDB:', error); + } + } + + public async deleteDataset(datasetName: string): Promise { + const collection = this.dataSetCollectionSubject.getValue(); + const entry = collection.find(c => c.dataset === datasetName); + + if (!entry || entry.source === 'local') { + return false; + } + + await this.datasetStorage.deleteDataset(datasetName); + + this.glyphCache.delete(datasetName); + this.schemaCache.delete(datasetName); + this.metaCache.delete(datasetName); + + const updated = collection.filter(c => c.dataset !== datasetName); + this.dataSetCollectionSubject.next(updated); + + if (this.config.loadedData === datasetName && updated.length > 0) { + this.config.loadData(updated[0].dataset); + } + + return true; + } + clearFilters() { this.filters.splice(0, this.filters.length); this.refreshFilters(); @@ -445,6 +569,27 @@ export class DataProviderService { this.totalItems = this.buildDataSet(name, timestamp, schema, meta, features, positions); this.filteredItems = this.totalItems; + } else if (item && dataset?.source === 'indexeddb') { + const saved = await this.datasetStorage.getDataset(name); + if (saved) { + const positionsMap = new Map(); + for (const [algo, posArr] of Object.entries(saved.positions)) { + positionsMap.set(algo, posArr); + } + this.config.colorFeature = saved.schema.color; + this.config.replaceActiveFeatures(saved.schema.glyph); + this.config.featureLabels = saved.schema.label; + if (saved.schema.colorRange !== undefined) { + this.config.colorRange = saved.schema.colorRange ? 0 : 4; + } + if (saved.schema.types) { + this.config.featureTypes = saved.schema.types; + } + this.config.updateConfiguration(); + this.totalItems = this.buildDataSet(name, saved.timestamp, saved.schema, saved.meta, saved.features, positionsMap); + this.filteredItems = this.totalItems; + this.extractFeatureMaxValuesFromMeta(name, saved.timestamp); + } } } diff --git a/src/app/services/dataset-storage.service.ts b/src/app/services/dataset-storage.service.ts new file mode 100644 index 0000000..2c81274 --- /dev/null +++ b/src/app/services/dataset-storage.service.ts @@ -0,0 +1,102 @@ +import { Injectable } from '@angular/core'; +import { GlyphSchema } from '../shared/interfaces/glyph-schema'; +import { GlyphMeta } from '../shared/interfaces/glyph-meta'; +import { GlyphFeature } from '../shared/interfaces/glyph-feature'; +import { GlyphPosition } from '../shared/interfaces/glyph-position'; + +export interface StoredDataset { + name: string; + timestamp: string; + savedAt: number; + schema: GlyphSchema; + meta: GlyphMeta; + features: GlyphFeature[]; + positions: { [algorithm: string]: GlyphPosition[] }; +} + +const DB_NAME = 'glyphspace-datasets'; +const DB_VERSION = 1; +const STORE_NAME = 'datasets'; + +@Injectable({ providedIn: 'root' }) +export class DatasetStorageService { + private dbPromise: Promise | null = null; + + private openDb(): Promise { + if (!this.dbPromise) { + this.dbPromise = new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: 'name' }); + } + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => { + this.dbPromise = null; + reject(request.error); + }; + }); + } + return this.dbPromise; + } + + async saveDataset(dataset: StoredDataset): Promise { + try { + const db = await this.openDb(); + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readwrite'); + tx.objectStore(STORE_NAME).put(dataset); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } catch (error) { + console.warn('[DatasetStorage] Failed to save dataset:', error); + } + } + + async getDataset(name: string): Promise { + try { + const db = await this.openDb(); + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readonly'); + const request = tx.objectStore(STORE_NAME).get(name); + request.onsuccess = () => resolve(request.result ?? undefined); + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.warn('[DatasetStorage] Failed to get dataset:', error); + return undefined; + } + } + + async getAllDatasets(): Promise { + try { + const db = await this.openDb(); + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readonly'); + const request = tx.objectStore(STORE_NAME).getAll(); + request.onsuccess = () => resolve(request.result ?? []); + request.onerror = () => reject(request.error); + }); + } catch (error) { + console.warn('[DatasetStorage] Failed to get all datasets:', error); + return []; + } + } + + async deleteDataset(name: string): Promise { + try { + const db = await this.openDb(); + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readwrite'); + tx.objectStore(STORE_NAME).delete(name); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); + } catch (error) { + console.warn('[DatasetStorage] Failed to delete dataset:', error); + } + } +} From 3fd0b39849ec52b039d6c2c29caa7c380735a1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Gr=C3=BCnder?= Date: Sun, 15 Feb 2026 21:03:02 +0100 Subject: [PATCH 02/19] wizard redesign --- src/app/canvas/glyph-canvas.component.ts | 7 - .../settingscontrols.component.scss | 168 +--- .../settingscontrols.component.ts | 54 +- src/app/canvas/tooltip/tooltip.component.scss | 24 +- .../legend-dropdown.component.scss | 6 +- src/app/menubar/menubar.component.ts | 4 - .../models/data-type.enum.ts | 4 + .../models/preprocessing-state.ts | 1 + .../preprocessing-wizard.component.scss | 58 +- .../services/preprocessing.service.ts | 42 +- .../shared/_wizard-shared.scss | 306 +++--- .../column-statistics.component.scss | 41 +- .../shared/constants/step-info.ts | 4 +- .../data-preview-table.component.scss | 45 +- .../wizard-histogram.component.ts | 25 +- .../step1-upload/step1-upload.component.html | 285 +++--- .../step1-upload/step1-upload.component.scss | 81 +- .../step2-column-selection.component.html | 312 +++---- .../step2-column-selection.component.scss | 280 ++---- .../step2-column-selection.component.ts | 18 +- ...ep3-configure-data-features.component.html | 653 +++++++------ ...ep3-configure-data-features.component.scss | 648 +++++++------ ...step3-configure-data-features.component.ts | 75 +- ...tep4-visualization-settings.component.html | 875 +++++++++-------- ...tep4-visualization-settings.component.scss | 877 ++++++++++-------- .../step4-visualization-settings.component.ts | 279 +++++- .../step5-review-processing.component.html | 445 ++++----- .../step5-review-processing.component.scss | 183 +--- .../step5-review-processing.component.ts | 7 +- src/app/services/config.service.ts | 14 - src/app/services/dataprovider.service.ts | 32 +- src/app/services/projection.service.ts | 6 - src/app/shared/interfaces/color-scale.ts | 31 + src/app/shared/interfaces/glyph-schema.ts | 1 + src/assets/preprocessing_processor.py | 4 +- src/assets/preprocessing_processor_config.py | 3 + src/styles.scss | 10 + src/styles/_mixins.scss | 194 +++- 38 files changed, 2901 insertions(+), 3201 deletions(-) diff --git a/src/app/canvas/glyph-canvas.component.ts b/src/app/canvas/glyph-canvas.component.ts index 30e9c5f..b8cddb4 100644 --- a/src/app/canvas/glyph-canvas.component.ts +++ b/src/app/canvas/glyph-canvas.component.ts @@ -610,13 +610,6 @@ export class GlyphCanvasComponent implements OnInit, AfterViewInit, OnDestroy { this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; } - logStatus() { - this.logger.log("Component " + this.id); - this.logger.log("-- isPanning: " + this.isPanning); - this.logger.log("-- magicLensActive: " + this.magicLensComponent.isActive()); - this.logger.log("-- isSelecting: " + this.isSelecting); - this.logger.log("-- selectionMode: " + this.selectionMode); - } //#endregion //#region Rendering and Glyph Manipulations diff --git a/src/app/canvas/settingscontrols/settingscontrols.component.scss b/src/app/canvas/settingscontrols/settingscontrols.component.scss index 68f9abd..d805e97 100644 --- a/src/app/canvas/settingscontrols/settingscontrols.component.scss +++ b/src/app/canvas/settingscontrols/settingscontrols.component.scss @@ -132,34 +132,12 @@ } .form-group { - display: flex; - flex-direction: column; - gap: 8px; + @include m.form-group; margin-bottom: 12px; - - select { - appearance: none; - padding: 8px 12px; - border-radius: 8px; - border: 1px solid #d0d0d0; - background: white; - font-size: 13px; - cursor: pointer; - color: v.$gray-700; - - &:focus { - outline: none; - border-color: v.$color-blue-highlight; - } - } } .menu-label { - font-size: 10px; - font-weight: 500; - color: v.$gray-600; - letter-spacing: 0.06em; - text-transform: uppercase; + @include m.menu-label; } /* Toggle buttons */ @@ -330,104 +308,7 @@ /* Color scale selector */ .color-scale-select { - position: relative; - cursor: pointer; - user-select: none; - - .selected-scale { - display: flex; - align-items: center; - gap: 4px; - padding: 6px 8px; - border-radius: 8px; - border: 1px solid #d0d0d0; - background: white; - - /* Custom arrow */ - &::after { - content: ""; - display: inline-block; - width: 16px; - height: 16px; - margin-left: auto; - background-image: url("data:image/svg+xml;utf8,"); - background-repeat: no-repeat; - background-position: center; - background-size: 16px 16px; - } - } - - .dropdown-icon { - margin-left: auto; - font-size: 20px; - color: #666; - } - - .color-scale-group-label { - font-size: 11px; - font-weight: 500; - color: #888; - padding: 4px 6px 2px; - text-transform: uppercase; - } - - .color-scale-group-divider { - height: 1px; - background: #e0e0e0; - margin: 6px 0; - } - - .color-scale-options { - position: absolute; - top: 100%; - left: 0; - width: 95%; - background: white; - border: 1px solid #d0d0d0; - border-radius: 8px; - margin-top: 4px; - z-index: 10; - display: flex; - flex-direction: column; - padding: 4px; - } - - .color-scale-option { - display: flex; - gap: 4px; - padding: 4px; - border-radius: 6px; - - &:hover { - background: #f0f5ff; - } - - .color-box { - flex: 1; - /* ← each box fills equal space */ - height: 20px; - /* adjust as needed */ - border-radius: 4px; - } - - .color-gradient { - width: 100%; - height: 20px; - border-radius: 4px; - } - } - - .selected-scale .color-box { - flex: 1; - height: 20px; - border-radius: 4px; - } - - .selected-scale .color-gradient { - width: 100%; - height: 20px; - border-radius: 4px; - } + @include m.color-scale-select; } /* Background projection status */ @@ -436,31 +317,13 @@ flex-direction: column; gap: 6px; max-height: 150px; - overflow-y: scroll; // Always show scrollbar area + overflow-y: scroll; overflow-x: hidden; - overscroll-behavior: contain; // Prevent scroll chaining to parent - -webkit-overflow-scrolling: touch; // Smooth scrolling on iOS - touch-action: pan-y; // Allow vertical touch/wheel scrolling - pointer-events: auto; // Ensure this element captures pointer events - - // Scrollbar styling - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: #f0f0f0; - border-radius: 3px; - } - - &::-webkit-scrollbar-thumb { - background: #ccc; - border-radius: 3px; - - &:hover { - background: #aaa; - } - } + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + touch-action: pan-y; + pointer-events: auto; + @include m.styled-scrollbar-thin; } .projection-status-item { @@ -536,19 +399,6 @@ } } -.spinning { - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - .progress-bar-mini { height: 3px; background: #e0e0e0; diff --git a/src/app/canvas/settingscontrols/settingscontrols.component.ts b/src/app/canvas/settingscontrols/settingscontrols.component.ts index 2ef7ac1..9d1065c 100644 --- a/src/app/canvas/settingscontrols/settingscontrols.component.ts +++ b/src/app/canvas/settingscontrols/settingscontrols.component.ts @@ -6,7 +6,11 @@ import { FeaturesData } from '../../shared/interfaces/glyph-meta'; import { GlyphSchema } from '../../shared/interfaces/glyph-schema'; import { DataProviderService } from '../../services/dataprovider.service'; import { ProjectionService } from '../../services/projection.service'; -import { COLOR_SCALES, ColorScale } from '../../shared/interfaces/color-scale'; +import { + COLOR_SCALES, ColorScale, buildGroupedColorScales, + getContinuousGradient as continuousGradientFn, + getCategoricalColors as categoricalColorsFn +} from '../../shared/interfaces/color-scale'; import { GlyphConfiguration } from '../../glyph/glyph-configuration'; import { GlyphType } from '../../shared/enum/glyph-type'; import { Subscription } from 'rxjs'; @@ -24,6 +28,8 @@ export class SettingsControlPanelComponent implements OnDestroy { @Input() visible = false; // controls fade in/out colorScales: ColorScale[] = COLOR_SCALES; + getContinuousGradient = continuousGradientFn; + getCategoricalColors = categoricalColorsFn; groupedColorScales: { group: string; @@ -94,7 +100,7 @@ export class SettingsControlPanelComponent implements OnDestroy { } ngOnInit(): void { - this.groupedColorScales = this.groupColorScales(this.colorScales); + this.groupedColorScales = buildGroupedColorScales(this.colorScales); // Subscribe to background projection status updates // Show running/pending projections, and completed ones for 5 seconds before fading out @@ -280,50 +286,6 @@ export class SettingsControlPanelComponent implements OnDestroy { } } - private groupColorScales(scales: any[]) { - const map = new Map(); - - for (const scale of scales) { - const group = scale.group ?? 'Other'; - if (!map.has(group)) { - map.set(group, []); - } - map.get(group)!.push(scale); - } - - return Array.from(map.entries()).map(([group, scales]) => ({ - group, - scales - })); - } - - getContinuousGradient(scale: any, steps = 10): string { - const domain = scale.scale.domain(); - const min = domain[0]; - const max = domain[domain.length - 1]; - - const colors: string[] = []; - - for (let i = 0; i < steps; i++) { - const t = i / (steps - 1); - const value = min + t * (max - min); - colors.push(scale.scale(value)); - } - - return `linear-gradient(to right, ${colors.join(', ')})`; - } - - getCategoricalColors(scaleDef: any): string[] { - const scale = scaleDef.scale; - - // Ordinal / Quantize / Quantile scales - if (typeof scale.range === 'function') { - return scale.range(); - } - - return []; - } - selectColorScale(id: number) { this.selectedColorScaleId = id; this.colorScaleDropdownOpen = false; diff --git a/src/app/canvas/tooltip/tooltip.component.scss b/src/app/canvas/tooltip/tooltip.component.scss index 0be7ad4..b3e3204 100644 --- a/src/app/canvas/tooltip/tooltip.component.scss +++ b/src/app/canvas/tooltip/tooltip.component.scss @@ -1,3 +1,5 @@ +@use '../../../styles/mixins' as m; + .tooltip { position: fixed; background: rgba(0, 0, 0, 0.75); @@ -14,27 +16,7 @@ max-height: 600px; overflow-wrap: break-word; overflow-y: auto; - - // Dark scrollbar styling for all browsers - scrollbar-width: thin; - scrollbar-color: rgba(255, 255, 255, 0.4) transparent; - - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.4); - border-radius: 3px; - - &:hover { - background: rgba(255, 255, 255, 0.6); - } - } + @include m.styled-scrollbar-dark; &.fixed { background: rgba(0, 0, 0, 1); diff --git a/src/app/menubar/legend-dropdown/legend-dropdown.component.scss b/src/app/menubar/legend-dropdown/legend-dropdown.component.scss index e33bc3b..92da64d 100644 --- a/src/app/menubar/legend-dropdown/legend-dropdown.component.scss +++ b/src/app/menubar/legend-dropdown/legend-dropdown.component.scss @@ -33,11 +33,7 @@ } .menu-label { - font-size: 12px; - font-weight: 300; - color: v.$gray-600; - letter-spacing: 0.05em; - text-transform: uppercase; + @include m.menu-label($size: 12px, $weight: 300); margin-bottom: 8px; } diff --git a/src/app/menubar/menubar.component.ts b/src/app/menubar/menubar.component.ts index 8e7a866..b32c459 100644 --- a/src/app/menubar/menubar.component.ts +++ b/src/app/menubar/menubar.component.ts @@ -58,10 +58,6 @@ export class MenuBarComponent implements OnInit, OnDestroy { this.configService.loadData(name); } - onContextSelect(context: string) { - // Do something with selected context - } - toggleLegend() { this.legendOpen = !this.legendOpen; } diff --git a/src/app/preprocessing-wizard/models/data-type.enum.ts b/src/app/preprocessing-wizard/models/data-type.enum.ts index bccae24..f0cc23d 100644 --- a/src/app/preprocessing-wizard/models/data-type.enum.ts +++ b/src/app/preprocessing-wizard/models/data-type.enum.ts @@ -47,3 +47,7 @@ export enum OutlierMethod { ZScore_3 = 'zscore_3', ZScore_4 = 'zscore_4' } + +export function getDataTypeBadgeClass(dataType: DataType | undefined): string { + return `badge-${dataType}`; +} diff --git a/src/app/preprocessing-wizard/models/preprocessing-state.ts b/src/app/preprocessing-wizard/models/preprocessing-state.ts index f6ac050..7a0c495 100644 --- a/src/app/preprocessing-wizard/models/preprocessing-state.ts +++ b/src/app/preprocessing-wizard/models/preprocessing-state.ts @@ -27,6 +27,7 @@ export interface PreprocessingState { glyphFeatures: string[]; // Array of 5 feature names for glyph rays (ordered) tooltipFeatures: string[]; // Array of feature names for tooltips colorScaleMode: 'continuous' | 'categorical'; // Auto-detected based on color feature data type + colorScaleId: number; // ID of selected color scale from COLOR_SCALES // UI state isProcessing: boolean; diff --git a/src/app/preprocessing-wizard/preprocessing-wizard.component.scss b/src/app/preprocessing-wizard/preprocessing-wizard.component.scss index 709891e..3553be6 100644 --- a/src/app/preprocessing-wizard/preprocessing-wizard.component.scss +++ b/src/app/preprocessing-wizard/preprocessing-wizard.component.scss @@ -1,4 +1,5 @@ @use '../../styles/variables' as v; +@use './shared/wizard-shared' as w; .preprocessing-wizard { display: flex; @@ -98,6 +99,13 @@ display: flex; flex-direction: column; min-height: 0; + + > * { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + } } .step-placeholder { @@ -135,59 +143,13 @@ } .btn-secondary { - display: flex; - align-items: center; - gap: 6px; + @include w.btn-secondary; padding: 8px 16px; - background: rgba(200, 200, 200, 0.5); - color: v.$gray-700; - border: none; - border-radius: 4px; - font-size: 13px; - cursor: pointer; - transition: background-color 0.2s ease; - - .material-icons { - font-size: 18px; - } - - &:hover:not(:disabled) { - background: rgba(180, 180, 180, 0.7); - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } } .btn-primary { - display: flex; - align-items: center; - gap: 6px; + @include w.btn-primary; padding: 8px 16px; - background: v.$active-color; - color: white; - border: none; - border-radius: 4px; - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: opacity 0.2s ease; - - .material-icons { - font-size: 18px; - } - - &:hover:not(:disabled) { - opacity: 0.9; - } - - &:disabled { - background: v.$gray-400; - cursor: not-allowed; - opacity: 0.5; - } } @media (max-width: 768px) { diff --git a/src/app/preprocessing-wizard/services/preprocessing.service.ts b/src/app/preprocessing-wizard/services/preprocessing.service.ts index 64b9379..ea45b8d 100644 --- a/src/app/preprocessing-wizard/services/preprocessing.service.ts +++ b/src/app/preprocessing-wizard/services/preprocessing.service.ts @@ -66,6 +66,7 @@ export class PreprocessingService { glyphFeatures: [], tooltipFeatures: [], colorScaleMode: 'continuous', + colorScaleId: 0, isProcessing: false, processingProgress: 0, processingStep: '', @@ -223,13 +224,29 @@ export class PreprocessingService { } } + // Auto-select matching default scale if current scale type doesn't match + const currentScaleId = this.currentState.colorScaleId; + let colorScaleId = currentScaleId; + const isCategorical = colorScaleMode === 'categorical'; + // Default numeric scales: 0-3, categorical scales: 4-5 + const currentIsCategorical = currentScaleId >= 4; + if (isCategorical !== currentIsCategorical) { + colorScaleId = isCategorical ? 4 : 0; + } + this.updateState({ columnConfigs: new Map(configs), - colorScaleMode: colorScaleMode + colorScaleMode: colorScaleMode, + colorScaleId: colorScaleId }); this.saveStateToStorage(); } + public setColorScaleId(id: number): void { + this.updateState({ colorScaleId: id }); + this.saveStateToStorage(); + } + // Glyph feature mapping public setGlyphFeatures(features: string[]): void { // Validate 3-12 features @@ -240,11 +257,6 @@ export class PreprocessingService { this.saveStateToStorage(); } - public setTooltipFeatures(features: string[]): void { - this.updateState({ tooltipFeatures: features }); - this.saveStateToStorage(); - } - public getPreviewFeatureNames(): string[] { // Returns predicted feature names after encoding return this.predictEncodedFeatureNames(); @@ -317,21 +329,6 @@ export class PreprocessingService { this.saveStateToStorage(); } - // Get column statistics - public getColumnStatistics(columnName: string): ColumnStatistics | undefined { - return this.currentState.dataProfile?.columns.find(col => col.name === columnName); - } - - public getEnabledColumns(): ColumnConfig[] { - return Array.from(this.currentState.columnConfigs.values()) - .filter(config => config.enabled); - } - - public getColumnsForProjection(): ColumnConfig[] { - return this.getEnabledColumns() - .filter(config => config.includeInProjection); - } - // Outlier detection public async detectOutliers(columnName: string, method: OutlierMethod): Promise<{ outlierCount: number; outlierIndices: number[] }> { if (!this.currentState.rawFileName) { @@ -490,7 +487,8 @@ export class PreprocessingService { // Glyph and tooltip feature mappings glyphFeatures: state.glyphFeatures, tooltipFeatures: state.tooltipFeatures.length > 0 ? state.tooltipFeatures : null, - colorScaleMode: state.colorScaleMode + colorScaleMode: state.colorScaleMode, + colorScaleId: state.colorScaleId }; } diff --git a/src/app/preprocessing-wizard/shared/_wizard-shared.scss b/src/app/preprocessing-wizard/shared/_wizard-shared.scss index 71148aa..2af7ff7 100644 --- a/src/app/preprocessing-wizard/shared/_wizard-shared.scss +++ b/src/app/preprocessing-wizard/shared/_wizard-shared.scss @@ -2,16 +2,6 @@ // Shared wizard styles following GlyphSpace design system -// Step containers -@mixin step-container { - display: flex; - flex-direction: column; - height: 100%; - padding: 16px 32px; - max-width: 1150px; - margin: 0 auto; -} - @mixin step-header { text-align: center; margin-bottom: 16px; @@ -33,27 +23,10 @@ @mixin card { background: white; border: 1px solid v.$gray-300; - border-radius: 4px; + border-radius: 8px; overflow: hidden; } -@mixin card-header { - padding: 12px; - border-bottom: 1px solid v.$gray-300; - background: v.$gray-100; - - h3, h4 { - margin: 0; - font-size: 13px; - color: v.$gray-800; - font-weight: 600; - } -} - -@mixin card-body { - padding: 12px; -} - // Buttons @mixin btn-base { display: flex; @@ -61,7 +34,7 @@ gap: 6px; padding: 6px 12px; font-size: 13px; - border-radius: 4px; + border-radius: 8px; cursor: pointer; transition: all 0.2s ease; border: none; @@ -118,66 +91,79 @@ // Badges @mixin badge { padding: 2px 8px; - border-radius: 3px; + border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; } -@mixin badge-info { - @include badge; - background: rgba(0, 188, 212, 0.1); - color: v.$active-color; -} +// Status badge — used in review/processing steps +@mixin status-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; -@mixin badge-warning { - @include badge; - background: rgba(255, 152, 0, 0.1); - color: #E65100; -} + .material-icons { + font-size: 14px; + } -@mixin badge-success { - @include badge; - background: rgba(76, 175, 80, 0.1); - color: #388E3C; -} + &.status-yes, + &.status-complete { + background: rgba(76, 175, 80, 0.1); + color: #388E3C; + } -@mixin badge-gray { - @include badge; - background: v.$gray-200; - color: v.$gray-700; + &.status-no { + background: rgba(158, 158, 158, 0.1); + color: #616161; + } + + &.status-running { + background: rgba(0, 188, 212, 0.1); + color: v.$active-color; + } + + &.status-error { + background: rgba(244, 67, 54, 0.1); + color: #C62828; + } } -// Data type badges - using centralized colors +// Data type badges - using centralized colors (supports both class and attribute selectors) @mixin data-type-badge { @include badge; + flex-shrink: 0; - &[data-type="numeric"] { + &.badge-numeric { background: v.$data-type-numeric-bg; color: v.$data-type-numeric; } - &[data-type="categorical"] { + &.badge-categorical { background: v.$data-type-categorical-bg; color: v.$data-type-categorical; } - &[data-type="text"] { + &.badge-text { background: v.$data-type-text-bg; color: v.$data-type-text; } - &[data-type="date"] { + &.badge-date { background: v.$data-type-date-bg; color: v.$data-type-date; } - &[data-type="boolean"] { + &.badge-boolean { background: v.$data-type-boolean-bg; color: v.$data-type-boolean; } - &[data-type="id"] { + &.badge-id { background: v.$data-type-id-bg; color: v.$data-type-id; } @@ -189,7 +175,7 @@ align-items: center; gap: 8px; padding: 10px 12px; - border-radius: 2px; + border-radius: 8px; font-size: 13px; .material-icons { @@ -211,13 +197,6 @@ color: #E65100; } -@mixin message-success { - @include message-base; - background: rgba(76, 175, 80, 0.1); - border-left: 3px solid #4CAF50; - color: #2E7D32; -} - @mixin message-error { @include message-base; background: rgba(244, 67, 54, 0.1); @@ -229,7 +208,7 @@ @mixin input-base { padding: 8px 10px; border: 1px solid v.$gray-300; - border-radius: 2px; + border-radius: 8px; font-size: 13px; background: white; transition: border-color 0.2s ease; @@ -261,7 +240,62 @@ accent-color: v.$active-color; } +// Search box — shared between column selection, data features, and legend +@mixin search-box($padding: 5px 8px, $icon-size: 18px, $border-color: v.$gray-300) { + display: flex; + align-items: center; + background: white; + border: 1px solid $border-color; + border-radius: 8px; + padding: $padding; + + .material-icons { + color: v.$gray-500; + font-size: $icon-size; + margin-right: 6px; + } + + .search-input { + flex: 1; + border: none; + outline: none; + font-size: 13px; + color: v.$gray-800; + background: transparent; + + &::placeholder { + color: v.$gray-500; + } + } + + .clear-search { + background: none; + border: none; + padding: 2px; + cursor: pointer; + display: flex; + align-items: center; + color: v.$gray-500; + + &:hover { + color: v.$gray-800; + } + + .material-icons { + font-size: 16px; + margin: 0; + } + } +} + // Tables +@mixin table-container { + background: white; + border: 1px solid v.$gray-300; + border-radius: 8px; + overflow-x: auto; +} + @mixin table { width: 100%; border-collapse: collapse; @@ -270,30 +304,50 @@ thead { background: v.$wizard-table-header-bg; - border-bottom: 2px solid v.$gray-300; - } + position: sticky; + top: 0; + z-index: 10; - th { - padding: 8px 10px; - text-align: left; - font-weight: 600; - color: v.$gray-700; - white-space: nowrap; - } + tr { + border-bottom: 2px solid v.$gray-300; + } - td { - padding: 8px 10px; - border-top: 1px solid v.$gray-200; - color: v.$gray-800; + th { + padding: 10px 12px; + text-align: left; + font-weight: 600; + color: v.$gray-700; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + white-space: nowrap; + background: v.$wizard-table-header-bg; + } } - tbody tr { - &:hover { - background: v.$gray-100; + tbody { + tr { + border-bottom: 1px solid v.$gray-200; + + &:hover { + background: v.$gray-100; + } + } + + td { + padding: 10px 12px; + vertical-align: middle; + color: v.$gray-800; } } } +// Table cell helpers +@mixin column-name-text { + font-weight: 500; + color: v.$gray-800; +} + // Progress bars @mixin progress-bar { width: 100%; @@ -310,16 +364,17 @@ } // No content states -@mixin no-content { +@mixin no-content($padding: 40px 20px, $icon-size: 48px, $text-size: 13px) { display: flex; flex-direction: column; align-items: center; justify-content: center; - padding: 40px 20px; + padding: $padding; text-align: center; + color: v.$gray-600; .material-icons { - font-size: 48px; + font-size: $icon-size; color: v.$gray-400; margin-bottom: 12px; } @@ -331,19 +386,29 @@ } p { - font-size: 13px; + font-size: $text-size; color: v.$gray-600; margin: 4px 0; } } +// Step content wrapper - constrains content width while allowing footer to be full-width +@mixin step-content { + flex: 1; + padding: 16px 32px; + max-width: 1150px; + width: 100%; + margin: 0 auto; + box-sizing: border-box; +} + // Action buttons footer - fixed at bottom with Back on left, Next on right @mixin action-buttons-footer { display: flex; justify-content: space-between; align-items: center; gap: 12px; - padding: 16px; + padding: 16px 32px; border-top: 1px solid v.$gray-300; margin-top: auto; background: v.$wizard-header-bg; @@ -374,15 +439,6 @@ } } -@mixin success-box { - @include message-success; - margin-bottom: 16px; - - .material-icons { - flex-shrink: 0; - } -} - // Step purpose statement @mixin step-purpose { text-align: center; @@ -414,18 +470,6 @@ } } -// Badge with data loss warning -@mixin badge-data-loss { - @include badge-warning; - display: inline-flex; - align-items: center; - gap: 4px; - - .material-icons { - font-size: 14px; - } -} - // Helper classes that can be used directly in templates .info-box { @include info-box; @@ -435,10 +479,6 @@ @include warning-box; } -.success-box { - @include success-box; -} - .step-purpose { @include step-purpose; } @@ -446,47 +486,3 @@ .feature-progress { @include feature-progress; } - -.badge-data-loss { - @include badge-data-loss; -} - -.badge-numeric { - @include badge; - background: v.$data-type-numeric-bg; - color: v.$data-type-numeric; -} - -.badge-categorical { - @include badge; - background: v.$data-type-categorical-bg; - color: v.$data-type-categorical; -} - -.badge-text { - @include badge; - background: v.$data-type-text-bg; - color: v.$data-type-text; -} - -.badge-date { - @include badge; - background: v.$data-type-date-bg; - color: v.$data-type-date; -} - -.badge-boolean { - @include badge; - background: v.$data-type-boolean-bg; - color: v.$data-type-boolean; -} - -.badge-id { - @include badge; - background: v.$data-type-id-bg; - color: v.$data-type-id; -} - -.badge-warning { - @include badge-warning; -} diff --git a/src/app/preprocessing-wizard/shared/column-statistics/column-statistics.component.scss b/src/app/preprocessing-wizard/shared/column-statistics/column-statistics.component.scss index 037a466..e92eeca 100644 --- a/src/app/preprocessing-wizard/shared/column-statistics/column-statistics.component.scss +++ b/src/app/preprocessing-wizard/shared/column-statistics/column-statistics.component.scss @@ -1,4 +1,5 @@ @use '../../../../styles/variables' as v; +@use '../../shared/wizard-shared' as w; .column-statistics { background: white; @@ -32,42 +33,11 @@ } .data-type-badge { + @include w.data-type-badge; padding: 4px 12px; border-radius: 12px; - font-size: 11px; - font-weight: 600; white-space: nowrap; - &.badge-numeric { - background: rgba(0, 188, 212, 0.1); - color: v.$active-color; - } - - &.badge-categorical { - background: rgba(156, 39, 176, 0.1); - color: #7B1FA2; - } - - &.badge-text { - background: rgba(255, 152, 0, 0.1); - color: #E65100; - } - - &.badge-date { - background: rgba(76, 175, 80, 0.1); - color: #388E3C; - } - - &.badge-boolean { - background: rgba(233, 30, 99, 0.1); - color: #C2185B; - } - - &.badge-id { - background: v.$gray-200; - color: v.$gray-700; - } - &.badge-unknown { background: v.$gray-100; color: v.$gray-600; @@ -121,16 +91,13 @@ } .completeness-bar { + @include w.progress-bar; flex: 1; height: 8px; - background: v.$gray-100; - border-radius: 4px; - overflow: hidden; } .completeness-fill { - height: 100%; - transition: width 0.3s ease; + @include w.progress-fill; &.good { background: #4CAF50; diff --git a/src/app/preprocessing-wizard/shared/constants/step-info.ts b/src/app/preprocessing-wizard/shared/constants/step-info.ts index ef7a6de..622aacd 100644 --- a/src/app/preprocessing-wizard/shared/constants/step-info.ts +++ b/src/app/preprocessing-wizard/shared/constants/step-info.ts @@ -19,11 +19,11 @@ export const STEP_INFO: Record = { }, 2: { title: 'Configure Data & Features', - purpose: 'Configure data cleaning, encoding, scaling, and projection settings for each column in a unified table view.' + purpose: 'Configure encoding, scaling, and data cleaning settings for each column. Select a column to view and edit its options.' }, 3: { title: 'Visualization Settings', - purpose: 'Select color and glyph features, and choose projection methods.' + purpose: 'Configure glyph features with live preview, set color mapping, and select projection methods.' }, 4: { title: 'Review & Process', diff --git a/src/app/preprocessing-wizard/shared/data-preview-table/data-preview-table.component.scss b/src/app/preprocessing-wizard/shared/data-preview-table/data-preview-table.component.scss index cf7179b..3f8953d 100644 --- a/src/app/preprocessing-wizard/shared/data-preview-table/data-preview-table.component.scss +++ b/src/app/preprocessing-wizard/shared/data-preview-table/data-preview-table.component.scss @@ -1,9 +1,8 @@ @use '../../../../styles/variables' as v; +@use '../../shared/wizard-shared' as wizard; .data-preview-table { - background: white; - border-radius: 4px; - border: 1px solid v.$gray-200; + @include wizard.table-container; overflow: hidden; } @@ -15,53 +14,19 @@ .table-container { overflow-x: auto; - max-height: 400px; - overflow-y: auto; } table { - width: 100%; - border-collapse: collapse; - font-size: 13px; -} - -thead { - position: sticky; - top: 0; - background: v.$gray-100; - z-index: 10; - - th { - padding: 10px 12px; - text-align: left; - font-weight: 600; - color: v.$gray-800; - border-bottom: 2px solid v.$gray-200; - white-space: nowrap; + @include wizard.table; + thead th { &.highlighted { background: rgba(0, 188, 212, 0.1); color: v.$active-color; } } -} - -tbody { - tr { - &:nth-child(even) { - background: v.$gray-100; - } - - &:hover { - background: v.$gray-200; - } - } - - td { - padding: 10px 12px; - border-bottom: 1px solid v.$gray-100; - color: v.$gray-600; + tbody td { &.highlighted { background: rgba(76, 175, 80, 0.1); } diff --git a/src/app/preprocessing-wizard/shared/wizard-histogram/wizard-histogram.component.ts b/src/app/preprocessing-wizard/shared/wizard-histogram/wizard-histogram.component.ts index c3ae70b..b282154 100644 --- a/src/app/preprocessing-wizard/shared/wizard-histogram/wizard-histogram.component.ts +++ b/src/app/preprocessing-wizard/shared/wizard-histogram/wizard-histogram.component.ts @@ -6,8 +6,7 @@ import { DataType } from '../../models/data-type.enum'; import { StackedBin } from '../../../shared/types/histogram.types'; import { prepareStackedBinsFromArray, - rebinHistogramData, - darkenColor + rebinHistogramData } from '../../../shared/utils/histogram.utils'; @Component({ @@ -133,7 +132,6 @@ export class WizardHistogramComponent implements OnInit, OnChanges, AfterViewIni if (bins.length === 0) return; const displayColor = this.enabled ? this.color : '#ccc'; - const darkerColor = darkenColor(displayColor); const bars = this.svg.selectAll('rect') .data(bins) @@ -147,7 +145,7 @@ export class WizardHistogramComponent implements OnInit, OnChanges, AfterViewIni .attr('fill', displayColor) .attr('rx', 2) .attr('ry', 2) - .attr('opacity', this.enabled ? 0.8 : 0.5) + .attr('opacity', this.enabled ? 0.4 : 0.3) .style('cursor', this.enabled ? 'pointer' : 'default') .style('pointer-events', 'all'); @@ -160,13 +158,13 @@ export class WizardHistogramComponent implements OnInit, OnChanges, AfterViewIni if (this.lastHoveredBar) { d3.select(this.lastHoveredBar) .attr('fill', displayColor) - .attr('opacity', 0.8); + .attr('opacity', 0.4); } - // Highlight current bar with darker version of original color + // Highlight current bar with full color d3.select(currentBar) - .attr('fill', darkerColor) - .attr('opacity', 1); + .attr('fill', displayColor) + .attr('opacity', 0.9); this.lastHoveredBar = currentBar; @@ -185,7 +183,6 @@ export class WizardHistogramComponent implements OnInit, OnChanges, AfterViewIni const counts = rebinHistogramData(this.data.counts, this.MAX_NUMERIC_BINS); const bins = counts.map((value: number, index: number) => ({ bin: index, value })); const displayColor = this.enabled ? this.color : '#ccc'; - const darkerColor = darkenColor(displayColor); const xScale = d3.scaleLinear() .domain([0, bins.length]) @@ -211,7 +208,7 @@ export class WizardHistogramComponent implements OnInit, OnChanges, AfterViewIni .attr('fill', displayColor) .attr('rx', 2) .attr('ry', 2) - .attr('opacity', this.enabled ? 0.8 : 0.5) + .attr('opacity', this.enabled ? 0.4 : 0.3) .style('cursor', this.enabled ? 'pointer' : 'default') .style('pointer-events', 'all'); @@ -224,13 +221,13 @@ export class WizardHistogramComponent implements OnInit, OnChanges, AfterViewIni if (this.lastHoveredBar) { d3.select(this.lastHoveredBar) .attr('fill', displayColor) - .attr('opacity', 0.8); + .attr('opacity', 0.4); } - // Highlight current bar with darker version of original color + // Highlight current bar with full color d3.select(currentBar) - .attr('fill', darkerColor) - .attr('opacity', 1); + .attr('fill', displayColor) + .attr('opacity', 0.9); this.lastHoveredBar = currentBar; diff --git a/src/app/preprocessing-wizard/steps/step1-upload/step1-upload.component.html b/src/app/preprocessing-wizard/steps/step1-upload/step1-upload.component.html index 5725b80..78abd84 100644 --- a/src/app/preprocessing-wizard/steps/step1-upload/step1-upload.component.html +++ b/src/app/preprocessing-wizard/steps/step1-upload/step1-upload.component.html @@ -1,186 +1,173 @@
-
-

{{ stepInfo.title }}

-

{{ stepInfo.purpose }}

-
+
+
+

{{ stepInfo.title }}

+

{{ stepInfo.purpose }}

+
- @if (!profile) { - -
- info -
- Supported formats: CSV files up to 50MB. - Your data will be analyzed automatically to detect column types and quality issues. + @if (!profile) { + +
+ info +
+ Supported formats: CSV files up to 50MB. + Your data will be analyzed automatically to detect column types and quality issues. +
-
- -
+ +
- @if (isLoading) { -
-
-
Loading and analyzing your data...
-
- } @else { -
- cloud_upload -
- Drag and drop your CSV file here - or + @if (isLoading) { +
+
+
Loading and analyzing your data...
- -
- Maximum file size: 50MB + } @else { +
+ cloud_upload +
+ Drag and drop your CSV file here + or +
+ +
+ Maximum file size: 50MB +
-
- } -
- - @if (error) { -
- error - {{ error }} + }
- } - -
-

- File Requirements - -

-
    -
  • First row should contain column headers
  • -
  • ID column (optional): Unique identifier for each row
  • -
  • Pre-computed coordinates (optional): "<name>-x" and "<name>-y" pairs (e.g., "pca-x", "pca-y")
  • -
  • Geographic data (optional): "latitude" and "longitude" columns
  • -
  • File too large? Try filtering rows in Excel or reducing columns before upload
  • -
-
- } @else { - -
-
-
- check_circle -

Data Loaded Successfully

-
-
- + @if (error) { +
+ error + {{ error }}
+ } + +
+

+ File Requirements + +

+
    +
  • First row should contain column headers
  • +
  • ID column (optional): Unique identifier for each row
  • +
  • Pre-computed coordinates (optional): "<name>-x" and "<name>-y" pairs (e.g., "pca-x", "pca-y")
  • +
  • Geographic data (optional): "latitude" and "longitude" columns
  • +
  • File too large? Try filtering rows in Excel or reducing columns before upload
  • +
-
-
- description -
-
File Name
-
{{ profile.fileName }}
+ } @else { + +
+
+
+ check_circle +

Data Loaded Successfully

-
- -
- storage -
-
File Size
-
{{ formatFileSize(profile.fileSize) }}
+
+
-
- table_rows -
-
Total Rows
-
{{ profile.totalRows }}
+
+
+ description +
+
File Name
+
{{ profile.fileName }}
+
-
-
- view_column -
-
Total Columns
-
{{ profile.totalColumns }}
+
+ storage +
+
File Size
+
{{ formatFileSize(profile.fileSize) }}
+
-
-
- assessment -
-
- Data Quality - -
-
- {{ profile.qualityScore.toFixed(1) }}% ({{ qualityLabel }}) +
+ table_rows +
+
Total Rows
+
{{ profile.totalRows }}
-
- @if (profile.duplicateCount > 0) { -
- content_copy +
+ view_column
-
Duplicates Found
-
{{ profile.duplicateCount }}
+
Total Columns
+
{{ profile.totalColumns }}
- } -
-
-

Data Preview (First 5 rows)

- - -
+
+ assessment +
+
+ Data Quality + +
+
+ {{ profile.qualityScore.toFixed(1) }}% ({{ qualityLabel }}) +
+
+
-
-

Column Overview

-
- @for (column of profile.columns; track column.name) { -
- {{ column.name }} - {{ column.dataType }} - @if (column.missingPercentage > 0) { - - {{ column.missingPercentage.toFixed(0) }}% missing - - } + @if (profile.duplicateCount > 0) { +
+ content_copy +
+
Duplicates Found
+
{{ profile.duplicateCount }}
+
}
-
-
-
-
- +
+

Data Preview (First 10 rows)

+ +
+ } +
+ + @if (profile) { +
+
+
+ +
}
diff --git a/src/app/preprocessing-wizard/steps/step1-upload/step1-upload.component.scss b/src/app/preprocessing-wizard/steps/step1-upload/step1-upload.component.scss index 0f3a36d..c2964dd 100644 --- a/src/app/preprocessing-wizard/steps/step1-upload/step1-upload.component.scss +++ b/src/app/preprocessing-wizard/steps/step1-upload/step1-upload.component.scss @@ -4,10 +4,17 @@ .step1-upload { display: flex; flex-direction: column; - height: 100%; - max-width: 1000px; + flex: 1; + min-height: 0; +} + +.step-content { + flex: 1; + max-width: 1150px; margin: 0 auto; - padding: 16px; + padding: 16px 32px; + width: 100%; + box-sizing: border-box; } .step-header { @@ -93,11 +100,6 @@ animation: spin 0.8s linear infinite; } -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - .loading-text { font-size: 13px; color: v.$gray-600; @@ -256,69 +258,6 @@ } } -.columns-summary { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.column-chip { - display: flex; - align-items: center; - gap: 6px; - padding: 6px 12px; - background: white; - border: 1px solid v.$gray-300; - border-radius: 3px; - font-size: 12px; - - &[data-type="numeric"] { - border-left: 3px solid v.$active-color; - } - - &[data-type="categorical"] { - border-left: 3px solid v.$gray-600; - } - - &[data-type="text"] { - border-left: 3px solid #FF9800; - } - - &[data-type="date"] { - border-left: 3px solid v.$gray-500; - } - - &[data-type="id"] { - border-left: 3px solid v.$gray-400; - } -} - -.column-name { - font-weight: 600; - color: v.$gray-800; -} - -.column-type { - padding: 2px 6px; - background: v.$gray-100; - border-radius: 2px; - font-size: 11px; - color: v.$gray-600; - text-transform: uppercase; -} - -.column-badge { - padding: 2px 6px; - border-radius: 2px; - font-size: 11px; - font-weight: 600; - - &.warning { - background: rgba(255, 152, 0, 0.1); - color: #E65100; - } -} - .action-buttons { @include wizard.action-buttons-footer; diff --git a/src/app/preprocessing-wizard/steps/step2-column-selection/step2-column-selection.component.html b/src/app/preprocessing-wizard/steps/step2-column-selection/step2-column-selection.component.html index 3eab212..e13750f 100644 --- a/src/app/preprocessing-wizard/steps/step2-column-selection/step2-column-selection.component.html +++ b/src/app/preprocessing-wizard/steps/step2-column-selection/step2-column-selection.component.html @@ -1,176 +1,178 @@
-
-

{{ stepInfo.title }}

-

{{ stepInfo.purpose }}

-
- - -
- info -
- Tip: Start by deselecting ID columns and text fields unless needed for tooltips. - You can search by column name or type (e.g., "numeric"). +
+
+

{{ stepInfo.title }}

+

{{ stepInfo.purpose }}

-
-
-