/ app.component.scss app.component.ts app.component.html components/ visualization/ visualization.component.scss visualization.component.ts visualization.component.html visualization.component.spec.ts modules/ dag-feature.module.ts svg-feature.module.ts intersection-mode-feature.module.ts intersection-details-dialog/ intersection-details-dialog.component.spec.ts intersection-details-dialog.component.scss intersection-details-dialog.component.ts intersection-details-dialog.component.html datapoint-tooltip/ datapoint-tooltip.component.scss datapoint-tooltip.component.ts datapoint-tooltip.component.html datapoint-tooltip.component.spec.ts search-dialog/ search-dialog.component.spec.ts search-dialog.component.scss search-dialog.component.ts search-dialog.component.html main_options/ rss-view/ rss-view.component.html rss-view.component.scss rss-view.component.spec.ts rss-view.component.ts file-selection-dialog/ file-selection-dialog.component.ts file-selection-dialog.component.html file-selection-dialog.component.spec.ts file-selection-dialog.component.scss pattern-summary/ pattern-summary.component.html pattern-summary.component.spec.ts pattern-summary.component.scss pattern-summary.component.ts pattern-dim-dialog/ pattern-dim-dialog.component.ts pattern-dim-dialog.component.scss pattern-dim-dialog.component.html pattern-dim-dialog.component.spec.ts error-dialog/ error-dialog.component.scss error-dialog.component.ts error-dialog.component.html error-dialog.component.spec.ts models/ color.ts intersection_details.ts pattern.ts svg.ts datapoint.ts services/ svg/ svg.service.spec.ts svg.service.ts dialog/ dialog.service.ts dialog.service.spec.ts api/ api.service.spec.ts api.service.ts
@use '@angular/material' as mat; // @import '../style.scss'; @import '../theme.scss';
// $primary-palette: map-get($map: , $key: )
$max_nav_height: 5vh;
body{ // $background: map-get($theme, background); overflow: hidden; background-color: $background-color; // position: absolute; z-index: 1; // top: 0px; // right: 0; // left: 0; // bottom: 0px; display: flex; flex-direction: row;
width: 100vw;
height: 100vh;
aside{
z-index: 2;
background-color: mat.get-color-from-palette($primary-palette, 900);
color: mat.get-color-from-palette($primary-palette, '900-contrast');
// flex: 1;
display: flex;
flex-direction: column;
width: 25vw;
max-height: 100vh;
min-height: 100vh;
// background-color: green;
#main-options{
display: flex;
flex-direction: row;
justify-content: space-around;
// background-color: brown;
padding-top: 0.5em;
padding-bottom: 0.5em;
padding-left: 0.5em;
padding-right: 0.5em;
height: 10vh;
#matlist-placeholder{
min-height: $max_nav_height;
max-height: $max_nav_height;
padding-top: 1em;
padding-bottom: 1em;
padding-left: 1em;
padding-right: 1em;
}
.main-option{
min-height: $max_nav_height;
max-height: $max_nav_height;
padding-top: 1em;
padding-bottom: 1em;
padding-left: 1em;
padding-right: 1em;
user-select: none;
display: flex;
flex-direction: row;
align-items: center;
border-radius: 10px;
background-color: mat.get-color-from-palette($primary-palette, 900);
mat-icon{
font-size: 2.5em;
width: 1em;
height: 1em;
}
h1{
max-width: 80%;
margin: 0 0 0 0;
padding-top: 0.5em;
padding-bottom: 0.5em;
padding-left: 0.5em;
padding-right: 0.5em;
word-wrap: break-word;
font-size: 1.25em;
}
}
.main-option:hover{
background-color: mat.get-color-from-palette($primary-palette, 800);
cursor: pointer;
}
#truncate-model-button{
mat-icon{
transform: scaleX(-1);
}
}
}
#content-cover{
width: 25vw;
height: 90vh;
position: absolute;
top: 10vh;
z-index: 2;
background-color: rgba(0, 0, 0, 1);
}
.sidenav-content{
height: 90vh;
background-color: mat.get-color-from-palette($primary-palette, 900);
#settings-wrapper-lower-content{
width: 100%;
height: 100%;
}
#settings-wrapper-upper-content{
background-color: mat.get-color-from-palette($primary-palette, 900);
width: 80%;
nav{
display: flex;
flex-direction: column;
.setting{
color: white;
background-color: mat.get-color-from-palette($primary-palette, 800);
margin: 0.5em 1em 0.5em 1em;
}
}
}
#pattern-summary{
width: 100%;
height: 100%;
color: white;
}
app-dynamic-paginator{
flex: 4;
}
}
h2{
padding-left: 1em;
padding-top: 1em;
}
}
#main-app{
// background-color: green;
width: 75vw;
height: 100vh;
flex: 4;
display: flex;
flex-direction: column;
#rss_view {
z-index: 2;
position: absolute;
top: 10vh;
left: 0vw;
height: 90vh;
width: 25vw;
}
#visualization{
width: 100%;
height: 100%;
// flex: 14;
#select-model-warning-wrapper{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
#button-wrapper{
display: flex;
align-items: center;
justify-content: center;
padding-top: 1em;
padding-bottom: 1em;
padding-left: 1em;
padding-right: 1em;
user-select: none;
display: flex;
flex-direction: row;
align-items: center;
border-radius: 10px;
background-color: mat.get-color-from-palette($primary-palette, 800);
width: 40em;
height: 10em;
h1{
font-size: 2em;
color: white;
}
}
#button-wrapper:hover{
background-color: mat.get-color-from-palette($primary-palette, 600);
cursor: pointer;
}
}
#progress-spinner{
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
h1{
user-select: none;
padding: 2em 0 0 0;
}
}
}
footer{
// flex: 1;
// background-color: brown;
}
}
}
.floatable { z-index: 1; position: absolute; }
.buttonToggled{ background-color: mat.get-color-from-palette($primary-palette, 800) !important; }
.main-option-disabled{ color: rgb(145, 145, 145) !important; background-color: rgb(209, 209, 209) !important; cursor: not-allowed !important; }
// https://www.telerik.com/blogs/angular-14-introducing-standalone-components#:~:text=Creating%20a%20Standalone%20Component,ng%20g%20c%20login%20%2D%2Dstandalone // https://material.angular.io/components/categories // https://css-tricks.com/snippets/css/a-guide-to-flexbox/ // https://br.pinterest.com/pin/800022321275429738/ // import * as numeric from 'numeric';
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, ViewChild , Renderer2, OnDestroy} from "@angular/core"; import { invoke } from "@tauri-apps/api/tauri"; import { VisualizationComponent } from "./components/visualization/visualization.component"; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import {MatTabsModule} from '@angular/material/tabs'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; import {MatDividerModule} from '@angular/material/divider'; import {MatListModule} from '@angular/material/list'; import {MatSelectModule} from '@angular/material/select'; import {MatCheckboxModule} from '@angular/material/checkbox'; import {MatMenuModule} from '@angular/material/menu'; import {MatButtonModule} from '@angular/material/button'; import {MatRippleModule} from '@angular/material/core'; import {MatPaginatorModule} from '@angular/material/paginator'; import {MatIconModule} from '@angular/material/icon'; import { CommonModule } from "@angular/common"; import { open } from '@tauri-apps/api/dialog'; import { RssViewComponent } from "./components/main_options/rss-view/rss-view.component"; import { provideRouter, Router, RouterOutlet} from "@angular/router"; import { environment } from "src/environments/environment"; import {MatSidenav, MatSidenavModule} from '@angular/material/sidenav' import { animate, state, style, transition, trigger } from '@angular/animations'; import { MatTooltipModule } from '@angular/material/tooltip'; import { FileSelectionDialogComponent } from './components/main_options/file-selection-dialog/file-selection-dialog.component'; import { take } from "rxjs/operators"; import { DialogService } from "./services/dialog/dialog.service"; import { ErrorDialogComponent } from "./components/error-dialog/error-dialog.component"; import { PatternSummaryComponent } from "./components/pattern-summary/pattern-summary.component"; import { Pattern } from "./models/pattern"; import { fs } from "@tauri-apps/api"; import { resolveResource } from "@tauri-apps/api/path"; import { ApiService } from "./services/api/api.service"; import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; import { Subscription } from "rxjs"; import { SearchDialogComponent } from "./components/visualization/search-dialog/search-dialog.component"; import { DataPoint } from "./models/datapoint";
export enum MainOption { MODEL_SELECTOR, SETTINGS, TRUNCATE_MODEL, INTERSECTION_MODE, HIGHLIGHT_SUPERPATTERNS, SEARCH };
export enum ApplicationStatus { UNLOADED, LOADING, LOADED };
@Component({ selector: "app-root", templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], standalone: true, animations: [ trigger('slideInOut', [ state('void', style({ transform: 'translateX(-100%)', opacity: 0 })), state('in', style({ transform: 'translateX(0)', opacity: 1 })), state('out', style({ transform: 'translateX(-100%)', opacity: 0 })), transition('void => in', [animate('0.4s ease-in-out')]), transition('in => out', [animate('0.4s ease-in-out')]), transition('out => in', [animate('0.4s ease-in-out')]) ]) ], imports: [RouterOutlet, CommonModule, VisualizationComponent, RssViewComponent, MatSlideToggleModule, MatTabsModule, MatButtonToggleModule, MatDividerModule, MatListModule, MatSelectModule, MatSlideToggleModule, MatCheckboxModule, MatMenuModule, MatButtonModule, MatRippleModule, MatPaginatorModule, MatSidenavModule, MatIconModule, MatTooltipModule, PatternSummaryComponent, MatProgressSpinnerModule] })
export class AppComponent implements AfterViewInit, OnDestroy{ protected all_application_status = Object.values(ApplicationStatus); protected application_status: ApplicationStatus = ApplicationStatus.UNLOADED;
protected MainOption = MainOption; protected settings_enabled: boolean = false; protected truncate_model_enabled: boolean; protected intersection_mode_enabled: boolean = false; protected highlight_superpatterns_enabled: boolean = false;
protected truncate_model_disabled: boolean = false; protected highlight_superpatterns_disabled: boolean = false;
private previous_filters: string[][];
@ViewChild("aside") aside: ElementRef; public matList_height: number;
@ViewChild("sidenav") sidenav: MatSidenav; @ViewChild("model_selector") model_selector: ElementRef; private last_opened_folder: string = ""; protected tensor_path: string = ""; protected patterns_path: string = ""; @ViewChild('rss_view') rss_view: RssViewComponent; @ViewChild('pattern_summary') pattern_summary: PatternSummaryComponent;
@ViewChild('visualization_view') visualization_view: VisualizationComponent; protected hovered_pattern: Pattern;
private datapoint_click_subscription: Subscription; private dag_change_subscription: Subscription;
constructor(private cdr: ChangeDetectorRef, private dialog_service: DialogService, private api_service: ApiService){}
async ngAfterViewInit(){ if(environment.dev_mode){ console.log("Entering dev mode");
// await fs.readTextFile(await resolveResource('resources/'))
// let base_path = "../../src-tauri/tests/test_data"
let tensor_path = await resolveResource('resources/dev_tensor.txt');
let patterns_path = await resolveResource('resources/dev_patterns.txt');
// let patterns_path = `${base_path}/other_patterns/primary_school.txt`
this.handleModelChange({tensor_path: tensor_path, patterns_path: patterns_path});
}
}
ngOnDestroy(){ this.datapoint_click_subscription.unsubscribe(); this.dag_change_subscription.unsubscribe(); }
private async handleModelChange(event: any){ console.log("Handling model change"); if (event.tensor_path == null || event.patterns_path == null){ return; } this.application_status = ApplicationStatus.LOADING; this.cdr.detectChanges();
this.last_opened_folder = event.last_opened_folder;
this.tensor_path = event.tensor_path;
this.patterns_path = event.patterns_path;
try{
await this.api_service.initApplication(this.tensor_path, this.patterns_path);
} catch(error){
console.error(error);
this.application_status = ApplicationStatus.UNLOADED;
this.cdr.detectChanges();
return;
}
this.application_status = ApplicationStatus.LOADED;
this.cdr.detectChanges();
this.datapoint_click_subscription = this.visualization_view.datapoint_click.subscribe(identifier => this.onDatapointClick(identifier));
this.dag_change_subscription = this.visualization_view.dag_change.subscribe(() => this.onDagChange());
// this.reloadApplication();
this.toggleMainOption(null);
this.togglePatternSummary(null);
this.updatePatternSummary(null);
}
protected toggleMainOption(option: MainOption | null){ if (this.isOptionDisabled(option)) { return; } this.deactivateMainOptionsExcept(option);
switch(option){
case MainOption.MODEL_SELECTOR:
this.openModelSelection();
break;
case MainOption.SETTINGS:
this.toggleSettings();
break;
case MainOption.TRUNCATE_MODEL:
this.toggleTruncateModel();
break;
case MainOption.HIGHLIGHT_SUPERPATTERNS:
this.toggleHighlightSuperpatterns();
break;
case MainOption.SEARCH:
this.openSearch();
break;
case null:
break
}
}
private isOptionDisabled(option: MainOption): boolean{ switch(option){ case MainOption.SETTINGS: return false; case MainOption.TRUNCATE_MODEL: return this.truncate_model_disabled; case MainOption.HIGHLIGHT_SUPERPATTERNS: return this.highlight_superpatterns_disabled; default: return false; } }
private deactivateMainOptionsExcept(option: MainOption){ if(this.settings_enabled && option != MainOption.SETTINGS){ this.toggleSettings(); } if(this.truncate_model_enabled && option != MainOption.TRUNCATE_MODEL){ this.toggleTruncateModel(); } if(this.highlight_superpatterns_enabled && option != MainOption.HIGHLIGHT_SUPERPATTERNS){ this.toggleHighlightSuperpatterns(); } }
private openModelSelection(): void { let dialog_data = { last_opened_folder: this.last_opened_folder, tensor_path: this.tensor_path, patterns_path: this.patterns_path }; this.dialog_service.open(FileSelectionDialogComponent, FileSelectionDialogComponent.WIDTH, FileSelectionDialogComponent.HEIGHT, dialog_data, this.handleModelChange.bind(this)); }
private toggleSettings(){ this.settings_enabled = !this.settings_enabled; this.sidenav.toggle(); }
private toggleTruncateModel(){ if(this.truncate_model_enabled == undefined){ return; }
this.truncate_model_enabled = !this.truncate_model_enabled;
this.cdr.detectChanges();
}
private toggleHighlightSuperpatterns(){ if(this.highlight_superpatterns_enabled == undefined){ return; }
this.highlight_superpatterns_enabled = !this.highlight_superpatterns_enabled;
this.visualization_view.toggleHighlightSuperpatterns(this.highlight_superpatterns_enabled);
this.pattern_summary.update(null);
this.cdr.detectChanges();
}
private async filterDatapoints(filters: string[][]){ this.previous_filters = filters; this.visualization_view.filterDatapoints(filters); }
private openSearch(){ this.visualization_view.openSearch(); this.pattern_summary.update(null); this.cdr.detectChanges();
let dialog_data = {previous_filters: this.previous_filters};
this.dialog_service.open(SearchDialogComponent,
SearchDialogComponent.WIDTH,
SearchDialogComponent.HEIGHT,
dialog_data,
this.filterDatapoints.bind(this));
}
private onDatapointClick(identifier){ this.highlight_superpatterns_enabled = false; }
protected disableRssView(){ this.truncate_model_enabled = false; this.cdr.detectChanges(); }
protected onTruncation(event){ this.rss_view.disableSlider(); setTimeout(() => { this.rss_view.enableSlider(); }, 1100);
this.visualization_view.onTruncation(event);
}
private async onDagChange(){ if(this.visualization_view.isOnFirstLevel()){ await this.rss_view.reset(); }
this.truncate_model_disabled = !this.visualization_view.isOnFirstLevel();
this.highlight_superpatterns_disabled = !this.visualization_view.isOnFirstLevel();
this.truncate_model_enabled = false;
this.highlight_superpatterns_enabled = false;
this.cdr.detectChanges();
}
protected updatePatternSummary(identifier){ this.pattern_summary.update(identifier); }
protected togglePatternSummary(identifier){ this.pattern_summary.toggleLock(identifier); }
get applicationStatusUnloaded(): ApplicationStatus { return ApplicationStatus.UNLOADED; }
get applicationStatusLoading(): ApplicationStatus { return ApplicationStatus.LOADING; }
get applicationStatusLoaded(): ApplicationStatus { return ApplicationStatus.LOADED; } }
<header class="main-option" id="truncate-model-button"
[ngClass]="{'buttonToggled': this.truncate_model_enabled, 'main-option-disabled':this.truncate_model_disabled}"
*ngIf="application_status == applicationStatusLoaded"
(click)="toggleMainOption(MainOption.TRUNCATE_MODEL)"
matTooltip="Truncate summary">
<mat-icon aria-hidden="false" aria-label="settings" fontIcon="timeline"></mat-icon>
</header>
<header class="main-option" id="search"
*ngIf="application_status == applicationStatusLoaded"
(click)="toggleMainOption(MainOption.SEARCH)"
matTooltip="Filter patterns">
<mat-icon>search</mat-icon>
</header>
<header class="main-option" id="highlight-superpatterns"
[ngClass]="{'buttonToggled': this.highlight_superpatterns_enabled, 'main-option-disabled':this.highlight_superpatterns_disabled}"
*ngIf="application_status == applicationStatusLoaded"
(click)="toggleMainOption(MainOption.HIGHLIGHT_SUPERPATTERNS)"
matTooltip="Highlight super-patterns">
<mat-icon>all_out</mat-icon>
</header>
</div>
<div id="content-cover" [hidden]="!truncate_model_enabled"></div>
<mat-sidenav-container #content class="sidenav-content" autosize [ngClass]="{'faded': truncate_model_enabled}">
<div id="settings-wrapper-lower-content">
<app-pattern-summary id="pattern-summary" #pattern_summary></app-pattern-summary>
</div>
<mat-sidenav #sidenav id="settings-wrapper-upper-content" mode="over" fixedInViewport="false">
<nav>
<button class="setting" mat-raised-button [matMenuTriggerFor]="scaleMenu">Scale</button>
<mat-menu class="setting" #scaleMenu="matMenu">
<button mat-menu-item>Linear</button>
<button mat-menu-item>Logarithmic</button>
</mat-menu>
<button class="setting" mat-raised-button [matMenuTriggerFor]="areaMenu">Area attribute</button>
<mat-menu class="setting" #areaMenu="matMenu">
<button mat-menu-item>Pattern size</button>
<button mat-menu-item>Density</button>
<button mat-menu-item>G</button>
</mat-menu>
<mat-menu class="setting" #groupMenu="matMenu">
<button mat-menu-item>Flat</button>
<button mat-menu-item>Group by fonts</button>
</mat-menu>
</nav>
</mat-sidenav>
</mat-sidenav-container>
</aside>
<div id="main-app">
<app-rss-view #rss_view id="rss_view" class="floatable"
(onTruncation)="onTruncation($event)"
(initialized)="disableRssView()"
[@slideInOut]="truncate_model_enabled ? 'in' : 'out'"
*ngIf="application_status == applicationStatusLoaded">
</app-rss-view>
<div id="visualization">
<!-- <router-outlet
(activate)="onActivate($event)"
(onTruncationFinished)="onTruncationFinished()"
>
</router-outlet> -->
<div id="select-model-warning-wrapper" *ngIf="application_status == applicationStatusUnloaded">
<div id="button-wrapper" (click)="toggleMainOption(MainOption.MODEL_SELECTOR)">
<h1> Select summary to visualize </h1>
</div>
</div>
<div id="progress-spinner" *ngIf="application_status == applicationStatusLoading">
<mat-spinner></mat-spinner>
<h1>Loading visualization...</h1>
</div>
<app-visualization #visualization_view
(onTruncation)="onTruncation($event)"
(datapoint_hover_in)="updatePatternSummary($event)"
(datapoint_hover_out)="updatePatternSummary(null)"
(datapoint_click)="togglePatternSummary($event)"
*ngIf="application_status == applicationStatusLoaded">
</app-visualization>
</div>
</div>
@use '@angular/material' as mat; @import '../../../theme.scss';
html, body{ display: flex; flex-direction: row; justify-content: center; // background-color: yellow; height: 100vh; width: 100;
overflow: hidden;
position: relative;
section{
display: flex;
flex-grow: 4;
justify-content: center;
position: relative;
#dag-controls{
right: 1em;
bottom: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1em 1em 1em 1em;
border-radius: 10px;
background-color: mat.get-color-from-palette($primary-palette, 500);
span{
color: white;
}
mat-icon{
padding-top: 0.5em;
padding-bottom: 0.5em;
padding-left: 0.5em;
padding-right: 0.5em;
border-radius: 10px;
background-color: rgb(75, 142, 230);
color: white;
}
mat-icon:hover{
cursor: pointer;
background-color: rgb(103, 157, 228);
}
}
#vizualization_div{
z-index: 0;
position: relative;
object-fit: cover;
width: 100%;
height: 100%;
// background-color: red;
object-fit:contain;
}
#intersection_details{
bottom: 5%;
right: 5%;
}
}
#datapoint_tooltip{
position: absolute;
top: 0;
left: 0;
width: 15%;
height: 15%;
}
}
.floatable { z-index: 1; position: absolute; }
.dashed-outline { border: 2px dashed #000; }
.button-disabled{ color: rgb(145, 145, 145) !important; background-color: rgb(209, 209, 209) !important; }
.button-disabled:hover{ cursor: not-allowed !important; }
import * as d3Tip from "d3-tip"; import { resolveResource } from '@tauri-apps/api/path' import { ChangeDetectorRef, Component, ComponentFactoryResolver, EventEmitter, InjectionToken, Input, OnDestroy, OnInit, Output, Renderer2, ViewContainerRef } from '@angular/core'; import { ComponentPortal, PortalModule } from '@angular/cdk/portal'; import { CommonModule } from '@angular/common'; import {MatCardModule} from '@angular/material/card'; import { ViewChild } from '@angular/core' import { ElementRef } from '@angular/core' import { AfterViewInit } from '@angular/core' import {cover, contain} from 'intrinsic-scale'; import { DataPoint } from 'src/app/models/datapoint'; import { event, fs, invoke } from '@tauri-apps/api'; import { BaseDirectory } from "@tauri-apps/api/fs"; import { SvgService } from 'src/app/services/svg/svg.service'; import { Subscription, take } from 'rxjs'; import { Color } from 'src/app/models/color'; import * as d3 from 'd3'; import { ActivatedRoute } from '@angular/router'; import { RssViewComponent } from 'src/app/components/main_options/rss-view/rss-view.component'; import { environment } from '../../../environments/environment'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { DataPointTooltipComponent } from "./datapoint-tooltip/datapoint-tooltip.component"; import { Pattern } from "src/app/models/pattern"; import { DialogService } from "src/app/services/dialog/dialog.service"; import { legendCircle } from 'src/js/circle_legend.js'; import { Legend } from 'src/js/color_legend.js'; import { IntersectionModeFeatureModule } from 'src/app/components/visualization/modules/intersection-mode-feature.module'; import { SvgFeatureModule } from "./modules/svg-feature.module"; import {MatButtonModule} from '@angular/material/button'; import { ApiService } from "src/app/services/api/api.service"; import { DagFeatureModule } from "./modules/dag-feature.module"; import { MatIconModule } from "@angular/material/icon"; import { MatTooltipModule } from "@angular/material/tooltip";
@Component({ selector: 'app-visualization', standalone: true, templateUrl: './visualization.component.html', styleUrls: ['./visualization.component.scss'], animations: [ trigger('slideInOut', [ state('void', style({ transform: 'translateX(100%)', opacity: 0 })), state('in', style({ transform: 'translateX(0)', opacity: 1 })), state('out', style({ transform: 'translateX(100%)', opacity: 0 })), transition('void => in', [ animate('0.5s ease-in-out') ]), transition('in => out', [ animate('0.5s ease-in-out') ]), transition('out => in', [ animate('0.5s ease-in-out') ]) ]) ], imports: [ CommonModule, MatCardModule, PortalModule, RssViewComponent, DataPointTooltipComponent, MatButtonModule, MatIconModule, MatTooltipModule ] })
export class VisualizationComponent implements OnInit, AfterViewInit, OnDestroy{ @Output() datapoint_hover_in = new EventEmitter(); @Output() datapoint_hover_out = new EventEmitter(); @Output() datapoint_click = new EventEmitter(); @Output() dag_change = new EventEmitter();
private datapoint_hover_in_subscription: Subscription; private datapoint_hover_out_subscription: Subscription; private datapoint_click_subscription: Subscription; private dag_change_subscription: Subscription;
@ViewChild('body') body: ElementRef; @ViewChild('vizualization_div') visualization_div: ElementRef;
private svg_feature: SvgFeatureModule; public intersection_mode_feature: IntersectionModeFeatureModule; protected dag_feature: DagFeatureModule;
constructor(private api_service: ApiService, private dialog_service: DialogService, private cdr: ChangeDetectorRef){ }
async ngOnInit() { this.intersection_mode_feature = new IntersectionModeFeatureModule(null, null, null); this.dag_feature = new DagFeatureModule(null, null, this.api_service); await this.dag_feature.init(); }
async ngAfterViewInit() { console.log("Initializing visualization component");
let svg_width = this.body.nativeElement.clientWidth;
let svg_height = this.body.nativeElement.clientHeight;
this.svg_feature = new SvgFeatureModule(this.cdr);
this.svg_feature.init(this.visualization_div, svg_width, svg_height);
let background_density = await this.api_service.getCurrentLevelBackgroundDensity();
this.svg_feature.setBackgroundColor(background_density);
let datapoints = await this.api_service.getDataPoints();
this.svg_feature.drawDataPoints(datapoints);
this.intersection_mode_feature = new IntersectionModeFeatureModule(this.svg_feature, this.dialog_service, this.api_service);
this.dag_feature = new DagFeatureModule(this.intersection_mode_feature, this.svg_feature, this.api_service);
await this.dag_feature.init();
this.datapoint_hover_in_subscription = this.svg_feature.datapoint_hover_in.subscribe(identifier => this.onDatapointHoverIn(identifier));
this.datapoint_hover_out_subscription = this.svg_feature.datapoint_hover_out.subscribe(identifier => this.onDatapointHoverOut(identifier));
this.datapoint_click_subscription = this.svg_feature.datapoint_click.subscribe(identifier => this.onDatapointClick(identifier));
this.dag_change_subscription = this.dag_feature.dag_change.subscribe(() => this.onDagChange());
// this.intersection_mode_feature.toggleIntersections(1); // TODO: Remove this line
// this.intersection_mode_feature.showIntersectionDetails(); // TODO: Remove this line
}
ngOnDestroy() { this.datapoint_hover_in_subscription.unsubscribe(); this.datapoint_hover_out_subscription.unsubscribe(); this.datapoint_click_subscription.unsubscribe(); this.dag_change_subscription.unsubscribe(); }
public async onResize(event) { console.log("Resizing window"); let width = this.body.nativeElement.clientWidth; let height = this.body.nativeElement.clientHeight; let datapoints = await this.api_service.getDataPoints();
this.svg_feature.resizeSvg(width, height, datapoints);
let background_density = await this.api_service.getCurrentLevelBackgroundDensity();
this.svg_feature.setBackgroundColor(background_density);
}
private onDatapointHoverIn(identifier: number){ this.datapoint_hover_in.emit(identifier); // To communicate with pattern summary }
private onDatapointHoverOut(identifier: number){ this.datapoint_hover_out.emit(identifier); // To communicate with pattern summary }
private async onDatapointClick(identifier: number){ this.dag_feature.setClickedDatapoint(identifier); this.dag_feature.toggleHighlightSuperpatterns(false); this.datapoint_click.emit(identifier); // To communicate with pattern summary
await this.intersection_mode_feature.toggleIntersections(identifier);
}
public async onTruncation(event){ let new_size = event - 1; // -1 because the first point is the null model rss let truncated_datapoints = await this.api_service.truncateModel(new_size);
this.svg_feature.deactivateHighlight();
this.dag_feature.setClickedDatapoint(null);
await this.intersection_mode_feature.toggleIntersections(null);
this.svg_feature.drawDataPoints(truncated_datapoints);
this.datapoint_click.emit(null);
}
private onDagChange(){ this.dag_change.emit(); }
public toggleHighlightSuperpatterns(toggle: boolean){ if(toggle == true && this.dag_feature.isHighlightingSuperpatterns()){ return; } if(toggle == false && !this.dag_feature.isHighlightingSuperpatterns()){ return; }
this.svg_feature.deactivateHighlight();
this.intersection_mode_feature.toggleIntersections(null).then(() => {
this.dag_feature.setClickedDatapoint(null);
this.dag_feature.toggleHighlightSuperpatterns(toggle);
// this.datapoint_click.emit(null); // To communicate with pattern summary
});
}
public openSearch(){ this.svg_feature.deactivateHighlight(); this.intersection_mode_feature.toggleIntersections(null).then(() => { this.dag_feature.setClickedDatapoint(null); this.dag_feature.toggleHighlightSuperpatterns(false); this.datapoint_click.emit(null); // To communicate with pattern summary }); }
public async ascendDag(){ let success = await this.dag_feature.ascendDag(); if(success){ let datapoints = await this.api_service.getDataPoints();
this.svg_feature.drawDataPoints(datapoints, true);
let background_density = await this.api_service.getCurrentLevelBackgroundDensity();
this.svg_feature.setBackgroundColor(background_density);
this.datapoint_click.emit(null);
}
}
public async descendDag(){ let success = await this.dag_feature.descendDag(); if(success){ let datapoints = await this.api_service.getDataPoints();
this.svg_feature.drawDataPoints(datapoints, true);
let background_density = await this.api_service.getCurrentLevelBackgroundDensity();
this.svg_feature.setBackgroundColor(background_density);
this.datapoint_click.emit(null);
}
}
public async filterDatapoints(filters: string[][]){ console.log("Filtering datapoints with filters: ", filters); let filtered_datapoints: DataPoint[] = await this.api_service.filterDatapoints(filters); this.svg_feature.drawDataPoints(filtered_datapoints, false); }
public isOnFirstLevel(){ return this.dag_feature.current_dag_level == 1; }
public getNbOfDatapoints(){ return this.svg_feature.getDrawnDataPoints().length; } }
<mat-icon id="arrow-down" [class.button-disabled]="!dag_feature.lower_dag_arrow_active" fontIcon="keyboard_arrow_down" matTooltip="Descend on DAG"
(click)="descendDag()"></mat-icon>
</div>
<div #vizualization_div id="vizualization_div"></div>
</section>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VisualizationComponent } from './visualization.component';
describe('VisualizationComponent', () => { let component: VisualizationComponent; let fixture: ComponentFixture;
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ VisualizationComponent ] }) .compileComponents();
fixture = TestBed.createComponent(VisualizationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => { expect(component).toBeTruthy(); }); });
import { ChangeDetectorRef, ElementRef, EventEmitter, NgModule, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DataPoint } from 'src/app/models/datapoint'; import { SvgFeatureModule } from './svg-feature.module'; import { ApiService } from 'src/app/services/api/api.service'; import { IntersectionModeFeatureModule } from './intersection-mode-feature.module';
@NgModule({ declarations: [], imports: [ CommonModule ] }) export class DagFeatureModule{ public dag_change = new EventEmitter();
public upper_dag_arrow_active: boolean = false;
public lower_dag_arrow_active: boolean = false;
protected supers_highlighted: boolean = false;
private datapoints_with_subpatterns: Set<number>;
public current_dag_level: number;
private clicked_datapoint: number;
private svg_feature: SvgFeatureModule;
private intersection_feature: IntersectionModeFeatureModule;
private api_service: ApiService
constructor(intersection_feature: IntersectionModeFeatureModule, svg_feature: SvgFeatureModule, api_service: ApiService) {
this.intersection_feature = intersection_feature;
this.svg_feature = svg_feature;
this.api_service = api_service;
}
public async init() {
this.datapoints_with_subpatterns = new Set(
(await this.api_service.getDatapointsWithSubPatterns()).map(datapoint => datapoint.identifier));
this.current_dag_level = 1;
}
public setClickedDatapoint(identifier: number){
if(identifier == this.clicked_datapoint){ // Clicked on the same pattern
this.clicked_datapoint = undefined;
this.lower_dag_arrow_active = false;
return;
}
this.clicked_datapoint = identifier;
this.lower_dag_arrow_active = this.datapoints_with_subpatterns.has(identifier)? true : false;
}
public toggleHighlightSuperpatterns(toggle: boolean){
console.log("Toggling superpatterns");
this.supers_highlighted = toggle;
if(toggle){
let gray_shade = 196;
let gray = `rgba(${gray_shade}, ${gray_shade}, ${gray_shade}, 0.5)`;
this.svg_feature.plot.selectAll('.datapoint')
.filter(d => !this.datapoints_with_subpatterns.has(d.identifier))
.raise()
.transition('mouseover')
.duration(300)
.attr('fill', d => gray)
.style('stroke', d=> gray);
}else {
this.svg_feature.drawDataPoints(this.svg_feature.getDrawnDataPoints(), true);
}
}
// public activateHighlightSuperpatterns(){
// this.toggleHighlightSuperpatterns(true);
// }
// public deactivateHighlightSuperpatterns(){
// this.toggleHighlightSuperpatterns(false);
// }
public isHighlightingSuperpatterns(){
return this.supers_highlighted;
}
private drawNewLevelDatapoints(datapoints: Array<DataPoint>){
this.intersection_feature.toggleIntersections(null, true);
this.svg_feature.deactivateHighlight();
this.svg_feature.drawDataPoints(datapoints, true);
}
public async ascendDag(): Promise<boolean>{
if(this.current_dag_level == 1){ return false; }
let datapoints = await this.api_service.ascendDag();
if(datapoints.length == 0){ return false; }
console.log("Ascending");
console.log("New level datapoints:");
console.log(datapoints);
this.drawNewLevelDatapoints(datapoints);
this.current_dag_level -= 1;
if(this.current_dag_level == 1){ this.upper_dag_arrow_active = false; }
else{ this.upper_dag_arrow_active = true; }
this.lower_dag_arrow_active = false;
this.clicked_datapoint = undefined;
console.log("Current level: ", this.current_dag_level);
this.dag_change.emit();
return true;
}
public async descendDag(): Promise<boolean>{
let super_datapoint = this.clicked_datapoint;
console.log("Descending from: ", super_datapoint)
if(super_datapoint == null){ return false; }
let datapoints = await this.api_service.descendDag(super_datapoint);
console.log("New level datapoints:");
console.log(datapoints);
if(datapoints.length == 0){ return false; }
this.drawNewLevelDatapoints(datapoints);
this.current_dag_level += 1;
this.upper_dag_arrow_active = true;
this.lower_dag_arrow_active = false;
this.clicked_datapoint = undefined;
console.log("Current level: ", this.current_dag_level);
this.dag_change.emit();
return true;
}
}
import * as d3Tip from "d3-tip"; import { ChangeDetectorRef, ElementRef, EventEmitter, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import * as d3 from 'd3'; import { DataPoint } from 'src/app/models/datapoint'; import { legendCircle } from 'src/js/circle_legend.js'; import { Legend } from 'src/js/color_legend.js';
@NgModule({ declarations: [], imports: [ CommonModule ] }) export class SvgFeatureModule { public datapoint_hover_in = new EventEmitter(); public datapoint_hover_out = new EventEmitter(); public datapoint_click = new EventEmitter();
private locked_datapoint: DataPoint; private datapoints: Array; private datapoints_mapping: Map<number, DataPoint>;
private visualization_div: ElementRef; public plot: any; public svg: any;
private zoom_level: number; private initial_scale: number = 1.4; private number_of_gridlines: number = 40; private y_correction = 0;
private svg_width: number; private svg_height: number; private x_scale: any; private y_scale: any;
private tooltip; private transition_duration = 300;
private cdr: ChangeDetectorRef;
constructor(cdr: ChangeDetectorRef){ this.cdr = cdr; }
public init(visualization_div: ElementRef, svg_width: number, svg_height: number){ this.visualization_div = visualization_div; this.svg_width = svg_width; this.svg_height = svg_height;
this.tooltip = d3Tip.default()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "\
<div style='background-color:#ededed; padding: 0.5em 0.5em 0.5em 0.5em; border-radius: 10px; border: 1px dashed black;'>\
<strong>ID:</strong> <span style='color:#BC2602'>" + d.identifier + "</span><br>\
<strong>Size:</strong> <span style='color:#BC2602'>" + d.pattern_size + "</span><br>\
<strong>Density:</strong> <span style='color:#BC2602'>" + Math.round(d.density * 100) / 100 + "</span>\
</div>\
";
});
this.svg = this.createSvg();
this.resizeSvg(this.svg_width, this.svg_height, this.datapoints);
this.cdr.detectChanges();
this.zoom_level = this.initial_scale;
}
public createSvg(){ let svg = d3.select(this.visualization_div.nativeElement) .append('svg') .attr('width', this.svg_width) .attr('height',this.svg_height) .on('dblclick', () => { });
return svg;
}
public resizeSvg(width: number, height: number, datapoints: Array){ this.svg .attr('width', width) .attr('height', height);
let x_scale = d3.scaleLinear()
.domain([-1, 1])
.range([0, (height - this.y_correction)/1]);
let y_scale = d3.scaleLinear()
.domain([-1, 1])
.range([(height - this.y_correction)/1, 0]);
this.x_scale = x_scale;
this.y_scale = y_scale;
this.svg_width = width;
this.svg_height = height;
this.createPlot();
this.drawDataPoints(datapoints);
}
private createPlot(){ if(this.plot != undefined){ this.svg.select("#plot").remove(); } this.plot = this.svg.append("g") .attr("id", "plot") .on('dblclick', () => { });
let panning_zoom = d3.zoom()
.scaleExtent([1.4, 10]) // This control how much you can unzoom (x1) and zoom (x10)
.translateExtent([[0, 0], [this.svg_height, this.svg_height]])
.on("start", (event, d) => { this.svg.attr("cursor", "grabbing"); })
.on("zoom", (event) => {
this.plot.attr("transform", event.transform);
if(event.sourceEvent instanceof WheelEvent){
this.zoom_level = event.transform.k;
this.drawCircleLegend();
}
})
.on("end", (event, d) => {this.svg.attr("cursor", "default")});
this.svg.call(panning_zoom);
// Apply initial zoom level
let x_translation_factor = 0.0;
let y_translation_factor = 0.2;
let initial_transform = d3.zoomIdentity
.translate(-this.svg_width*(x_translation_factor), -this.svg_height*(y_translation_factor))
.scale(this.initial_scale);
this.svg.call(panning_zoom.transform, initial_transform);
this.drawGridLines();
this.drawUnselectionRect();
}
private drawGridLines() {
let makeXGridlines = () => { return d3.axisBottom(this.x_scale).ticks(this.number_of_gridlines) }
let makeYGridlines = () => { return d3.axisLeft(this.y_scale).ticks(this.number_of_gridlines) }
let grey_tonality = 220;
let color = rgb(${grey_tonality}, ${grey_tonality}, ${grey_tonality})
;
this.plot.append("g") // Add the X gridlines
.attr("class", "grid")
.attr("transform", "translate(0," + this.svg_height + ")")
.attr("color", color)
.call(makeXGridlines()
.tickSize(-this.svg_height)
.tickFormat(() => "")
)
this.plot.append("g") // Add the Y gridlines
.attr("class", "grid")
.attr("color", color)
.call(makeYGridlines()
.tickSize(-1 * this.svg_width)
// .tickSize(-300)
.tickFormat(() => "")
)
}
private drawUnselectionRect(){ this.plot.append('rect') .attr('id', 'overlay') .attr('x', 0) .attr('y', 0) .attr('width', this.svg_width) .attr('height', this.svg_height) .style('fill', 'rgba(255, 0, 0, 1)') .lower() .on('click', (event, d) => { this.locked_datapoint = undefined; this.toggleHighlight(undefined); this.datapoint_click.emit(null) }); }
private drawCircleLegend(){ let min_pattern_size = Math.min(...this.datapoints.map(datapoint => Math.abs(datapoint.pattern_size))); let max_pattern_size = Math.max(...this.datapoints.map(datapoint => Math.abs(datapoint.pattern_size))); let mean_pattern_size = 0; for(let i = 0; i < this.datapoints.length; i++){ mean_pattern_size += this.datapoints[i].pattern_size; } mean_pattern_size /= this.datapoints.length; mean_pattern_size = Math.round(mean_pattern_size);
let min_size = Math.min(...this.datapoints.map(datapoint => Math.abs(datapoint.size))) * this.zoom_level;
let max_size = Math.max(...this.datapoints.map(datapoint => Math.abs(datapoint.size))) * this.zoom_level;
let legend = legendCircle(null)
.scale(
d3.scaleLinear()
.domain([min_pattern_size, max_pattern_size])
.range([min_size, max_size])
)
.tickValues([min_pattern_size, mean_pattern_size, max_pattern_size])
.tickFormat((d, i, e) => `${d}${i === e.length - 1 ? " Cells" : ""}`)
.tickSize(max_size); // defaults to 5
const legend_x_padding = 10;
const legend_y_padding = 10;
this.svg.select("#circle_legend").remove();
this.svg.append("g")
.attr('id', 'circle_legend')
.attr('transform', `translate(${legend_x_padding}, ${legend_y_padding})`)
.call(legend);
}
private drawColorLegend(){ let oldLegend = document.getElementById("color_legend"); if(oldLegend){ oldLegend.parentNode.removeChild(oldLegend); }
let svg_width = this.svg.attr('width');
let legend_width = 320;
const legend_x_padding = 10;
let legend_x = svg_width - (legend_width + legend_x_padding);
let legend = Legend(d3.scaleLinear([0, 1], ["rgba(255,255,255,1)", "rgba(255,0,0,1)"]), {
title: "Density",
width: legend_width,
})
let legendGroup = this.svg.append('g')
.attr('id', 'color_legend')
.attr("transform", `translate(${legend_x}, 0)`);
legendGroup.node().appendChild(legend);
}
private scalingFunction(datapoints: Array) { // let x_max_module = Math.max(...datapoints.map(datapoint => Math.abs(datapoint.x))); // let y_max_module = Math.max(...datapoints.map(datapoint => Math.abs(datapoint.y))); // let max_module = Math.max(x_max_module, y_max_module);
let scaled_datapoints: Array<DataPoint> = [...datapoints];
let screen_coverage = 0.5;
// let screen_coverage = 0.8;
scaled_datapoints.forEach(datapoint => {
// let result_x = datapoint.x / x_max_module;
// let result_y = datapoint.y / y_max_module;
// if (isNaN(result_x) || !isFinite(result_x)) { result_x = datapoint.x; }
// if (isNaN(result_y) || !isFinite(result_y)) { result_y = datapoint.y; }
datapoint.x = datapoint.x / ((1-screen_coverage) + 1);
datapoint.y = datapoint.y / ((1-screen_coverage) + 1);
});
return scaled_datapoints;
}
private toggleHighlight(datapoint: DataPoint){ if(this.locked_datapoint){ return; }
let highlight_circle = this.plot.selectAll('.highlight');
if(highlight_circle){ highlight_circle.remove(); }
if(datapoint){ // Add a EMPTY circle with id highlight, the circle should not block mouse hover and click events
// Draw a new blue circle on the coordinates of datapoint
let highlight_radius = datapoint.size * 1.6;
let highlight_color = 'rgba(114, 232, 247)';
let highlight_opacity = 0.5;
let stroke_width = highlight_radius/3;
this.plot.append('circle')
.attr('class', 'highlight')
.attr('cx', this.x_scale(datapoint.x))
.attr('cy', this.y_scale(datapoint.y))
.attr('r', highlight_radius)
.attr('fill', 'none')
.attr('stroke', highlight_color)
.attr('stroke-width', stroke_width)
.attr('opacity', highlight_opacity)
.style('pointer-events', 'none');
this.plot.append('circle')
.attr('class', 'highlight')
.attr('cx', this.x_scale(datapoint.x))
.attr('cy', this.y_scale(datapoint.y))
.attr('r', highlight_radius*1.4)
.attr('fill', 'none')
.attr('stroke', highlight_color)
.attr('stroke-width', stroke_width/2)
.attr('opacity', highlight_opacity)
.style('pointer-events', 'none');
}
}
public deactivateHighlight(){ this.locked_datapoint = undefined; this.toggleHighlight(undefined); }
public drawDataPoints(datapoints: Array, force_redraw: boolean = false) { if(datapoints == undefined || datapoints == null){ return; } if(this.plot == undefined){ return; }
console.log("Drawing " + datapoints.length + " datapoints");
console.log(datapoints);
let transition_duration = this.transition_duration;
if(force_redraw){
this.plot.selectAll('.datapoint').remove();
transition_duration = 0;
}
this.datapoints = datapoints;
this.datapoints_mapping = new Map<number, DataPoint>();
this.datapoints.forEach(datapoint => this.datapoints_mapping.set(datapoint.identifier, datapoint));
this.plot.call(this.tooltip);
let scaled_datapoints = this.datapoints;
if(!force_redraw){ scaled_datapoints = this.scalingFunction(datapoints); }
const circles = this.plot.selectAll('.datapoint')
.data(scaled_datapoints, d => d.identifier);
circles.exit()
.transition()
.duration(transition_duration)
.attr('r', 0)
.remove();
circles.transition()
.duration(transition_duration)
.attr('cx', d => {
const result = this.x_scale(parseFloat(d.x));
return result;
})
.attr('cy', d => this.y_scale(parseFloat(d.y)));
circles.enter().append('circle') // Add new datapoints with animation
.attr('cx', d => {
const result = this.x_scale(parseFloat(d.x));
return result;
})
.attr('class', 'datapoint')
.attr('cy', d => this.y_scale(parseFloat(d.y)))
.attr('r', 0)
.attr('fill', d => `rgba(${d.r}, ${d.g}, ${d.b}, ${d.a})`)
.style('cursor', 'pointer')
.style('stroke', 'rgba(255, 0, 0, 1')
.on('mouseover', (event, d) => {
this.toggleHighlight(d);
this.tooltip.show(d, event.currentTarget);
this.datapoint_hover_in.emit(d.identifier);
})
.on('mouseout', (event, d) => {
this.toggleHighlight(undefined);
this.tooltip.hide(d, event.currentTarget);
this.datapoint_hover_out.emit(d.identifier);
})
.on('click', (event, d) => {
if((this.locked_datapoint) && (this.locked_datapoint.identifier == d.identifier)){
// Unhighlight the locked datapoint
this.locked_datapoint = undefined;
this.toggleHighlight(undefined);
}else{
// Highlight the clicked datapoint and lock
this.locked_datapoint = undefined;
this.toggleHighlight(d);
this.locked_datapoint = d;
}
this.datapoint_click.emit(d.identifier);
})
.transition()
.duration(transition_duration)
.attr('r', d => d.size);
this.drawCircleLegend();
this.drawColorLegend();
}
public setBackgroundColor(density: number) {
let color = rgba(255, 0, 0, ${density})
;
this.plot.select("#overlay")
.transition()
.duration(300)
.style('fill', color);
}
public showTooltip(datapoint: DataPoint, circle: any){ this.tooltip.show(datapoint, circle); }
public hideTooltip(datapoint: DataPoint, circle: any){ this.tooltip.hide(datapoint, circle); }
public xScale(x: number){ return this.x_scale(x); }
public yScale(y: number){ return this.y_scale(y); }
public getDrawnDataPoints(){ return this.datapoints; }
public getSvgWidth(){ return this.svg_width; }
public getSvgHeight(){ return this.svg_height; }
public getDatapoint(identifier: number){ return this.datapoints_mapping.get(identifier); } }
import { EventEmitter, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DataPoint } from 'src/app/models/datapoint'; import { SvgFeatureModule } from './svg-feature.module'; import * as d3 from 'd3'; import { environment } from 'src/environments/environment'; import { fs, invoke } from '@tauri-apps/api'; import { DialogService } from 'src/app/services/dialog/dialog.service'; import { resolveResource } from '@tauri-apps/api/path'; import { IntersectionDetailsDialogComponent } from '../intersection-details-dialog/intersection-details-dialog.component'; import { IntersectionDetails } from 'src/app/models/intersection_details'; import { ApiService } from 'src/app/services/api/api.service';
@NgModule({ declarations: [], imports: [ CommonModule ] }) export class IntersectionModeFeatureModule { private old_clicked_datapoint = null; private clicked_datapoint_data: DataPoint = null; private intersection_details: IntersectionDetails; private transition_duration: number = 300;
private svg_feature: SvgFeatureModule; private dialog_service: DialogService; private api_service: ApiService
constructor(svg_feature: SvgFeatureModule, dialog_service: DialogService, api_service: ApiService) { this.svg_feature = svg_feature; this.dialog_service = dialog_service; this.api_service = api_service; }
private connectDatapoints(center: DataPoint, intersections:Map<number, number>){ let svg_circles = this.svg_feature.plot.selectAll('.datapoint'); let id_to_datapoint = new Map<number, DataPoint>(svg_circles.data() .map(d => [d.identifier, d]));
for(let [identifier, percentage] of intersections.entries()){
if(identifier == this.clicked_datapoint_data.identifier){ continue; } // itself
if(identifier == 0){ continue; } // Excess intersections
let related_datapoint = id_to_datapoint.get(identifier) || null;
if (related_datapoint == null) { continue; } // Related circle is a subpattern
let related_circle = svg_circles.filter(d => d.identifier == identifier);
let greatest_density_color = `rgba(${center.r}, ${center.g}, ${center.b}, ${center.a})`;
if(related_datapoint.density > center.density){
greatest_density_color = `rgba(${related_datapoint.r}, ${related_datapoint.g}, ${related_datapoint.b}, ${related_datapoint.a})`;
}
let stroke_width = 6 * percentage + 2; // 2 to 8
let x1 = this.svg_feature.xScale(center.x);
let y1 = this.svg_feature.yScale(center.y);
let line = this.svg_feature.plot.append('line')
.datum({identifier:identifier, x1: x1, y1: y1}) // Bind the original coordinates to the line
.raise()
.attr('class', 'intersection_line')
.attr('x1', this.svg_feature.xScale(center.x)) // Start position (x) of the line
.attr('y1', this.svg_feature.yScale(center.y)) // Start position (y) of the line
.attr('x2', this.svg_feature.xScale(center.x)) // Initially, end position (x) is the same as start position
.attr('y2', this.svg_feature.yScale(center.y)) // Initially, end position (y) is the same as start position
.attr('stroke', greatest_density_color)
.attr('stroke-width', stroke_width)
.on('mouseover', (event, l) => {
d3.select(event.currentTarget).style('cursor', 'pointer');
d3.select(event.currentTarget).attr('stroke-width', stroke_width * 3);
})
.on('mouseout', (event, l) => {
d3.select(event.currentTarget).style('cursor', 'default');
d3.select(event.currentTarget).attr('stroke-width', stroke_width);
})
.on('click', (event, l) => {
this.showIntersectionDetails(l.identifier);
})
.transition('mouseover')
.duration(this.transition_duration*2)
.attr('x2', this.svg_feature.xScale(related_datapoint.x)) // Actual end position (x) of the line
.attr('y2', this.svg_feature.yScale(related_datapoint.y)) // Actual end position (y) of the line
}
}
private highlightDatapoints(relationed_identifiers: Array){
let identifiers_set = new Set(relationed_identifiers);
let gray_shade = 196;
let gray = rgba(${gray_shade}, ${gray_shade}, ${gray_shade}, 0.5)
;
this.svg_feature.plot.selectAll('.datapoint')
.filter(d => !identifiers_set.has(d.identifier) && d.identifier != this.clicked_datapoint_data.identifier)
.raise()
.transition('mouseover')
.duration(this.transition_duration)
.attr('fill', d => gray)
.style('stroke', d=> gray);
}
private createIntersectionChart(root_circle: any, intersections: Map<number, number>, original_radius: number, chart_radius: number){ let root_datapoint = root_circle.node().data; let pie = d3.pie() .value((d: any) => d.value);
let data: Array<any> = Array.from(intersections, ([key, value]) => ({key, value}));
let pie_data = pie(data);
let original_arc = d3.arc()
.innerRadius(0)
.outerRadius(d => original_radius);
let pie_chart_arc = d3.arc()
.innerRadius(0)
.outerRadius(chart_radius);
let pie_group = this.svg_feature.plot.append('g')
.attr('class', 'pie_chart')
.attr('transform', `translate(${root_circle.attr('cx')}, ${root_circle.attr('cy')})`);
pie_group.selectAll('path')
.data(pie_data)
.enter()
.append('path')
.attr('pointer-events', 'none')
.attr('d', original_arc)
.attr('fill', 'red')
.transition('mouseover')
.duration(this.transition_duration)
.attr('d', pie_chart_arc)
.attr('fill', (d: any) => {
let related_datapoint = this.svg_feature.getDatapoint(d.data.key);
let r = 2;
let g = 178;
let b = 227;
let a = 1;
if(related_datapoint){ // If it isnt id 0 (which means total intersection to the clicked datapoint)
// Dont color the percetage related to intersection with itself
if(related_datapoint.identifier == root_datapoint.identifier){ a = 0; }
}
let color = `rgba(${r}, ${g}, ${b}, ${a})`;
return color;
});
}
private createIntersectionCharts(identifiers: Array, intersections: Map<number, number>){ let clicked_datapoint = this.svg_feature.plot.selectAll('.datapoint') .filter(d => d.identifier == this.clicked_datapoint_data.identifier);
let intersection_data: Map<number, number> = new Map<number, number>();
let parent_current_percentage = intersections.get(this.clicked_datapoint_data.identifier);
let complement_percentage = 1 - parent_current_percentage; // Colored with current circle color
intersection_data.set(0, parent_current_percentage);
intersection_data.set(this.clicked_datapoint_data.identifier, complement_percentage);
let original_radius = this.clicked_datapoint_data.size;
let chart_radius = this.clicked_datapoint_data.size;
this.createIntersectionChart(clicked_datapoint, intersection_data, original_radius, chart_radius);
intersections.delete(this.clicked_datapoint_data.identifier);
let identifiers_set = new Set(identifiers);
let circles = this.svg_feature.plot.selectAll('.datapoint')
.filter(d => identifiers_set.has(d.identifier));
circles.each((d, i, nodes) => {
intersection_data = new Map<number, number>();
let parent_current_percentage = intersections.get(d.identifier); // Colored with the parent color
let complement_percentage = 1 - parent_current_percentage; // Colored with current circle color
intersection_data.set(this.clicked_datapoint_data.identifier, parent_current_percentage);
intersection_data.set(d.identifier, complement_percentage);
original_radius = d.size;
chart_radius = d.size;
this.createIntersectionChart(d3.select(nodes[i]), intersection_data, original_radius, chart_radius);
});
}
private async showIntersections(){ if(this.clicked_datapoint_data == null){ return };
// let intersections = await this.api_service.getIntersectionsPercentages(this.clicked_datapoint_data.identifier);
let intersections_details = await this.api_service.getIntersectionDetails(this.clicked_datapoint_data.identifier);
console.log(intersections_details);
let intersections: Map<number, number> = new Map();
intersections_details.intersections.forEach((value, key) => {
intersections.set(key, value[0]);
});
intersections.set(intersections_details.identifier, intersections_details.total_intersection_percentage);
console.log(intersections);
let relationed_datapoints: Array<number> = Array.from(intersections.keys())
.filter(d => (d != this.clicked_datapoint_data.identifier) && (d != 0));
this.highlightDatapoints(relationed_datapoints);
this.connectDatapoints(this.clicked_datapoint_data, intersections);
let expansion_factor = 1;
// this.expandCircle(clicked_circle, expansion_factor, intersections, intersections_colors);
this.createIntersectionCharts(relationed_datapoints, intersections);
}
private async hideIntersections(no_transition: boolean = false){ let transition_duration = no_transition? 0 : this.transition_duration; let intersection_lines = this.svg_feature.svg.selectAll('.intersection_line'); await intersection_lines .transition('mouseout') .duration(transition_duration) .attr('x2', d => d.x1) // End position (x) becomes the start position .attr('y2', d => d.y1) // End position (y) becomes the start position .remove();
if(this.clicked_datapoint_data != null){
let circle_arc = d3.arc()
.innerRadius(0)
.outerRadius(d => this.clicked_datapoint_data.size);
let pie_chart = this.svg_feature.svg.selectAll('.pie_chart');
await pie_chart.selectAll('path')
.transition('mouseout')
.duration(transition_duration)
.attr('d', d=> d.size)
.remove(); // Remove the paths after the transition
this.svg_feature.drawDataPoints(this.svg_feature.getDrawnDataPoints(), true);
}
}
public async toggleIntersections(identifier: number, no_transition: boolean = false){ await this.hideIntersections(no_transition); await this.updateClickedDatapoint(identifier);
if(identifier == null || identifier==undefined){return;}
if((this.old_clicked_datapoint != null) && (identifier == this.old_clicked_datapoint.identifier)){ // Datapoint was clicked again
await this.updateClickedDatapoint(null);
}
await this.showIntersections();
}
private async updateClickedDatapoint(identifier: number) { this.old_clicked_datapoint = this.clicked_datapoint_data;
if(identifier == null){
this.clicked_datapoint_data = null;
this.intersection_details = null;
return;
}
let clicked_circle = this.svg_feature.plot.selectAll('.datapoint')
// .filter(d => d.identifier == 13); // Fix black color
.filter(d => d.identifier == identifier);
this.clicked_datapoint_data = clicked_circle.node().__data__;
this.intersection_details = await this.api_service.getIntersectionDetails(this.clicked_datapoint_data.identifier);
}
public clickedPatternHasIntersections(): boolean { if(this.intersection_details == null){ return false; }
return this.intersection_details.intersections.size > 0;
}
public getClickedDataPoint(): DataPoint { return this.clicked_datapoint_data; }
private async showIntersectionDetails(intersector_id: number){ if(this.clicked_datapoint_data == null){ console.warn("No clicked datapoint to show details."); return; }
let identifier = this.clicked_datapoint_data.identifier;
console.log("Clicked intersection (", identifier, ", ", intersector_id, ")");
let intersection_details = await this.api_service.getIntersectionDetails(identifier);
console.log("Fetched intersection details for identifier: ", identifier);
console.log(intersection_details);
let dialog_data = {
intersector: intersector_id,
intersection_details: intersection_details
}
this.dialog_service.open(IntersectionDetailsDialogComponent,
IntersectionDetailsDialogComponent.WIDTH,
IntersectionDetailsDialogComponent.HEIGHT,
dialog_data);
} }
src/app/components/visualization/intersection-details-dialog/intersection-details-dialog.component.spec.ts BEGIN
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IntersectionDetailsDialogComponent } from './intersection-details-dialog.component';
describe('IntersectionDetailsDialogComponent', () => { let component: IntersectionDetailsDialogComponent; let fixture: ComponentFixture;
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ IntersectionDetailsDialogComponent ] }) .compileComponents();
fixture = TestBed.createComponent(IntersectionDetailsDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => { expect(component).toBeTruthy(); }); });
src/app/components/visualization/intersection-details-dialog/intersection-details-dialog.component.spec.ts END
src/app/components/visualization/intersection-details-dialog/intersection-details-dialog.component.scss BEGIN
@use '@angular/material' as mat; @import '../../../../theme.scss';
body{ margin: 0 0 0 0; padding: 0 0 0 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
overflow-x: hidden;
overflow-y: scroll;
// background-color: green;
header{
width: 100%;
height: fit-content;
padding: 1em 1em 1em 4em;
// background-color: blue;
}
section{
width: 95%;
height: 90%;
padding: 1em 1em 1em 2em;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
// background-color: yellow;
// overflow: hidden;
#pattern-dropdown-wrapper{
}
h2{
font-weight: normal;
}
#intersector_table_wrapper{
width: 100%;
overflow: auto;
// background-color: blue;
table{
max-width: 90%;
// border-left: 1px solid #ccc;
// border-right: 1px solid #ccc;
.mat-column-dim_number{
text-align: center;
width: 5em;
}
.mat-column-dim_values_preview{
text-align: center;
width: 100%;
// background-color: blue;
}
.mat-column-expand{
// background-color: brown;
width: 3em;
}
th{ // Table header
user-select: none;
cursor: default;
}
tr.data-row{
// Selector for the rows
}
tr.data-row:not(.expanded-row):hover {
background: whitesmoke;
}
tr.data-row:not(.expanded-row):active {
background: #efefef;
}
.data-row td {
border-bottom-width: 0;
}
.mat-column-expand{
button{
// width: 2em;
mat-icon{
// font-size: 1em;
}
}
}
tr.detail-row {
height: 0;
}
.detail {
display: flex;
// background-color: red;
width: 100%;
word-break: normal;
}
.detail-value {
padding: 16px;
}
.detail-value-attribution {
opacity: 0.5;
}
}
}
}
}
.expanded-row{
}
.selected-row{ background: whitesmoke; }
src/app/components/visualization/intersection-details-dialog/intersection-details-dialog.component.scss END
src/app/components/visualization/intersection-details-dialog/intersection-details-dialog.component.ts BEGIN
import { ChangeDetectorRef, Component, Inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { IntersectionDetails } from 'src/app/models/intersection_details'; import {MatSort, MatSortModule} from '@angular/material/sort'; import {MatTableDataSource, MatTableModule} from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import {MatIconModule} from '@angular/material/icon'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select';
export interface IntersectedTuple { dim_number: String; dim_values_preview: Array; dim_values: Array;
needs_expand: boolean; }
@Component({ selector: 'app-intersection-details-dialog', standalone: true, imports: [ CommonModule, MatIconModule, MatTabsModule, MatSortModule, MatTableModule, MatFormFieldModule, MatSelectModule ], templateUrl: './intersection-details-dialog.component.html', styleUrls: ['./intersection-details-dialog.component.scss'], animations: [ trigger('detailExpand', [ state('collapsed,void', style({height: '0px', minHeight: '0'})), state('expanded', style({height: '*'})), transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), ]), ], }) export class IntersectionDetailsDialogComponent { public static WIDTH = '40vw'; public static HEIGHT = '80vh';
protected identifier: number; protected total_untouched_percentage: number; protected total_intersection_percentage: number; protected intersections: Map<number, [number, Array<Array>]>;
protected intersector_id: number;
protected intersector_data_source: IntersectedTuple[]; protected intersector_displayed_columns = ['dim_number', 'dim_values_preview']; protected intersector_displayed_columns_names: Map<String, String> = new Map([ ['dim_number', 'Dim'], ['dim_values_preview', 'Dim preview'] ]); protected intersector_displayed_columns_with_expand = [...this.intersector_displayed_columns, 'expand']; protected expanded_element: IntersectedTuple | null; private max_dim_values_preview_length = 44;
constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: {intersector: number, intersection_details: IntersectionDetails}, private cdr: ChangeDetectorRef){
this.identifier = data.intersection_details.identifier;
this.total_untouched_percentage = data.intersection_details.total_untouched_percentage;
this.total_intersection_percentage = data.intersection_details.total_intersection_percentage;
this.intersector_id = data.intersector;
let sorted_intersections: Map<number, [number, Array<Array<string>>]> = new Map([...data.intersection_details.intersections.entries()]
.sort((a, b) => {
return a[1][0] - b[1][0];
}));
this.intersections = sorted_intersections;
// let data_source: Array<Array<number>> = Array.from(this.intersections.keys(), key => [key])
// this.intersectors_data_source = new MatTableDataSource(data_source);
}
ngOnInit(): void { console.log("Initializing intersection details dialog"); }
ngAfterViewInit(){ this.changeIntersector(); this.cdr.detectChanges(); }
protected trackColumn(index: number, column: string): any { return column; }
protected getColumnName(column: String): String { return this.intersector_displayed_columns_names.get(column); }
protected changeIntersector(){ let intersected_dims: Array<Array> = this.intersections.get(this.intersector_id)[1];
let i = 0;
let intersector_data_source: IntersectedTuple[] = [];
intersected_dims.forEach(dim => {
let values: Array<String> = [];
let all_values_length = 0;
dim.flat().forEach(value => {
all_values_length += value.length;
all_values_length += 1; // For the comma
all_values_length += 1; // For the space
values.push(" " + value);
});
let needs_expand: boolean;
let dim_values_preview: Array<String> = [];
if(all_values_length <= this.max_dim_values_preview_length - 2){ // -2 for the last comma and space
dim_values_preview = values;
needs_expand = false;
}else{
dim_values_preview.push("{" + values.length + " elements...}");
needs_expand = true;
}
intersector_data_source.push(
{dim_number: 'DIM' + (i+1), dim_values_preview: dim_values_preview, dim_values: values, needs_expand: needs_expand}
);
i++;
});
this.intersector_data_source = intersector_data_source;
}
protected expandRow(element: IntersectedTuple): void { if(element.needs_expand === false){ this.expanded_element = null; return; }
this.expanded_element = this.expanded_element === element ? null : element;
}
}
src/app/components/visualization/intersection-details-dialog/intersection-details-dialog.component.ts END
src/app/components/visualization/intersection-details-dialog/intersection-details-dialog.component.html BEGIN
Total un-intersected percentage of {{identifier}}: {{total_untouched_percentage*100 | number:'1.2-2'}}%
<section>
<h2>Intersection subtensor preview:</h2>
<div id="intersector_table_wrapper">
<!-- Declares a table. The multiTemplateDataRows attribute allows multiple <ng-container> elements per row -->
<table mat-table [dataSource]="intersector_data_source" multiTemplateDataRows>
<!-- Creates a column for each item in the array. -->
<ng-container *ngFor="let column of intersector_displayed_columns; trackBy: trackColumn" matColumnDef="{{column}}" sticky>
<th mat-header-cell *matHeaderCellDef> {{getColumnName(column)}} </th>
<td mat-cell *matCellDef="let element"> {{element[column]}} </td>
</ng-container>
<ng-container matColumnDef="expand">
<th mat-header-cell *matHeaderCellDef aria-label="row actions"> </th>
<td mat-cell *matCellDef="let element">
<button mat-icon-button aria-label="expand row"
(click)="(expanded_element = expanded_element === element ? null : element);
$event.stopPropagation()"
[hidden]="!element.needs_expand">
<mat-icon *ngIf="expanded_element === element">keyboard_arrow_up</mat-icon>
<mat-icon *ngIf="expanded_element !== element">keyboard_arrow_down</mat-icon>
</button>
</td>
</ng-container>
<!-- Expanded Content Column - The detail row is made up of this one column that spans across all columns -->
<ng-container matColumnDef="expandedDetail">
<td mat-cell *matCellDef="let element" [attr.colspan]="intersector_displayed_columns_with_expand.length">
<div class="detail"
[@detailExpand]="element == expanded_element ? 'expanded' : 'collapsed'">
<div class="detail-value">
{{element.dim_values}}
</div>
</div>
</td>
</ng-container>
<!-- Creates a header row for the table. -->
<tr mat-header-row *matHeaderRowDef="intersector_displayed_columns_with_expand"></tr>
<!-- Defines the data rows. When a row is clicked, it toggles the expanded state of the row -->
<tr mat-row *matRowDef="let element; columns: intersector_displayed_columns_with_expand;"
class="data-row"
[style.cursor]="element.needs_expand ? 'pointer' : 'default'"
[class.expanded-row]="expanded_element === element"
(click)="expandRow(element)">
</tr>
<!-- Defines the expanded detail row. -->
<tr mat-row *matRowDef="let row; columns: ['expandedDetail']" class="detail-row"></tr>
</table>
</div>
</section>
<footer></footer>
src/app/components/visualization/intersection-details-dialog/intersection-details-dialog.component.html END
html, body{ margin: 0 0 0 0; padding: 0 0 0 0;
width: 100%;
height: 100%;
background-color: red;
}
import { Component, ElementRef, Input, Renderer2 } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DataPoint } from 'src/app/models/datapoint';
@Component({ selector: 'app-datapoint-tooltip', standalone: true, imports: [CommonModule], templateUrl: './datapoint-tooltip.component.html', styleUrls: ['./datapoint-tooltip.component.scss'] }) export class DataPointTooltipComponent { protected visible: boolean = false; private datapoint: DataPoint;
constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
ngOnInit(): void { console.log(this.datapoint) }
public toggleVisibility(){ this.visible = !this.visible; }
public setDatapoint(datapoint: DataPoint){ this.datapoint = datapoint; }
public setPosition(top: number, left: number) { let top_str = top + 'px'; let left_str = left + 'px'; this.renderer.setStyle(this.elementRef.nativeElement, 'top', top_str); this.renderer.setStyle(this.elementRef.nativeElement, 'left', left_str); } }
### src/app/components/visualization/datapoint-tooltip/datapoint-tooltip.component.html END ###import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DataPointTooltipComponent } from './datapoint-tooltip.component';
describe('DatapointTooltipComponent', () => { let component: DataPointTooltipComponent; let fixture: ComponentFixture;
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ DataPointTooltipComponent ] }) .compileComponents();
fixture = TestBed.createComponent(DataPointTooltipComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => { expect(component).toBeTruthy(); }); });
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchDialogComponent } from './search-dialog.component';
describe('SearchDialogComponent', () => { let component: SearchDialogComponent; let fixture: ComponentFixture;
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ SearchDialogComponent ] }) .compileComponents();
fixture = TestBed.createComponent(SearchDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => { expect(component).toBeTruthy(); }); });
body{ margin: 0 0 0 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow-y: hidden;
header{
width: 100%;
height: 10%;
padding-bottom: 1em;
border-bottom: 1px solid lightgray;
h1{
padding: 1em 1em 1em 1em;
margin: 0 0 0.5em 0;
}
}
section{
width: 100%;
height: 70%;
display: flex;
flex-direction: column;
#inputs-wrapper{
height: fit-content;
display: flex;
flex-direction: row;
justify-content: flex-start;
padding: 1em 0 1em 1em;
overflow-x: auto;
overflow-y: hidden;
mat-form-field{
padding: 0 1em 0 0;
width: fit-content;
height: fit-content;
}
button:hover{
cursor: pointer;
}
}
#table{
padding: 1em 0 1em 1em;
width: 98%;
height: 100%;
display: flex;
flex-direction: row;
// justify-content: center;
overflow-x: auto;
white-space: nowrap;
.table_column{
display: flex;
flex-direction: column;
min-width: 150px; // Ensure minimum width for columns
height: fit-content;
padding-right: 1em;
h3{
padding-left: 1em;
margin: 0 0 0.5em 0;
user-select: none;
// color: #3f51b5; // Example color
}
.selected_value_wrapper{
display: flex;
flex-direction: row;
align-items: center; // Align items vertically
padding: 0.5em 0.5em 0.5em 0.5em;
margin-bottom: 0.5em;
border-radius: 5px;
background-color: rgb(214, 214, 214);
overflow: hidden;
p {
margin: 0; // Remove default paragraph margins
flex-grow: 1; // Allow the text to take up available space
}
button {
background: none;
border: none;
padding: 0;
margin-left: 0.5em;
cursor: pointer;
mat-icon {
font-size: 16px;
color: #f44336; // Example color
}
}
}
}
}
}
footer{
width: 100%;
height: 10%;
display: flex;
justify-content: flex-end;
align-items: flex-end;
padding-top: 1em;
border-top: 1px solid lightgray;
#dialog-actions{
padding-right: 1em;
button{
width: 8em;
height: 3em;
margin-left: 1em;
}
button:hover{
cursor: pointer;
}
}
}
}
import { Component, Inject } from '@angular/core'; import { AsyncPipe, CommonModule } from '@angular/common'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatSelectModule } from '@angular/material/select'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { ApiService } from 'src/app/services/api/api.service'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatIconModule } from '@angular/material/icon'; import { MatTooltipModule } from '@angular/material/tooltip'; import {MatAutocompleteModule} from '@angular/material/autocomplete';
@Component({ selector: 'app-search-dialog', standalone: true, imports: [CommonModule, MatFormFieldModule, MatSelectModule, MatTableModule, FormsModule, MatIconModule, MatTooltipModule, FormsModule, MatAutocompleteModule,ReactiveFormsModule, AsyncPipe], templateUrl: './search-dialog.component.html', styleUrls: ['./search-dialog.component.scss'] }) export class SearchDialogComponent { public static WIDTH = '60vw'; public static HEIGHT = '70vh';
protected selected_input: number; protected filtered_values:string[];
private previous_filters: string[][];
protected nb_of_dims: number[]; protected dims_values: string[][]; protected selectedValues: string[][];
protected displayedColumns: string[];
constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: {previous_filters: string[][]}, private api_service: ApiService) { this.previous_filters = data.previous_filters; this.loadData(); }
protected resetFilteredValues(dim_index: number){ this.selected_input = dim_index; this.filtered_values = this.dims_values[dim_index]; }
protected onKey(event: KeyboardEvent) { const inputValue = (event.target as HTMLInputElement).value; this.filtered_values = this.search(inputValue); }
protected search(value: string) { let filter = value.toLowerCase(); return this.dims_values[this.selected_input].filter(option => option.toLowerCase().includes(filter)); }
private async loadData(){ this.dims_values = await this.api_service.getAllDimsValues(); this.nb_of_dims = Array(this.dims_values.length).fill(0).map((_, i) => i); this.displayedColumns = this.nb_of_dims.map(i => 'dim' + (i + 1)); this.resetSelectedValues();
if (this.previous_filters){
this.previous_filters.forEach((filter, i) => {
this.selectedValues[i] = filter;
});
}
}
protected onSelectionChange(value, dim_index){ this.selectedValues[dim_index].push(value); }
protected deleteValue(dim_index: number, value_index: number){ this.selectedValues[dim_index].splice(value_index, 1); }
protected clearFilters(){ this.resetSelectedValues(); }
protected close(){ this.dialogRef.close(); }
protected ok(): Array<Array>{ this.dialogRef.close(this.selectedValues); return this.selectedValues; // Return the selected values }
private resetSelectedValues(){ this.selectedValues = []; this.nb_of_dims.forEach(i => this.selectedValues.push([])); } }
<section>
<div id="inputs-wrapper">
<mat-form-field *ngFor="let i of nb_of_dims; let dim_index = index" appearance="fill">
<mat-label>Dimension {{dim_index + 1}}</mat-label>
<mat-select (click)="resetFilteredValues(dim_index)" (selectionChange)="onSelectionChange($event.value, dim_index)">
<input class="select-filter" placeholder="Filter values" (keyup)="onKey($event)">
<mat-option *ngFor="let value of filtered_values" [value]="value">{{value}}</mat-option>
</mat-select>
</mat-form-field>
<button mat-fab aria-label="Reset filters" matTooltip="Clear filters" (click)="clearFilters()">
<mat-icon>autorenew</mat-icon>
</button>
</div>
<div id="table">
<div class="table_column" *ngFor="let i of nb_of_dims; let dim_index = index">
<h3>Dimension {{dim_index + 1}}</h3>
<div class="selected_value_wrapper" *ngFor="let selected_value of selectedValues[dim_index]; let j = index">
<p>{{selected_value}}</p>
<button mat-icon-button (click)="deleteValue(dim_index, j)">
<mat-icon>close</mat-icon>
</button>
</div>
</div>
</div>
</section>
<footer>
<div id="dialog-actions" mat-dialog-actions>
<button mat-button mat-dialog-close cdkFocusInitial (click)="close()">Close</button>
<button mat-button mat-dialog-close (click)="ok()">Ok</button>
</div>
</footer>
<p>Number of patterns: {{pattern_number - 1}} </p>
</div>
@use '@angular/material' as mat; @import 'src/theme.scss';
html,body{ height: 100%; width: 100%; padding: 0 0 0 0; margin : 0 0 0 0;
overflow: hidden;
background-color: mat.get-color-from-palette($primary-palette, 900);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
user-select: none;
#drawer{
z-index: 0;
background-color: red;
width: 10%;
height: 10%;
user-select: none;
}
#drawer:hover{
cursor: pointer;
}
#visualization_div{
z-index: 0;
position: relative;
object-fit:contain;
width: 90%;
height: 25%; // Of available space
margin-top: 2em;
border: 4px solid mat.get-color-from-palette($primary-palette, 700);
user-select: none;
background-color: mat.get-color-from-palette($primary-palette, '900-contrast');
}
#slider-div{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 87%;
height: 15%;
padding-right: 1%;
padding-left: 1%;
padding-top: 0%;
// padding-bottom: 1%;
mat-slider{
// color: white;
}
.mat-mdc-slider {
width: 100%;
// background-color: brown;
}
p{
user-select: none;
color: mat.get-color-from-palette($primary-palette, '900-contrast');
}
// background-color: yellow;
}
#placeholder{
height: 60%;
background-color: red;
}
}
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RssViewComponent } from './rss-view.component';
describe('RssViewComponent', () => { let component: RssViewComponent; let fixture: ComponentFixture;
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ RssViewComponent ] }) .compileComponents();
fixture = TestBed.createComponent(RssViewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => { expect(component).toBeTruthy(); }); });
import { resolveResource } from '@tauri-apps/api/path' import * as d3 from 'd3'; import { Component, ElementRef, EventEmitter, Output, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import {MatSliderModule} from '@angular/material/slider'; import { SvgService } from 'src/app/services/svg/svg.service'; import { FormsModule } from '@angular/forms'; import {MatCheckboxModule} from '@angular/material/checkbox'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatCardModule} from '@angular/material/card'; import { DataPoint } from 'src/app/models/datapoint'; import { fs, invoke } from '@tauri-apps/api'; import { ChangeDetectorRef } from '@angular/core'; import { AfterViewInit } from '@angular/core' import { Color } from 'src/app/models/color'; import { ActivatedRoute } from '@angular/router'; import { Subscription } from 'rxjs'; import { environment } from 'src/environments/environment'; import { DialogService } from 'src/app/services/dialog/dialog.service'; import { ApiService } from 'src/app/services/api/api.service';
@Component({ selector: 'app-rss-view', standalone: true, imports: [ CommonModule, MatSliderModule, FormsModule ], templateUrl: './rss-view.component.html', styleUrls: ['./rss-view.component.scss'] }) export class RssViewComponent implements AfterViewInit{ @ViewChild('body') body: ElementRef;
@ViewChild('visualization_div') visualization_div: ElementRef; private svg: any; private plot: any;
private initial_scale: number = 1.4; private number_of_gridlines: number = 40; private y_correction = 0;
private svg_width: number; private svg_height: number; private x_scale: any; private y_scale: any;
protected sliderDisabled: boolean = false; @Output() onTruncation: EventEmitter = new EventEmitter(); @Output() initialized: EventEmitter = new EventEmitter();
public rss_evolution: Array = []; private datapoints: Array; private scaled_datapoints: Array; protected pattern_number; private initial_pattern_number: number;
constructor(private route: ActivatedRoute, private dialog_service: DialogService, private api_service: ApiService){}
async ngAfterViewInit() { this.rss_evolution = await this.api_service.getFullRssEvolution(); let subpatterns_identifiers: number[] = await this.api_service.getAllSubpatternsIdentifiers();
this.pattern_number = this.rss_evolution.length;
this.initial_pattern_number = this.pattern_number;
this.datapoints = this.wrapIntoDatapoints(this.rss_evolution, subpatterns_identifiers);
let width = this.visualization_div.nativeElement.clientWidth;
let height = this.visualization_div.nativeElement.clientHeight;
this.svg = this.createSvg();
this.resizeSvg(width, height, 0);
this.drawDataPoints();
this.connectDatapoints();
this.initialized.emit();
}
public async reset(){ this.pattern_number = this.initial_pattern_number; this.onSliderDrag(null); this.onSliderChange(null); }
private wrapIntoDatapoints(rss_evolution: Array, subpatterns_identifiers: number[]): Array{ let datapoints: DataPoint[] = []; let subpatterns_identifiers_set: Set = new Set(subpatterns_identifiers);
let gray_shade = 160;
for (let i = 0; i < rss_evolution.length; i++){
let identifier = i; // i because the first index is the null model rss
let x = undefined;
let y = undefined;
let r = 0;
let g = 0;
let b = 0;
let a = 1;
if(subpatterns_identifiers_set.has(identifier)){
r = gray_shade;
g = gray_shade;
b = gray_shade;
}
let datapoint = new DataPoint(i, 10, 10, 0, 0, x, y, r, g, b, a);
datapoints[i] = datapoint;
}
return datapoints;
}
private scalingFunction(datapoints: Array): Array{ let min_rss = Math.min(...this.rss_evolution.map(rss => Math.abs(rss))); let max_rss = Math.max(...this.rss_evolution.map(rss => Math.abs(rss)));
let max_y = max_rss;
let y_range = max_rss - min_rss;
let length = datapoints.length;
let lateral_screen_coverage = 1;
let vertical_screen_coverage = 0.9;
let scaled_datapoints: Array<DataPoint> = datapoints;
for (let i = 0; i < datapoints.length; i++){
let datapoint: DataPoint = datapoints[i];
let rss = this.rss_evolution[i];
let x = ((i + 0.5)/length) * 2 - 1; // scale x to be between -1 and 1
x /= ((1-lateral_screen_coverage) + 1)
let y = (rss - min_rss) / y_range; // Scale y to be between 0 and 1
y = y * 2 - 1; // Scale y to be between -1 and 1
y /= ((1-vertical_screen_coverage) + 1)
let radius = 3;
let scaled_datapoint = new DataPoint(i, radius, 10, 0, 0, x, y, datapoint.r, datapoint.g, datapoint.b, datapoint.a);
scaled_datapoints[i] = scaled_datapoint;
}
return scaled_datapoints;
}
private drawDataPoints() { if(this.plot == undefined){ return; }
this.scaled_datapoints = this.scalingFunction(this.datapoints);
const circles = this.plot.selectAll('circle')
.data(this.scaled_datapoints, d => d.identifier); // Each datapoint has a unique identifier
circles.enter().append('circle') // Add new datapoints with animation
.attr('cx', d => {
const result = this.x_scale(parseFloat(d.x));
return result;
})
.attr('cy', d => this.y_scale(parseFloat(d.y)))
.attr('r', d => d.size)
.attr('fill', d => `rgba(${d.r}, ${d.g}, ${d.b}, ${d.a})`)
.style('cursor', 'pointer'); // Set cursor to pointer
}
private connectDatapoints(){ console.log("Connecting datapoints"); if(this.scaled_datapoints.length < 2){ return; }
let line = d3.line<DataPoint>()
.x(d => this.x_scale(d.x))
.y(d => this.y_scale(d.y));
for(let i = 0; i < this.scaled_datapoints.length - 1; i++) {
let point1 = this.scaled_datapoints[i];
let point2 = this.scaled_datapoints[i+1];
this.plot.append('path')
.attr('d', line([point1, point2]))
.attr('stroke', 'black')
.attr('stroke-width', 2)
.attr('fill', 'none');
}
}
public enableSlider(){ this.sliderDisabled = false; } public disableSlider(){ this.sliderDisabled = true; }
protected onSliderChange(event: any) { this.onTruncation.emit(this.pattern_number); }
protected onSliderDrag(event: any) { let x = this.datapoints[this.pattern_number - 1].x; this.drawVerticalLine(x); }
public onResize(event) { let width = this.visualization_div.nativeElement.clientWidth; let height = this.visualization_div.nativeElement.clientHeight;
this.resizeSvg(width, height);
this.drawDataPoints();
this.connectDatapoints();
}
public getPatternNumber(): number{
return this.pattern_number;
}
public setPatternNumber(pattern_number: number){ this.pattern_number = pattern_number; }
// ========================= SVG FUNCTIONS ========================= //
private createSvg(){ let svg = d3.select(this.visualization_div.nativeElement) .append('svg') .attr('width', this.svg_width) .attr('height',this.svg_height);
return svg;
}
public resizeSvg(width: number, height: number, y_correction=0){ this.svg .attr('width', width) .attr('height', height);
let x_scale;
x_scale = d3.scaleLinear()
.domain([-1, 1])
.range([0, (width/1)]);
let y_scale = d3.scaleLinear()
.domain([-1, 1])
.range([(height - y_correction)/1, 0]);
this.x_scale = x_scale;
this.y_scale = y_scale;
this.svg_width = width;
this.svg_height = height;
this.createPlot();
}
private drawGridLines() { let makeXGridlines = () => { return d3.axisBottom(this.x_scale).ticks(this.number_of_gridlines) } let makeYGridlines = () => { return d3.axisLeft(this.y_scale).ticks(this.number_of_gridlines) }
// Add the X gridlines
this.plot.append("g")
.attr("class", "grid")
.attr("transform", "translate(0," + this.svg_height + ")")
.attr("color", "lightgrey")
.call(makeXGridlines()
.tickSize(-this.svg_height)
.tickFormat(() => "")
)
// Add the Y gridlines
this.plot.append("g")
.attr("class", "grid")
.attr("color", "lightgrey")
.call(makeYGridlines()
.tickSize(-1 * this.svg_width)
// .tickSize(-300)
.tickFormat(() => "")
)
}
private createPlot(){ if(this.plot != undefined){ this.svg.select("#plot").remove(); } this.plot = this.svg.append("g").attr("id", "plot");
this.drawGridLines();
}
private drawVerticalLine(x: number) { this.plot.selectAll('#vertical-line').remove();
this.plot.append('line')
.attr('id', 'vertical-line')
.attr('x1', this.x_scale(x))
.attr('y1', 0)
.attr('x2', this.x_scale(x))
.attr('y2', this.svg_height)
.attr('stroke', 'red')
.attr('stroke-width', 2);
}
// ========================= SVG FUNCTIONS ========================= // }
import { open } from '@tauri-apps/api/dialog'; import {Component, EventEmitter, Inject, Input, NgModule, Output} from '@angular/core'; import {MatDialogRef, MatDialogModule} from '@angular/material/dialog'; import {MatButtonModule} from '@angular/material/button'; import {MatIconModule} from '@angular/material/icon'; import { MAT_DIALOG_DATA } from '@angular/material/dialog';
@Component({ selector: 'app-file-selection-dialog', templateUrl: './file-selection-dialog.component.html', styleUrls: ['./file-selection-dialog.component.scss'] }) export class FileSelectionDialogComponent { public static WIDTH = '45vw'; public static HEIGHT = '50vh'; @Output() modelChange: EventEmitter = new EventEmitter();
private last_opened_folder: string;
private tensor_path: string = ""; protected tensor_name: string = "";
private patterns_path: string = ""; protected patterns_name: string = "";
constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: {last_opened_folder: string, tensor_path: string, patterns_path: string}) { this.last_opened_folder = data.last_opened_folder; this.tensor_path = data.tensor_path; this.patterns_path = data.patterns_path; this.setNames(); }
private isStateValid(): boolean{ if(this.tensor_path == undefined || this.tensor_path == null || this.tensor_path == ""){ return false; }
if(this.patterns_path == undefined || this.patterns_path == null || this.patterns_path == ""){
return false;
}
return true;
}
private setNames(){ this.tensor_name = this.tensor_path.split('\').pop().split('/').pop(); this.patterns_name = this.patterns_path.split('\').pop().split('/').pop(); }
public async selectTensor(){ const options = { multiple: false, defaultPath: this.last_opened_folder }; const selected = await open(options); if (selected === null) { return; } // No tensor selected
this.tensor_path = selected.toString();
this.setNames();
if (this.tensor_path == ""){ return; } // No tensor selected
this.last_opened_folder = this.tensor_path;
}
public async selectPatterns(){ const options = { multiple: false, defaultPath: this.last_opened_folder }; const selected = await open(options); if (selected === null) { return; } // No patterns selected
this.patterns_path = selected.toString();
this.setNames();
if (this.patterns_path == ""){ return; } // No patterns selected
this.last_opened_folder = this.patterns_path;
}
protected submit() { if (this.isStateValid()){ this.dialogRef.close({last_opened_folder: this.last_opened_folder, tensor_path: this.tensor_path, patterns_path: this.patterns_path}); }else{ this.dialogRef.close({last_opened_folder: "", tensor_path: null, patterns_path: null}); } } }
@NgModule({ declarations: [FileSelectionDialogComponent], imports: [ MatButtonModule, MatDialogModule, MatIconModule], }) export class FileSelectionDialogComponentModule {}
<div id="dialog-content" mat-dialog-content>
Select the source tensor file and the patterns you want to visualize.
</div>
<div id="file-selection-wrapper">
<div class="file-selector" id="tensor-selection">
<h2>Tensor</h2>
<div class="clickable">
<mat-icon (click)="selectTensor()"> attach_file </mat-icon>
<p>{{this.tensor_name}}</p>
</div>
</div>
<div class="file-selector" id="patterns-selection">
<h2>Patterns</h2>
<div class="clickable">
<mat-icon (click)="selectPatterns()"> attach_file </mat-icon>
<p>{{this.patterns_name}}</p>
</div>
</div>
</div>
<div id="dialog-actions" mat-dialog-actions>
<button mat-button mat-dialog-close cdkFocusInitial>Close</button>
<button mat-button mat-dialog-close (click)="submit()">Ok</button>
</div>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FileSelectionDialogComponent } from './file-selection-dialog.component';
describe('FileSelectionDialogComponent', () => { let component: FileSelectionDialogComponent; let fixture: ComponentFixture;
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ FileSelectionDialogComponent ] }) .compileComponents();
fixture = TestBed.createComponent(FileSelectionDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => { expect(component).toBeTruthy(); }); });
@use '@angular/material' as mat; @import 'src/theme.scss'; body{ margin: 0 0 0 0; padding: 0 0 0 0;
display: flex;
flex-direction: column;
// background-color: red;
width: 100%;
height: 100%;
#title{
margin: 0 0 0 0;
flex: 0.5;
// background-color: blue;
}
#dialog-content{
flex: 0.75;
padding: 1em 1em 1em 1em;
// background-color: green;
}
#file-selection-wrapper{
flex: 3;
// background-color: yellow;
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
.file-selector{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
// background-color: orange;
width: 12em;
height: 12em;
h2{
margin: 0 0 0 0;
font-size: 1.25em;
text-align: center;
}
.clickable{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
padding: 1em 0 0 0;
// background-color: green;
mat-icon{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 3.5em;
width: 50%;
height: 50%;
padding: 0.15em 0.10em 0.15em 0.10em;
color: mat.get-color-from-palette($primary-palette, '50-contrast');
background-color: mat.get-color-from-palette($primary-palette, 50);
border: 2px solid mat.get-color-from-palette($primary-palette, 100);
border-radius: 10px;
}
mat-icon:hover{
background-color: mat.get-color-from-palette($primary-palette, 100);
cursor: pointer;
}
p{
padding: 1em 0 0 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
// background-color: red;
}
}
}
}
#dialog-actions{
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
flex: 0.5;
// background-color: purple;
}
}
<section>
<div id="dim-summaries">
<div class="dim-summary" *ngFor="let dim_values of this.pattern.dims_values; let i = index" (click)="openDimDialog(i)">
<div class="dim-summary-header">
<h2>DIM {{i+1}}</h2>
<span>({{dim_values.length}} elements)</span> <br>
</div>
<div class="dim-summary-content">
<span class="element"> {{this.formatDimValues(dim_values)}} </span>
</div>
</div>
</div>
</section>
<footer></footer>
</div>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PatternSummaryComponent } from './pattern-summary.component';
describe('PatternSummaryComponent', () => { let component: PatternSummaryComponent; let fixture: ComponentFixture;
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ PatternSummaryComponent ] }) .compileComponents();
fixture = TestBed.createComponent(PatternSummaryComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => { expect(component).toBeTruthy(); }); });
@use '@angular/material' as mat; @import '../../../theme.scss';
body{ padding: 0 0 0 0; margin: 0 0 0 0;
width: 100%;
height: 100%;
// background-color: red;
overflow-x: hidden; /* Hide horizontal scrollbar */
#info{
padding: 0em 0 1em 2em;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
// background-color: yellow;
h1{
margin: 0 0 0 0;
// background-color: red;
}
#details{
// background-color: green;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
span{
padding-right: 1em;
}
}
}
section{
height: 100%;
#dim-summaries{
.dim-summary{
padding: 1em 0 1em 2em;
width: 100%;
max-height: 15em;
// background-color: red;
overflow: hidden;
user-select: none;
.dim-summary-header{
display: flex;
flex-direction: row;
align-items: center;
h2,span{
margin: 0 0 0 0;
padding: 0 0 0 0;
}
h2{
padding-right: 0.5em;
}
}
.dim-summary-content{
display: inline-block;
width: 85%;
// white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
word-wrap: break-word;
.element{
word-wrap: break-word;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.dim-summary:hover{
cursor: pointer;
background-color: mat.get-color-from-palette($primary-palette, 800);
}
}
}
}
import { ChangeDetectorRef, Component, Input, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Pattern } from 'src/app/models/pattern'; import { environment } from 'src/environments/environment'; import { fs, invoke } from '@tauri-apps/api'; import { DialogService } from 'src/app/services/dialog/dialog.service'; import { resolveResource } from '@tauri-apps/api/path'; import {MatTabsModule} from '@angular/material/tabs'; import {MatTableDataSource, MatTableModule} from '@angular/material/table'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatSort, MatSortModule} from '@angular/material/sort'; import {MatPaginator, MatPaginatorModule} from '@angular/material/paginator'; import { MatInputModule } from '@angular/material/input'; import {MatSelectChange, MatSelectModule} from '@angular/material/select'; import { ApiService } from 'src/app/services/api/api.service'; import { PatternDimDialogComponent } from './pattern-dim-dialog/pattern-dim-dialog.component';
const MAX_VALUE_STRING_LENGTH = 140;
@Component({ selector: 'app-pattern-summary', standalone: true, imports: [CommonModule, MatTabsModule, MatTableModule, MatFormFieldModule, MatPaginatorModule, MatInputModule, MatSelectModule], templateUrl: './pattern-summary.component.html', styleUrls: ['./pattern-summary.component.scss'] }) export class PatternSummaryComponent { @Input() public pattern: Pattern; private locked: boolean = false;
private input: HTMLInputElement;
constructor(private api_service: ApiService, private dialog_service: DialogService) {}
async ngOnInit(): Promise { console.log("Initializing PatternSummaryComponent"); // await this.update(1); // TODO: Retirar // this.openDimDialog(1); // TODO: Retirar }
protected formatDimValues(dims_values: string[]): string { let formated_string = dims_values.join(", ");
if(formated_string.length > MAX_VALUE_STRING_LENGTH){
formated_string = formated_string.slice(0, MAX_VALUE_STRING_LENGTH) + " (...)";
}
return formated_string;
}
protected openDimDialog(dim_index: number): void { let dialog_data = { dim_values: this.pattern.dims_values[dim_index] };
this.dialog_service.open(PatternDimDialogComponent,
PatternDimDialogComponent.WIDTH,
PatternDimDialogComponent.HEIGHT,
dialog_data);
}
public async update(identifier){ if (this.locked){ return; }
if(identifier == null){
this.pattern = undefined;
return;
}
this.pattern = await this.api_service.getPattern(identifier);
}
public toggleLock(identifier: number){ if(identifier == null){ // De-select current pattern this.locked = false; this.update(null); return; }
if((!this.pattern) || (identifier != this.pattern.identifier)){ // Lock on another pattern
this.locked = false;
this.update(identifier);
}
this.locked = !this.locked;
} }
import { Component, Inject, Input, OnInit, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field';
@Component({ selector: 'app-pattern-dim-dialog', standalone: true, imports: [CommonModule, MatFormFieldModule, MatPaginatorModule, MatInputModule, MatTableModule], templateUrl: './pattern-dim-dialog.component.html', styleUrls: ['./pattern-dim-dialog.component.scss'] }) export class PatternDimDialogComponent implements OnInit{ public static WIDTH = '40vw'; public static HEIGHT = '60vh';
private dim_values: string[]; protected displayed_columns: string[] = ['Elements']; // protected data_source: MatTableDataSource<Array>; protected data_source;
@ViewChild("input") input: HTMLInputElement;
constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: {dim_values: string[]}) { this.dim_values = data.dim_values; this.data_source = new MatTableDataSource(data.dim_values); }
ngOnInit(): void {
}
protected applyFilter(event: Event) { this.data_source.data = this.dim_values; this.input = (event.target as HTMLInputElement);
const filterValue = (event.target as HTMLInputElement).value.trim().toLowerCase();
let filteredData = this.data_source.data.filter(item => {
let itemStr = JSON.stringify(item).toLowerCase();
return itemStr.includes(filterValue);
});
this.data_source.data = filteredData;
} }
body{ padding: 0 0 0 0; margin: 0 0 0 0;
width: 100%;
height: 100%;
// background-color: red;
display: flex;
flex-direction: row;
overflow: hidden;
#table-wrapper{
width: 70%;
height: 90%;
overflow-y: auto;
overflow-x: auto;
}
#filter{
overflow: hidden;
padding-top: 1em;
padding-left: 2em;
width: 20%;
height: 20%;
}
}
<tr mat-header-row *matHeaderRowDef="displayed_columns; sticky: true"></tr>
<tr class="intersectors_data_row" mat-row *matRowDef="let row; columns: displayed_columns;"></tr>
</table>
</div>
<mat-form-field id="filter" class="filter">
<mat-label>Filter</mat-label>
<input #input matInput (keyup)="applyFilter($event)" placeholder="">
</mat-form-field>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PatternDimDialogComponent } from './pattern-dim-dialog.component';
describe('PatternDimDialogComponent', () => { let component: PatternDimDialogComponent; let fixture: ComponentFixture;
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ PatternDimDialogComponent ] }) .compileComponents();
fixture = TestBed.createComponent(PatternDimDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => { expect(component).toBeTruthy(); }); });
body{ overflow: hidden; padding: 0 0 0 0; margin: 0 0 0 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
// background-color: red;
width: 100%;
height: 100%;
header{
width: 100%;
display: flex;
flex-direction: column;
// background-color: blue;
#title{
display: flex;
flex-direction: row;
align-items: center;
padding: 1em 0 0 1em;
// background-color: green;
mat-icon{
color: red;
}
h1{
margin: 0 0 0 0;
padding: 0 0 0 0.5em;
// background-color: yellow;
// height: 20%;
}
}
span{
padding: 0.5em 0 0 1em;
width: 100%;
height: 10%;
// background-color: blue;
}
}
section{
padding: 1em 0 0 2em;
width: 100%;
height: 40%;
// background-color: green;
color: red;
}
#dialog-actions{
// height: 10%;
// background-color: yellow;
}
}
import { Component, Inject, NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import {MatButtonModule} from '@angular/material/button'; import {MatIconModule} from '@angular/material/icon';
@Component({ selector: 'app-error-dialog', templateUrl: './error-dialog.component.html', styleUrls: ['./error-dialog.component.scss'] }) export class ErrorDialogComponent { public static WIDTH = '30vw'; public static HEIGHT = '30vh';
protected error_message: string;
constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: {error_message: string}) { this.error_message = data.error_message; }
protected submit(){ this.dialogRef.close(); }
}
@NgModule({ declarations: [ErrorDialogComponent], imports: [ CommonModule, MatButtonModule, MatIconModule ], }) export class ErrorDialogComponentModule {}
<span>An error occurred while executing the application:</span>
</header>
<section>{{this.error_message}}</section>
<div id="dialog-actions" mat-dialog-actions>
<button mat-button mat-dialog-close cdkFocusInitial (click)="submit()">Close</button>
</div>
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ErrorDialogComponent } from './error-dialog.component';
describe('ErrorDialogComponent', () => { let component: ErrorDialogComponent; let fixture: ComponentFixture;
beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ ErrorDialogComponent ] }) .compileComponents();
fixture = TestBed.createComponent(ErrorDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => { expect(component).toBeTruthy(); }); });
export class Color{ r: number; g: number; b: number; }
export class IntersectionDetails{ identifier: number; total_untouched_percentage: number; total_intersection_percentage: number; intersections: Map<number, [number, Array<Array>]>;
constructor (identifier: number, total_untouched_percentage: number, total_intersection_percentage: number,
intersections: Map<number, [number, Array<Array<string>>]>){
this.identifier = identifier;
this.total_untouched_percentage = total_untouched_percentage;
this.total_intersection_percentage = total_intersection_percentage;
this.intersections = intersections;
}
}
export class Pattern{ identifier:number; dims_values: Array<Array>; density: number; size: number;
constructor(identifier: number, dims_values: Array<Array<string>>, density: number, size: number){
this.identifier = identifier;
this.dims_values = dims_values;
this.density = density;
this.size = size;
}
public static fromResponse(response: any): Pattern{
return new Pattern(response.identifier, response.dims_values, response.density, response.size);
}
}
// import * as d3 from 'd3'; // import { ElementRef } from '@angular/core'; // import { DataPoint } from './datapoint'; // import { event } from '@tauri-apps/api';
// export class Svg { // public d3_svg: any; // public plot: any;
// private width: number; // private height: number;
// private x_scale: any; // private y_scale: any;
// private gridlines: boolean; // private number_of_gridlines: number;
// private pannable: boolean;
// private initial_scale: number;
// constructor(vizualization_div: ElementRef, width: number, height: number, // number_of_gridlines: number = 40, gridlines: boolean = true, pannable: boolean = true){
// this.width = width; // this.height = height; // this.number_of_gridlines = number_of_gridlines; // this.gridlines = gridlines; // this.pannable = pannable; // this.create(vizualization_div); // }
// private create(vizualization_div: ElementRef){ // this.d3_svg = d3.select(vizualization_div.nativeElement) // .append('svg') // .attr('width', this.width) // .attr('height',this.height); // }
// public resize(width: number, height: number, y_correction=0){ // this.d3_svg // .attr('width', width) // .attr('height', height);
// let x_scale;
// if(this.pannable){ // Only the pannable visualization will have square aspect ratio // x_scale = d3.scaleLinear() // .domain([-1, 1]) // .range([0, (height - y_correction)/1]);
// }else if(!this.pannable){ // x_scale = d3.scaleLinear() // .domain([-1, 1]) // .range([0, (width/1)]); // }
// let y_scale = d3.scaleLinear() // .domain([-1, 1]) // .range([(height - y_correction)/1, 0]);
// this.x_scale = x_scale; // this.y_scale = y_scale; // this.width = width; // this.height = height;
// this.createPlot(); // }
// private drawGridLines() { // let makeXGridlines = () => { return d3.axisBottom(this.x_scale).ticks(this.number_of_gridlines) } // let makeYGridlines = () => { return d3.axisLeft(this.y_scale).ticks(this.number_of_gridlines) }
// // Add the X gridlines // this.plot.append("g") // .attr("class", "grid") // .attr("transform", "translate(0," + this.height + ")") // .attr("color", "lightgrey") // .call(makeXGridlines() // .tickSize(-this.height) // .tickFormat(() => "") // )
// // Add the Y gridlines // this.plot.append("g") // .attr("class", "grid") // .attr("color", "lightgrey") // .call(makeYGridlines() // .tickSize(-1 * this.width) // // .tickSize(-300) // .tickFormat(() => "") // ) // }
// private createPlot(){ // if(this.plot != undefined){ this.d3_svg.select("#plot").remove(); } // this.plot = this.d3_svg.append("g").attr("id", "plot");
// if(this.pannable){ // Only the pannable square visualization will execute this // let panning_zoom = d3.zoom() // .scaleExtent([1.4, 10]) // This control how much you can unzoom (x1) and zoom (x10) // // .translateExtent([[0, 0], [this.height, this.height/1.2]]) // .translateExtent([[0, 0], [this.height, this.height]]) // .on("start", (event, d) => { this.d3_svg.attr("cursor", "grabbing"); }) // .on("zoom", (event) => { this.plot.attr("transform", event.transform); }) // .on("end", (event, d) => {this.d3_svg.attr("cursor", "default")});
// this.d3_svg.call(panning_zoom);
// // Apply initial zoom level // this.initial_scale= 1.4; // let x_translation_factor = 0.0; // // let y_translation_factor = 0.15; // let y_translation_factor = 0.2; // let initial_transform = d3.zoomIdentity // .translate(-this.width*(x_translation_factor), -this.height*(y_translation_factor)) // // .translate(-this.width*(x_translation_factor), 0) // .scale(this.initial_scale); // this.d3_svg.call(panning_zoom.transform, initial_transform); // }
// if(this.gridlines){ this.drawGridLines(); } // // this.drawDataPoints(); // }
// public drawVerticalLine(x: number) { // // Remove any existing line // this.plot.selectAll('#vertical-line').remove();
// // Draw a new line // this.plot.append('line') // .attr('id', 'vertical-line') // .attr('x1', this.x_scale(x)) // .attr('y1', 0) // .attr('x2', this.x_scale(x)) // .attr('y2', this.height) // .attr('stroke', 'red') // .attr('stroke-width', 2); // }
// public getXScale(){ // return this.x_scale; // }
// public getYScale(){ // return this.y_scale; // }
// public getInitialScale(){ // return this.initial_scale; // } // }
export class DataPoint{ identifier: number; size: number; pattern_size: number; density: number; stroke_width: number;
x: number;
y: number;
r: number;
g: number;
b: number;
a: number;
constructor(identifier: number, size: number, pattern_size: number, density: number, stroke_width: number, x: number, y: number, r: number, g: number, b: number, a:number){
this.identifier = identifier;
this.pattern_size = pattern_size;
this.size = size;
this.density = density;
this.stroke_width = stroke_width;
this.x = x;
this.y = y;
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
}
import { TestBed } from '@angular/core/testing';
import { SvgService } from './svg.service';
describe('SvgService', () => { let service: SvgService;
beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(SvgService); });
it('should be created', () => { expect(service).toBeTruthy(); }); });
import { ElementRef, Injectable } from '@angular/core'; import { Color } from 'src/app/models/color'; import * as d3 from 'd3';
@Injectable({ providedIn: 'root' }) export class SvgService {
constructor() { }
// private drawGridLines() { // let makeXGridlines = () => { return d3.axisBottom(this.x_scale).ticks(40) } // let makeYGridlines = () => { return d3.axisLeft(this.y_scale).ticks(40) }
// // Add the X gridlines // this.plot.append("g") // .attr("class", "grid") // .attr("transform", "translate(0," + this.height + ")") // .attr("color", "grey") // .call(makeXGridlines() // .tickSize(-this.height) // .tickFormat(() => "") // )
// // Add the Y gridlines // this.plot.append("g") // .attr("class", "grid") // .attr("color", "grey") // .call(makeYGridlines() // .tickSize(-1 * this.width) // .tickFormat(() => "") // ) // }
// private createPlot(svg: any, width: number, height: number){ // svg.select("#plot").remove(); // let plot = svg.append("g").attr("id", "plot");
// let panning_zoom = d3.zoom() // .scaleExtent([1, 10]) // This control how much you can unzoom (x1) and zoom (x10) // .translateExtent([[0, 0], [width, height]]) // .on("start", (event, d) => { svg.attr("cursor", "grabbing"); }) // .on("zoom", (event) => { plot.attr("transform", event.transform); }) // .on("end", (event, d) => {svg.attr("cursor", "default")});
// svg.call(panning_zoom);
// // Apply initial zoom level // let initial_scale = 1.2; // let translation_factor = 0.1; // let initial_transform = d3.zoomIdentity // .translate(-width*(translation_factor), -height*(translation_factor)) // .scale(initial_scale); // svg.call(panning_zoom.transform, initial_transform);
// this.drawGridLines(); // this.drawDataPoints(); // }
}
import { Injectable, OnDestroy } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Subscription } from 'rxjs'; import { take } from 'rxjs/operators'; import { ErrorDialogComponent } from 'src/app/components/error-dialog/error-dialog.component';
@Injectable({ providedIn: 'root' }) export class DialogService implements OnDestroy{ private dialog_subscription: Subscription;
constructor(public dialog: MatDialog) { }
public open(dialog_component, width: string, height: string, dialog_data, closeFunction=null) { console.log("Opening dialog..."); let enterAnimationDuration = '300ms'; let exitAnimationDuration = '300ms';
const dialogRef = this.dialog.open(dialog_component, {
width: width,
height: height,
enterAnimationDuration,
exitAnimationDuration,
data: dialog_data
});
this.dialog_subscription = dialogRef.afterClosed().pipe(take(1)).subscribe(result => {
// Executes when the dialog is closed
if (result) {
if (closeFunction){
closeFunction(result);
}
}
});
}
ngOnDestroy() { if (this.dialog_subscription) { this.dialog_subscription.unsubscribe(); } }
public openErrorDialog(error_message: string) { this.open(ErrorDialogComponent, ErrorDialogComponent.WIDTH, ErrorDialogComponent.HEIGHT, {error_message: error_message}); } }
import { TestBed } from '@angular/core/testing';
import { DialogService } from './dialog.service';
describe('DialogService', () => { let service: DialogService;
beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(DialogService); });
it('should be created', () => { expect(service).toBeTruthy(); }); });
import { TestBed } from '@angular/core/testing';
import { ApiService } from './api.service';
describe('ApiService', () => { let service: ApiService;
beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(ApiService); });
it('should be created', () => { expect(service).toBeTruthy(); }); });
import { Injectable } from '@angular/core'; import { fs, invoke } from '@tauri-apps/api'; import { Pattern } from 'src/app/models/pattern'; import { environment } from 'src/environments/environment'; import { DialogService } from '../dialog/dialog.service'; import { resolveResource } from '@tauri-apps/api/path'; import { DataPoint } from 'src/app/models/datapoint'; import { IntersectionDetails } from 'src/app/models/intersection_details';
@Injectable({ providedIn: 'root' }) export class ApiService { constructor(private dialog_service: DialogService) { }
public async initApplication(tensor_path: string, patterns_path: string){ await invoke("initApplication", {tensorPath: tensor_path, patternsPath: patterns_path}).catch((error: any) => { // console.error(error); this.dialog_service.openErrorDialog("ERROR Could not read tensor or patterns."); throw error; }); }
public async getFullRssEvolution(): Promise<Array> { console.log("Initializing rss view component"); console.log("Invoking getFullRssEvolution");
let rss_evolution;
rss_evolution = await invoke("getFullRssEvolution").catch((error: any) => {
// console.error(error);
this.dialog_service.openErrorDialog("Could not load rss graph.");
throw error;
});
console.log("Received rss_evolution:");
console.log(rss_evolution);
return rss_evolution;
}
public async truncateModel(new_size: number): Promise{ console.log("Truncating datapoints to only: " + new_size); let truncated_datapoints; await invoke("truncateModel", {newSize: new_size}).catch((error: any) => { // console.error(error); this.dialog_service.openErrorDialog("Error while truncating datapoints."); throw error; });
truncated_datapoints = await this.getDataPoints();
return truncated_datapoints;
}
public async getIntersectionDetails(identifier: number): Promise{ let data: any; data = await invoke("getIntersectionDetails", {identifier: identifier}).catch((error: any) => { // console.error(error); this.dialog_service.openErrorDialog("Error while fetching intersection details."); throw error; });
let intersections: Map<number, [number, Array<Array<string>>]> = new Map();
for (let key in data.intersections) {
let value = data.intersections[key];
let percentage = Math.round(value[0]*100000)/100000;
let dims_intersections = value[1];
intersections.set(Number(key), [percentage, dims_intersections]);
}
let intersection_details: IntersectionDetails = new IntersectionDetails(
data.identifier,
Math.round(data.total_untouched_percentage * 10000)/10000,
Math.round(data.total_intersection_percentage * 10000)/10000,
intersections
);
return intersection_details;
}
public async getPattern(identifier: number): Promise { let pattern; pattern = await invoke("getPattern", {identifier: identifier}).catch((error: any) => { // console.error(error); this.dialog_service.openErrorDialog("Error while fetching pattern."); throw error; });
return Pattern.fromResponse(pattern);
}
public async getDataPoints(): Promise<Array> { console.log("Invoking getDataPoints"); let datapoints; datapoints = await invoke("getDataPoints").catch((error: any) => { // console.error(error); this.dialog_service.openErrorDialog("Error while fetching data points."); throw error; });
console.log("Received datapoints:");
console.log(datapoints);
return datapoints;
}
public async getAllSubpatternsIdentifiers(): Promise<Array> { let subpatterns_identifiers; subpatterns_identifiers = await invoke("getAllSubPatternsIdentifiers").catch((error: any) => { // console.error(error); this.dialog_service.openErrorDialog("Error while fetching subpatterns identifiers."); throw error; });
return subpatterns_identifiers;
}
public async getDatapointsWithSubPatterns(): Promise<Array> { let datapoints; datapoints = await invoke("getDatapointsWithSubPatterns").catch((error: any) => { // console.error(error); this.dialog_service.openErrorDialog("Error while fetching datapoints with subpatterns."); throw error; });
return datapoints;
}
public async descendDag(identifier: number): Promise<Array> { let datapoints; datapoints = await invoke("descendDag", {nextIdentifier: identifier}).catch((error: any) => { // console.error(error); this.dialog_service.openErrorDialog("Error while descending DAG."); throw error; });
return datapoints;
}
public async ascendDag(): Promise<Array> { let datapoints; datapoints = await invoke("ascendDag").catch((error: any) => { // console.error(error); this.dialog_service.openErrorDialog("Error while ascending DAG."); throw error; });
return datapoints;
}
public async getCurrentLevelBackgroundDensity(): Promise { let density; density = await invoke("getCurrentLevelBackgroundDensity").catch((error: any) => { // console.error(error); this.dialog_service.openErrorDialog("Error while fetching background density."); throw error; });
return density;
}
public async getAllDimsValues(): Promise<string[][]> { let dims_values; dims_values = await invoke("getAllDimsValues").catch((error: any) => { this.dialog_service.openErrorDialog("Error while fetching dimensions values."); throw error; });
return dims_values;
}
public async filterDatapoints(filters: string[][]): Promise<DataPoint[]> { let datapoints; datapoints = await invoke("filterDatapoints", {filters: filters}).catch((error: any) => { this.dialog_service.openErrorDialog("Error while filtering datapoints."); throw error; });
return datapoints;
} }
Elements ({{ data_source.data.length}}) | {{row}} |
---|