File
Description
Component which helps to visualize a confusion matrix.
As a set o function allowing some level of visual configuration.
Implements
Metadata
selector |
confusion-matrix |
styleUrls |
./confusion-matrix.component.scss |
templateUrl |
./confusion-matrix.component.html |
Index
Properties
|
|
Methods
|
|
Inputs
|
|
Outputs
|
|
Accessors
|
|
confusionMatrix
|
|
Sets the confusion matrix labels and values.
|
levelsColors
|
Type : Array
|
Default value : new Array<string>()
|
|
Sets the confusion matrix color level (a.k.a color intensity).
Should be order asc from less intense (close to 0) to max intense (close to max value).
|
roundRules
|
Default value : '1.0-2'
|
|
|
title
|
Default value : ""
|
|
Sets the confusion matrix title.
If undefined, the title reserved space will be hidden.
|
zoom
|
Type : number
|
|
Represents the confusion matrix size.
|
Methods
add
|
add(index: number)
|
|
Parameters :
Name |
Type |
Optional |
index |
number
|
No
|
|
allowDrop
|
allowDrop(event: any)
|
|
Parameters :
Name |
Type |
Optional |
event |
any
|
No
|
|
calculateInputSize
|
calculateInputSize(event: any)
|
|
Parameters :
Name |
Type |
Optional |
event |
any
|
No
|
|
changeLabel
|
changeLabel(event: any, index: number)
|
|
Parameters :
Name |
Type |
Optional |
event |
any
|
No
|
index |
number
|
No
|
|
changeTitle
|
changeTitle(event: any)
|
|
Parameters :
Name |
Type |
Optional |
event |
any
|
No
|
|
Private
deepCopy
|
deepCopy(object: any)
|
|
Deep copies a given object.
Parameters :
Name |
Type |
Optional |
Description |
object |
any
|
No
|
The object to be deep copied.
|
|
download
|
download()
|
|
Downloads a image of the confusion matrix.
IT IS NOT YET FULLY IMPLEMENTED. BETA ONLY!
|
dragEnter
|
dragEnter(index: number)
|
|
Parameters :
Name |
Type |
Optional |
index |
number
|
No
|
|
dragExist
|
dragExist(index: number)
|
|
Parameters :
Name |
Type |
Optional |
index |
number
|
No
|
|
dragstart
|
dragstart(from: number)
|
|
Parameters :
Name |
Type |
Optional |
from |
number
|
No
|
|
getColor
|
getColor(value: number)
|
|
Given a value, returns the color intensity associated with.
Parameters :
Name |
Type |
Optional |
value |
number
|
No
|
Color intensity in hexadecimal.
|
Private
getSquareSize
|
getSquareSize()
|
|
Gets the square size for each confusion matrix value.
|
getTranspose
|
getTranspose()
|
|
Returns : ConfusionMatrix
|
matrixValueChange
|
matrixValueChange(event: any, row: number, column: number)
|
|
|
ngAfterViewInit
|
ngAfterViewInit()
|
|
|
Private
onConfusionMatrixChange
|
onConfusionMatrixChange()
|
|
|
onDrop
|
onDrop(target: number)
|
|
Parameters :
Name |
Type |
Optional |
target |
number
|
No
|
|
removeLabel
|
removeLabel(name: string)
|
|
Parameters :
Name |
Type |
Optional |
name |
string
|
No
|
|
Private
updateZoomValue
|
updateZoomValue(zoom: number, throwExceptions)
|
|
Parameters :
Name |
Type |
Optional |
Default value |
zoom |
number
|
No
|
|
throwExceptions |
|
No
|
true
|
|
_confusionMatrix
|
Default value : new ConfusionMatrix()
|
|
Confusion matrix labels and values
|
_confusionMatrixTransposed
|
Default value : new ConfusionMatrix()
|
|
_levelsColor
|
Default value : new Array<string>()
|
|
Confusion matrix color level (a.k.a color intensity).
|
Private
_zoom
|
Type : number
|
Default value : 1
|
|
confusionMatrixElement
|
Type : ElementRef | undefined
|
Decorators :
@ViewChild('confusionMatrix')
|
|
Confusion matrix wrapper dom element reference.
|
dragging
|
Default value : false
|
|
dragHighlight
|
Default value : new Array<boolean>()
|
|
Private
dragIndex
|
Default value : -1
|
|
editionMode
|
Default value : false
|
|
Private
fullyInitialized
|
Default value : false
|
|
Private
numberOfItemsAdded
|
Type : number
|
Default value : 0
|
|
Private
originalHeight
|
Type : number
|
Default value : 0
|
|
Private
originalWidth
|
Type : number
|
Default value : 0
|
|
rows
|
Type : ElementRef | undefined
|
Decorators :
@ViewChild('rows')
|
|
Confusion matrix row dom element reference.
|
showConfigurationPanel
|
Default value : false
|
|
showMetricsPanel
|
Default value : false
|
|
showNormalizationConfiguration
|
Default value : false
|
|
showOption
|
Default value : false
|
|
Accessors
zoom
|
setzoom(zoom: number)
|
|
Represents the confusion matrix size.
Parameters :
Name |
Type |
Optional |
zoom |
number
|
No
|
|
confusionMatrix
|
getconfusionMatrix()
|
|
setconfusionMatrix(value)
|
|
Sets the confusion matrix labels and values.
|
intensityHeight
|
getintensityHeight()
|
|
Gets the intensity bar height.
|
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
import { ConfusionMatrix } from '@fullexpression/confusion-matrix-stats';
import { DecimalPipe } from '@angular/common';
import { animate, style, transition, trigger } from '@angular/animations';
import { ConfigurationsOption } from './configurations/configurations.component.model';
import * as html2canvas from "html2canvas";
import { confusionMatrixAnimations } from './confusion-matrix.animations';
import { DownloadService } from '../services/download.service';
import { ImportService } from '../services/import.service';
import { IntensityBarService } from './intensity-bar/intensity-bar.service';
/**
* Component which helps to visualize a confusion matrix.
* As a set o function allowing some level of visual configuration.
*/
@Component({
selector: 'confusion-matrix',
templateUrl: './confusion-matrix.component.html',
styleUrls: ['./confusion-matrix.component.scss'],
animations: confusionMatrixAnimations
})
export class ConfusionMatrixComponent implements AfterViewInit {
/**
* Sets the confusion matrix title.
* If undefined, the title reserved space will be hidden.
*/
@Input()
title = "";
@Output()
titleChange = new EventEmitter<string>();
/**
* Represents the confusion matrix size.
*/
@Input()
set zoom(zoom: number) {
this.updateZoomValue(zoom, false);
}
@Output()
zoomChange = new EventEmitter<number>();
/**
* Sets the confusion matrix color level (a.k.a color intensity).
* Should be order asc from less intense (close to 0) to max intense (close to max value).
*/
@Input()
levelsColors = new Array<string>();
@Output()
levelsColorsChange = new EventEmitter<Array<string>>()
/**
* Sets the confusion matrix labels and values.
*/
@Input()
set confusionMatrix(value: ConfusionMatrix) {
this._confusionMatrix = value.clone();
this._confusionMatrixTransposed = this._confusionMatrix.clone();
this._confusionMatrixTransposed.transpose();
this.dragHighlight = new Array();
}
get confusionMatrix(): ConfusionMatrix {
return this._confusionMatrix;
}
@Output()
confusionMatrixChange = new EventEmitter<ConfusionMatrix>()
/**
* Allows to define the numbers display format.
* If follows the angular decimal pipes rules:
* https://angular.io/api/common/DecimalPipe
*/
@Input()
roundRules = '1.0-2';
/**
* Confusion matrix row dom element reference.
*/
@ViewChild('rows') rows: ElementRef | undefined;
/**
* Confusion matrix wrapper dom element reference.
*/
@ViewChild('confusionMatrix') confusionMatrixElement: ElementRef | undefined;
/**
* Gets the intensity bar height.
* @return Gets the intensity height in pixels.
*/
get intensityHeight(): number {
return this.getSquareSize() * this._confusionMatrix.matrix.length;
}
get scale(): string {
return `scale(${this._zoom})`
}
/**
* Confusion matrix color level (a.k.a color intensity).
*/
_levelsColor = new Array<string>();
/**
* Confusion matrix labels and values
*/
_confusionMatrix = new ConfusionMatrix();
showOption = false;
showConfigurationPanel = false;
editionMode = false;
dragging = false;
showMetricsPanel = false;
showNormalizationConfiguration = false;
_confusionMatrixTransposed = new ConfusionMatrix();
private originalWidth = 0;
private originalHeight = 0;
private _zoom = 1;
private fullyInitialized = false;
private numberOfItemsAdded = 0;
private dragIndex = -1;
dragHighlight = new Array<boolean>();
/**
* Constructs the confusion matrix.
* @decimalPipe Decimal angular service injected using dependency injection.
*/
constructor(private decimalPipe: DecimalPipe,
private host: ElementRef,
private downloadService: DownloadService,
private importService: ImportService,
private intensityBarService: IntensityBarService,
private detectChanges: ChangeDetectorRef) {
this.confusionMatrixChange.subscribe(() => {
new Array(this._confusionMatrix.labels.length);
});
}
ngAfterViewInit(): void {
this.fullyInitialized = true;
this.originalWidth = this.host.nativeElement.clientWidth;
this.originalHeight = this.host.nativeElement.clientHeight;
this.updateZoomValue(this._zoom);
this.detectChanges.detectChanges();
}
/**
* Given a value, returns the color intensity associated with.
* @return Color intensity in hexadecimal.
*/
getColor(value: number): string {
return this.intensityBarService.getColor(value);
}
getTranspose(): ConfusionMatrix {
const clone = this._confusionMatrix.clone();
clone.transpose();
return clone;
}
/**
* Downloads a image of the confusion matrix.
* IT IS NOT YET FULLY IMPLEMENTED. BETA ONLY!
*/
download() {
(html2canvas as any)(this.confusionMatrixElement?.nativeElement).then((canvas: any) => {
const link = document.createElement('a');
link.download = 'confusion-matrix.png';
link.href = canvas.toDataURL()
link.click();
link.remove();
});
}
optionChanged(option: ConfigurationsOption) {
switch (option) {
case ConfigurationsOption.ZoomIn:
this.zoomIn();
break;
case ConfigurationsOption.Download:
this.download();
break;
case ConfigurationsOption.ZoomOut:
this.zoomOut();
break;
case ConfigurationsOption.Transpose:
this.transpose();
break;
case ConfigurationsOption.View:
this.editionMode = false;
this.updateZoomValue(this._zoom);
break;
case ConfigurationsOption.Edit:
this.editionMode = true;
this.updateZoomValue(this._zoom);
break;
case ConfigurationsOption.Save:
this.save();
break;
case ConfigurationsOption.Import:
this.import();
break;
case ConfigurationsOption.Normalization:
this.showNormalizationConfiguration = true;
break;
}
}
zoomIn() {
this._zoom += 0.1;
this.zoomChange.emit(this._zoom);
}
zoomOut() {
this._zoom -= 0.1;
this.zoomChange.emit(this._zoom);
}
matrixValueChange(event: any, row: number, column: number) {
const value = parseInt(event.target.innerText);
if (!isNaN(value)) {
this._confusionMatrix.matrix[row][column] = value;
this._confusionMatrix.matrix = this._confusionMatrix.matrix;
this.confusionMatrixChange.emit(this._confusionMatrix);
} else {
event.target.value = this._confusionMatrix.matrix[row][column];
}
}
calculateInputSize(event: any) {
const size = event.target.value.length * 2;
event.target.style.width = `${size}px`;
}
changeLabel(event: any, index: number) {
this._confusionMatrix.labels[index] = event.target.innerText ?? this._confusionMatrix.labels[index];
this.confusionMatrixChange.emit(this._confusionMatrix);
}
changeTitle(event: any) {
this.title = event.target.innerText ?? this.title;
this.titleChange.emit(this.title);
}
transpose() {
this._confusionMatrix.transpose();
this.confusionMatrixChange.emit(this._confusionMatrix);
}
undo() {
this._confusionMatrix.undo();
this.confusionMatrixChange.emit(this._confusionMatrix);
}
redo() {
this._confusionMatrix.redo();
this.confusionMatrixChange.emit(this._confusionMatrix);
}
// This function will delete a given label
removeLabel(name: string) {
this._confusionMatrix.removeLabel(name);
this.confusionMatrixChange.emit(this._confusionMatrix);
this.originalWidth -= 40;
this.originalHeight -= 40;
this.updateZoomValue(this._zoom);
}
add(index: number) {
const emptyArray = new Array<number>(this._confusionMatrix.matrix.length + 1);
emptyArray.fill(0, 0, this._confusionMatrix.matrix.length + 1);
let name = 'Untitled';
if (this.numberOfItemsAdded > 0) {
name = `${name}-${this.numberOfItemsAdded}`;
}
this._confusionMatrix.addLabel(name, emptyArray, emptyArray, index);
++this.numberOfItemsAdded;
this.confusionMatrixChange.emit(this._confusionMatrix);
this.originalWidth += 40;
this.originalHeight += 40;
this.updateZoomValue(this._zoom);
}
dragstart(from: number) {
this.dragIndex = from;
this.dragging = true;
}
onDrop(target: number) {
this._confusionMatrix.changeLabelOrder(this.dragIndex, target);
this.confusionMatrixChange.emit(this._confusionMatrix);
}
allowDrop(event: any) {
event.preventDefault();
return false;
}
allowDrag() {
return !this.editionMode;
}
dragEnter(index: number) {
this.dragHighlight[index] = true;
}
dragExist(index: number) {
this.dragHighlight[index] = false;
}
save() {
this.downloadService.download(this._confusionMatrix.convertToJson(),
'confusion-matrix.json');
}
async import() {
const json = await this.importService.import();
if (json) {
this._confusionMatrix.importAsJson(json);
this.confusionMatrixChange.emit(this._confusionMatrix);
}
}
private onConfusionMatrixChange() {
this.dragHighlight = new Array(this._confusionMatrix.labels.length);
}
private updateZoomValue(zoom: number, throwExceptions = true) {
if (zoom < 0.2) {
if (!throwExceptions) return;
throw "Zoom can not be less then 0.2";
}
this._zoom = zoom;
if (this.fullyInitialized) {
const width = this.originalWidth + (this.editionMode ? 45 : 0);
const height = this.originalHeight + (this.editionMode ? 45 : 0)
this.host.nativeElement.style.width = `${width * this._zoom}px`;
this.host.nativeElement.style.height = `${height * this._zoom}px`;
}
}
/**
* Gets the square size for each confusion matrix value.
* @returns The square size.
*/
private getSquareSize(): number {
const _rows = this.rows?.nativeElement;
if (_rows) {
const row = _rows.getElementsByClassName('row')[0];
if (row) {
return row.clientWidth;
}
}
return 0;
}
/**
* Deep copies a given object.
* @param object The object to be deep copied.
* @returns The deep copied object.
*/
private deepCopy(object: any): any {
return JSON.parse(JSON.stringify(object));
}
}
<div class="confusion-matrix-wrapper" *ngIf="_confusionMatrix" #confusionMatrix [style.transform]="scale"
[@inOutAnimation]>
<div class="title">
<div>
<span type='text' (focusout)="changeTitle($event);" [attr.contenteditable]="editionMode">{{title}}</span>
</div>
</div>
<div class="confusion-matrix" [class.dragging]="dragging" [class.drag-enable]="allowDrag()">
<div class="rows-label">
<div class="row-label" *ngFor="let label of _confusionMatrix.labels; let i = index"
[class.dragging]='dragHighlight[i]'>
<span class="text" type='text' (focusout)="changeLabel($event, i);"
[attr.contenteditable]="editionMode">{{label}}</span>
<add-button *ngIf="editionMode" class='add' (click)="add(i)" [@rowsAddDeleteAnimation]>➕</add-button>
<remove-button *ngIf="editionMode" class="remove" (click)="removeLabel(_confusionMatrix.labels[i])"
[@rowsAddDeleteAnimation]>❌</remove-button>
</div>
</div>
<div class="rows" #rows>
<div class="row" [attr.draggable]="allowDrag()"
*ngFor="let label of _confusionMatrix.labels; let rowIndex = index" [@removeAddColumns]
(dragstart)="dragstart(rowIndex)" (drop)="onDrop(rowIndex)" (dragover)="allowDrop($event)"
(dragenter)="dragEnter(rowIndex)" (dragleave)="dragExist(rowIndex)"
[class.dragging]='dragHighlight[rowIndex]' (dragend)='dragging = false'>
<div class="columns">
<div class="column" [style.background-color]="getColor(column)"
*ngFor="let column of _confusionMatrixTransposed.matrix[rowIndex]; let columnIndex = index"
[@removeAddLine] [class.dragging]='dragHighlight[columnIndex]'>
<div class='column-value'>
<span type='text' (focusout)="matrixValueChange($event, columnIndex , rowIndex);"
[attr.contenteditable]="editionMode">{{column | number:
roundRules}}</span>
</div>
</div>
<div class="row-label">
<add-button class='add' *ngIf="editionMode" class='add' (click)="add(rowIndex)"
[@columnsAddDeleteAnimation]></add-button>
<remove-button *ngIf="editionMode" class="remove"
(click)="removeLabel(_confusionMatrix.labels[rowIndex])" [@columnsAddDeleteAnimation]>❌
</remove-button>
<span class="text" type='text' (focusout)="changeLabel($event, rowIndex);"
[attr.contenteditable]="editionMode">{{label}}</span>
</div>
</div>
</div>
</div>
<intensity-bar [confusionMatrix]="_confusionMatrix" [levelsColors]="levelsColors"
[intensityHeight]="intensityHeight">
</intensity-bar>
<div class="tools" (click)="showConfigurationPanel = true;" [class.edition-mode]="editionMode">
⚙️ Tools
</div>
<div class="undo-redo" [class.edition-mode]="editionMode">
<span class="redo" [class.available]="_confusionMatrix.isUndoAvailable()" (click)="undo()">↪️</span>
<span class="undo" [class.available]="_confusionMatrix.isRedoAvailable()" (click)="redo()">↩️</span>
</div>
</div>
</div>
<metrics-panel [confusionMatrix]="_confusionMatrix" [(visible)]="showMetricsPanel"></metrics-panel>
<configurations [(visible)]="showConfigurationPanel" (optionChange)="optionChanged($event)"
[editionToggle]="editionMode" [(metricsToggle)]="showMetricsPanel">
</configurations>
<normalize [(visible)]="showNormalizationConfiguration" [(confusionMatrix)]="confusionMatrix"></normalize>
:host {
display: flex;
transition: all 0.2s ease;
justify-content: center;
align-items: center;
margin: 20px;
flex-direction: column;
position: relative;
.confusion-matrix-wrapper {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
transition: transform 0.2s ease;
span {
background-color: transparent;
width: 100%;
height: 100%;
border: none;
font-family: Arial, Helvetica, sans-serif;
display: flex;
justify-content: center;
align-items: center;
cursor: default;
&:focus {
outline: none;
}
&[contenteditable='true'] {
cursor: text;
}
}
.title {
display: flex;
flex-direction: column;
margin-bottom: 20px;
padding: 4px;
padding-left: 20px;
padding-right: 20px;
border: 1px solid #f7f7f7;
border-radius: 100px;
background-color: #ffffffc2;
box-shadow: rgb(0 0 0 / 41%) 1px 1px 4px -2px;
.title-edit {
display: flex;
width: 100%;
height: 100%;
}
}
.confusion-matrix {
display: flex;
justify-content: center;
position: relative;
&.dragging {
.column {
opacity: 0.3;
filter: grayscale(100%);
}
}
&.drag-enable {
.column span {
cursor: move;
}
}
.rows-label {
display: flex;
flex-direction: column;
margin-right: 10px;
.row-label {
height: 40px;
display: flex;
align-items: center;
&.dragging {
.row-label {
transform: scale(1.1);
}
}
.text {
justify-content: flex-end;
}
add-button {
margin-left: 8px;
}
remove-button {
margin-left: 4px;
}
}
}
.rows {
display: flex;
.row {
&.dragging {
.column {
opacity: 1;
filter: grayscale(0%);
}
.row-label {
opacity: 1;
filter: grayscale(0%);
}
}
.columns {
.column {
&.dragging {
opacity: 1;
filter: grayscale(0%);
}
transition: all ease 0.2s;
.column-value {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
.column-edit {
display: flex;
width: 100%;
height: 100%;
}
transition: background-color 0.2s ease;
}
}
.add-row {
height: 4px;
width: 100%;
background-color: #9e9e9e;
}
.row-label {
writing-mode: vertical-rl;
margin-top: 10px;
width: 100%;
text-align: center;
justify-content: center;
align-items: center;
display: flex;
add-button {
margin-bottom: 4px;
}
remove-button {
margin-bottom: 8px;
}
}
}
}
}
.tools {
margin-left: 3px;
cursor: pointer;
position: absolute;
bottom: 40px;
right: 0;
transition: 0.1s ease;
&.edition-mode {
bottom: 85px;
transition: 0.3s ease;
}
&:hover {
transform: scale(1.2);
}
}
.undo-redo {
margin-left: 22px;
position: absolute;
bottom: 44px;
left: 0;
transition: 0.1s ease;
flex-direction: column;
flex-direction: row;
display: flex;
font-size: 20px;
cursor: default;
&.edition-mode {
margin-left: 70px;
bottom: 83px;
transition: 0.3s ease;
}
span {
transform: rotate(180deg);
transition: 0.1s ease;
filter: grayscale(100%);
opacity: 0.3;
&.available {
opacity: 1;
filter: none;
cursor: pointer;
&:hover {
transform: rotate(180deg) scale(1.2);
}
}
}
}
}
}
}
Legend
Html element with directive