File

src/lib/components/confusion-matrix.component.ts

Description

Component which helps to visualize a confusion matrix. As a set o function allowing some level of visual configuration.

Implements

AfterViewInit

Metadata

selector confusion-matrix
styleUrls ./confusion-matrix.component.scss
templateUrl ./confusion-matrix.component.html

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor(decimalPipe: DecimalPipe, host: ElementRef, downloadService: DownloadService, importService: ImportService, intensityBarService: IntensityBarService, detectChanges: ChangeDetectorRef)

Constructs the confusion matrix.

Parameters :
Name Type Optional
decimalPipe DecimalPipe No
host ElementRef No
downloadService DownloadService No
importService ImportService No
intensityBarService IntensityBarService No
detectChanges ChangeDetectorRef No

Inputs

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'

Allows to define the numbers display format. If follows the angular decimal pipes rules: https://angular.io/api/common/DecimalPipe

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.

Outputs

confusionMatrixChange
Type : EventEmitter
levelsColorsChange
Type : EventEmitter
titleChange
Type : EventEmitter
zoomChange
Type : EventEmitter

Methods

add
add(index: number)
Parameters :
Name Type Optional
index number No
Returns : void
allowDrag
allowDrag()
Returns : boolean
allowDrop
allowDrop(event: any)
Parameters :
Name Type Optional
event any No
Returns : boolean
calculateInputSize
calculateInputSize(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
changeLabel
changeLabel(event: any, index: number)
Parameters :
Name Type Optional
event any No
index number No
Returns : void
changeTitle
changeTitle(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
Private deepCopy
deepCopy(object: any)

Deep copies a given object.

Parameters :
Name Type Optional Description
object any No

The object to be deep copied.

Returns : any

The deep copied object.

download
download()

Downloads a image of the confusion matrix. IT IS NOT YET FULLY IMPLEMENTED. BETA ONLY!

Returns : void
dragEnter
dragEnter(index: number)
Parameters :
Name Type Optional
index number No
Returns : void
dragExist
dragExist(index: number)
Parameters :
Name Type Optional
index number No
Returns : void
dragstart
dragstart(from: number)
Parameters :
Name Type Optional
from number No
Returns : void
getColor
getColor(value: number)

Given a value, returns the color intensity associated with.

Parameters :
Name Type Optional
value number No
Returns : string

Color intensity in hexadecimal.

Private getSquareSize
getSquareSize()

Gets the square size for each confusion matrix value.

Returns : number

The square size.

getTranspose
getTranspose()
Returns : ConfusionMatrix
Async import
import()
Returns : any
matrixValueChange
matrixValueChange(event: any, row: number, column: number)
Parameters :
Name Type Optional
event any No
row number No
column number No
Returns : void
ngAfterViewInit
ngAfterViewInit()
Returns : void
Private onConfusionMatrixChange
onConfusionMatrixChange()
Returns : void
onDrop
onDrop(target: number)
Parameters :
Name Type Optional
target number No
Returns : void
optionChanged
optionChanged(option: ConfigurationsOption)
Parameters :
Name Type Optional
option ConfigurationsOption No
Returns : void
redo
redo()
Returns : void
removeLabel
removeLabel(name: string)
Parameters :
Name Type Optional
name string No
Returns : void
save
save()
Returns : void
transpose
transpose()
Returns : void
undo
undo()
Returns : void
Private updateZoomValue
updateZoomValue(zoom: number, throwExceptions)
Parameters :
Name Type Optional Default value
zoom number No
throwExceptions No true
Returns : void
zoomIn
zoomIn()
Returns : void
zoomOut
zoomOut()
Returns : void

Properties

_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
Returns : void
confusionMatrix
getconfusionMatrix()
setconfusionMatrix(value)

Sets the confusion matrix labels and values.

Parameters :
Name Optional
value No
Returns : void
intensityHeight
getintensityHeight()

Gets the intensity bar height.

Returns : number
scale
getscale()
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>

./confusion-matrix.component.scss

: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
Component
Html element with directive

result-matching ""

    No results matching ""