lib/components/plot/plot.component.ts
Wrapper for a Plotly plot using angular-ploty.js
Handles setting the plot height based on width and wanted aspect ratio.
See https://plotly.com/javascript/ See https://github.com/plotly/angular-plotly.js
encapsulation | ViewEncapsulation.None |
selector | nshmp-plot |
imports |
NgClass
PlotlyViaCDNModule
NgxSkeletonLoaderModule
|
templateUrl | ./plot.component.html |
styleUrl | ./plot.component.scss |
Properties |
|
Methods |
|
constructor()
|
Defined in lib/components/plot/plot.component.ts:112
|
Private addLegendSnackBar |
addLegendSnackBar()
|
Defined in lib/components/plot/plot.component.ts:239
|
Returns :
void
|
Private createMobileData | ||||||||
createMobileData(plotData: Partial
|
||||||||
Defined in lib/components/plot/plot.component.ts:263
|
||||||||
Changes the plot data to better be shown on mobiles screens.
Parameters :
Returns :
Partial[]
|
onLegendClick | ||||||||||||
onLegendClick(curveIndex: number, allowEmit)
|
||||||||||||
Defined in lib/components/plot/plot.component.ts:122
|
||||||||||||
Parameters :
Returns :
void
|
onPlotInitialized |
onPlotInitialized()
|
Defined in lib/components/plot/plot.component.ts:188
|
Returns :
void
|
Private onSelectLegendUid | ||||||||||||
onSelectLegendUid(uid: string, allowEmit)
|
||||||||||||
Defined in lib/components/plot/plot.component.ts:279
|
||||||||||||
Parameters :
Returns :
void
|
onUpdate |
onUpdate()
|
Defined in lib/components/plot/plot.component.ts:205
|
Handle update event.
Returns :
void
|
Private redrawPlot |
redrawPlot()
|
Defined in lib/components/plot/plot.component.ts:291
|
Redraw the plot by updating the revision value.
Returns :
void
|
Private removeMinorAxisTicks | ||||||||
removeMinorAxisTicks(ticks: SVGTextElement[])
|
||||||||
Defined in lib/components/plot/plot.component.ts:305
|
||||||||
Remove minor ticks on mobile devices for single axis.
Parameters :
Returns :
void
|
Private removeMinorTicks |
removeMinorTicks()
|
Defined in lib/components/plot/plot.component.ts:316
|
Remove minor ticks.
Returns :
void
|
Private setColorScheme |
setColorScheme()
|
Defined in lib/components/plot/plot.component.ts:332
|
Sets the color scheme of the plot based on application scheme.
Returns :
void
|
Private updateDimensions |
updateDimensions()
|
Defined in lib/components/plot/plot.component.ts:403
|
Update the height of the plot based on the width and aspect ratio.
Returns :
void
|
updatePlot |
updatePlot()
|
Defined in lib/components/plot/plot.component.ts:213
|
Update the plot based on screen size.
Returns :
void
|
config |
Type : Partial<PlotlyConfig>
|
Default value : {}
|
Defined in lib/components/plot/plot.component.ts:65
|
Plot config, changes based on screen size |
data |
Type : Partial<PlotData>[]
|
Default value : []
|
Defined in lib/components/plot/plot.component.ts:67
|
Plot data, changes based on screen size |
Private dataAdded |
Type : boolean
|
Defined in lib/components/plot/plot.component.ts:56
|
Private dataClicked |
Type : Partial<PlotData>
|
Default value : {}
|
Defined in lib/components/plot/plot.component.ts:53
|
Private destroyRef |
Default value : inject(DestroyRef)
|
Defined in lib/components/plot/plot.component.ts:51
|
Private headerService |
Default value : inject(HeaderService)
|
Defined in lib/components/plot/plot.component.ts:48
|
isSmallScreen |
Default value : false
|
Defined in lib/components/plot/plot.component.ts:69
|
Whether it is small screen, based on panel breakpoint |
layout |
Type : Partial<PlotlyLayout>
|
Default value : {}
|
Defined in lib/components/plot/plot.component.ts:71
|
Plotly layout, changes based on screen size |
legendClick |
Default value : output<Partial<PlotData>>()
|
Defined in lib/components/plot/plot.component.ts:110
|
legendSelectUid |
Default value : input<string, string>('', {
transform: uid => {
this.onSelectLegendUid(uid);
return uid;
},
})
|
Defined in lib/components/plot/plot.component.ts:103
|
Private lineWidth |
Type : number
|
Default value : 2
|
Defined in lib/components/plot/plot.component.ts:55
|
Private markerSize |
Type : number
|
Default value : 6
|
Defined in lib/components/plot/plot.component.ts:54
|
mobileData |
Type : Partial<PlotData>[]
|
Default value : []
|
Defined in lib/components/plot/plot.component.ts:73
|
Data for mobile screens |
Readonly nshmpPlotlyPlotEl |
Default value : viewChild.required<ElementRef<HTMLElement>>('nshmpPlotlyPlot')
|
Defined in lib/components/plot/plot.component.ts:75
|
Wrapper element around plotly-plot |
Private nshmpTemplateService |
Default value : inject(NshmpTemplateService)
|
Defined in lib/components/plot/plot.component.ts:47
|
panelBreakpoint |
Type : number
|
Default value : 660
|
Defined in lib/components/plot/plot.component.ts:77
|
Panel breakpoint for small screen |
Readonly plot |
Default value : input.required<PlotlyPlot, PlotlyPlot>({
transform: plot => {
this.updateDimensions();
this.showSkeleton = true;
this.data = plot.data;
this.mobileData = this.createMobileData(this.data);
this.layout = plot.layout;
this.config = plot.config;
this.dataAdded = false;
this.dataClicked = {};
this.selectedOptions = {
...plot.lineSelected,
...this.selectedOptions,
};
this.onSelectLegendUid(this.legendSelectUid(), false);
return plot;
},
})
|
Defined in lib/components/plot/plot.component.ts:84
|
Set the Plotly plot data and options. |
Readonly plotlyComponent |
Default value : viewChild.required<PlotlyComponent>('plotlyComponent')
|
Defined in lib/components/plot/plot.component.ts:79
|
The ploty-plot componenet |
Private selectedOptions |
Type : LineSelected
|
Default value : {
lineWidth: 6,
lineWidthMobile: 2,
markerSize: 15,
markerSizeMobile: 6,
}
|
Defined in lib/components/plot/plot.component.ts:57
|
showSkeleton |
Default value : true
|
Defined in lib/components/plot/plot.component.ts:112
|
Private snackBar |
Default value : inject(MatSnackBar)
|
Defined in lib/components/plot/plot.component.ts:50
|
Private templateService |
Default value : inject(NshmpTemplateService)
|
Defined in lib/components/plot/plot.component.ts:49
|
import {NgClass} from '@angular/common';
import {
Component,
DestroyRef,
effect,
ElementRef,
inject,
input,
output,
viewChild,
ViewEncapsulation,
} from '@angular/core';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {MatSnackBar, MatSnackBarRef, TextOnlySnackBar} from '@angular/material/snack-bar';
import {HeaderService, NshmpTemplateService} from '@ghsc/nshmp-template';
import {
heightFromAspectRatio,
LineSelected,
PlotlyConfig,
PlotlyLayout,
PlotlyPlot,
} from '@ghsc/nshmp-utils-ts/libs/plotly';
import {PlotlyComponent, PlotlyViaCDNModule} from 'angular-plotly.js';
import {NgxSkeletonLoaderModule} from 'ngx-skeleton-loader';
import {DataTitle, PlotData} from 'plotly.js';
import {COLORWAY} from '../../utils';
PlotlyViaCDNModule.setPlotlyVersion('strict-3.0.1');
/**
* Wrapper for a Plotly plot using angular-ploty.js
*
* Handles setting the plot height based on width and wanted aspect ratio.
*
* @see https://plotly.com/javascript/
* @see https://github.com/plotly/angular-plotly.js
*/
@Component({
encapsulation: ViewEncapsulation.None,
imports: [NgClass, PlotlyViaCDNModule, NgxSkeletonLoaderModule],
selector: 'nshmp-plot',
styleUrl: './plot.component.scss',
templateUrl: './plot.component.html',
})
export class NshmpPlotComponent {
private nshmpTemplateService = inject(NshmpTemplateService);
private headerService = inject(HeaderService);
private templateService = inject(NshmpTemplateService);
private snackBar = inject(MatSnackBar);
private destroyRef = inject(DestroyRef);
private dataClicked: Partial<PlotData> = {};
private markerSize: number = 6;
private lineWidth: number = 2;
private dataAdded!: boolean;
private selectedOptions: LineSelected = {
lineWidth: 6,
lineWidthMobile: 2,
markerSize: 15,
markerSizeMobile: 6,
};
/** Plot config, changes based on screen size */
config: Partial<PlotlyConfig> = {};
/** Plot data, changes based on screen size */
data: Partial<PlotData>[] = [];
/** Whether it is small screen, based on panel breakpoint */
isSmallScreen = false;
/** Plotly layout, changes based on screen size */
layout: Partial<PlotlyLayout> = {};
/** Data for mobile screens */
mobileData: Partial<PlotData>[] = [];
/** Wrapper element around plotly-plot */
readonly nshmpPlotlyPlotEl = viewChild.required<ElementRef<HTMLElement>>('nshmpPlotlyPlot');
/** Panel breakpoint for small screen */
panelBreakpoint = 660;
/** The ploty-plot componenet */
readonly plotlyComponent = viewChild.required<PlotlyComponent>('plotlyComponent');
/**
* Set the Plotly plot data and options.
*/
readonly plot = input.required<PlotlyPlot, PlotlyPlot>({
transform: plot => {
this.updateDimensions();
this.showSkeleton = true;
this.data = plot.data;
this.mobileData = this.createMobileData(this.data);
this.layout = plot.layout;
this.config = plot.config;
this.dataAdded = false;
this.dataClicked = {};
this.selectedOptions = {
...plot.lineSelected,
...this.selectedOptions,
};
this.onSelectLegendUid(this.legendSelectUid(), false);
return plot;
},
});
legendSelectUid = input<string, string>('', {
transform: uid => {
this.onSelectLegendUid(uid);
return uid;
},
});
legendClick = output<Partial<PlotData>>();
showSkeleton = true;
constructor() {
effect(() => {
this.headerService.controlPanelSelected();
this.headerService.settingsSelected();
this.redrawPlot();
});
}
onLegendClick(curveIndex: number, allowEmit = true): void {
const data = this.data[curveIndex];
const lineWidthSelected = this.isSmallScreen
? this.selectedOptions.lineWidthMobile
: this.selectedOptions.lineWidth;
const markerSizeSelected = this.isSmallScreen
? this.selectedOptions.markerSizeMobile
: this.selectedOptions.markerSize;
if (data.line?.width !== this.selectedOptions.lineWidth) {
this.lineWidth = data.line?.width ?? 2;
}
if (data.marker?.size !== this.selectedOptions.markerSize) {
this.markerSize = (data.marker?.size as number) ?? 6;
}
if (this.dataAdded) {
this.data.pop();
}
this.data.forEach(plotData => {
plotData.line = {
...plotData.line,
width: this.lineWidth,
};
plotData.marker = {
...plotData.marker,
size: this.markerSize,
};
});
if (data === this.dataClicked) {
this.dataClicked = {};
this.dataAdded = false;
} else {
const color = data.line?.color ?? COLORWAY[curveIndex % COLORWAY.length];
data.line = {
...data.line,
color,
width: lineWidthSelected,
};
data.marker = {
...data.marker,
color,
size: markerSizeSelected,
};
const newData = {...data};
newData.showlegend = false;
this.data.push(newData);
this.dataClicked = data;
this.dataAdded = true;
if (allowEmit) {
this.legendClick.emit(data);
}
}
}
onPlotInitialized(): void {
this.nshmpTemplateService.isSmallScreen$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.redrawPlot());
this.templateService.colorSchemeChanged
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => this.setColorScheme());
this.updatePlot();
this.showSkeleton = false;
}
/**
* Handle update event.
*/
onUpdate(): void {
this.removeMinorTicks();
this.showSkeleton = false;
}
/**
* Update the plot based on screen size.
*/
updatePlot(): void {
const panelWidth = this.nshmpPlotlyPlotEl().nativeElement.getBoundingClientRect().width;
this.isSmallScreen = panelWidth <= (this.plot()?.panelBreakpoint ?? this.panelBreakpoint);
if (this.isSmallScreen) {
this.data = this.mobileData;
this.layout = this.plot().mobileLayout ?? this.plot().layout;
this.config = this.plot().mobileConfig ?? this.plot().config;
} else {
this.layout = this.plot().layout;
this.config = this.plot().config;
this.data = this.plot().data;
}
this.setColorScheme();
this.updateDimensions();
setTimeout(() => {
this.removeMinorTicks();
if (!this.isSmallScreen) {
this.addLegendSnackBar();
}
}, 0);
}
private addLegendSnackBar(): void {
let snackRef: MatSnackBarRef<TextOnlySnackBar> | undefined = undefined;
const plotEl = this.plotlyComponent().plotEl.nativeElement as HTMLElement;
plotEl
.querySelector('g.legend')
?.querySelectorAll('.traces')
.forEach(traceEl => {
traceEl.addEventListener('mouseover', () => {
snackRef = this.snackBar.open('Click on legend entry to highlight', '', {
duration: 3000,
});
});
traceEl.addEventListener('mouseout', () => snackRef?.dismiss());
});
}
/**
* Changes the plot data to better be shown on mobiles screens.
*
* @param plotData The plot data
*/
private createMobileData(plotData: Partial<PlotData>[]): Partial<PlotData>[] {
return [...plotData].map(data => {
return {
...data,
line: {
...data.line,
width: this.selectedOptions.lineWidthMobile!,
},
marker: {
...data.marker,
size: this.selectedOptions.markerSizeMobile!,
},
};
});
}
private onSelectLegendUid(uid: string, allowEmit = true): void {
const curveIndex = this.data.findIndex(plotData => plotData.uid === uid);
if (curveIndex !== -1) {
this.dataClicked = {};
this.onLegendClick(curveIndex, allowEmit);
}
}
/**
* Redraw the plot by updating the revision value.
*/
private redrawPlot(): void {
if (this.plotlyComponent().plotlyInstance) {
setTimeout(() => {
this.plot().config.revision = Date.now();
this.updatePlot();
});
}
}
/**
* Remove minor ticks on mobile devices for single axis.
*
* @param ticks The tick elements
*/
private removeMinorAxisTicks(ticks: SVGTextElement[]): void {
const maxFontSize = Math.max(...ticks.map(tick => Number.parseInt(tick.style.fontSize)));
ticks
.filter(tick => Number.parseInt(tick.style.fontSize) < maxFontSize)
.forEach(tick => (tick.style.display = 'none'));
}
/**
* Remove minor ticks.
*/
private removeMinorTicks(): void {
const plotEl: HTMLElement = (this.plotlyComponent().plotEl as ElementRef<HTMLElement>)
.nativeElement;
const svgEl: SVGElement | null = plotEl.querySelector('svg.main-svg');
const xticks: NodeListOf<SVGTextElement> | undefined = svgEl?.querySelectorAll('g.xtick text');
const yticks: NodeListOf<SVGTextElement> | undefined = svgEl?.querySelectorAll('g.ytick text');
this.removeMinorAxisTicks(Array.from(xticks ?? []));
this.removeMinorAxisTicks(Array.from(yticks ?? []));
}
/**
* Sets the color scheme of the plot based on application scheme.
*/
private setColorScheme(): void {
const black = 'black';
const root = document.documentElement;
const backgroundColors = getComputedStyle(root)
.getPropertyValue('--mat-sys-surface-container-high')
.replace('light-dark(', '')
.replace(')', '')
.split(',');
const lightBackgroundColor = backgroundColors.shift() ?? '#f4f3f7';
const darkBackgroundColor = backgroundColors.pop() ?? '#1a1c1e';
if (this.templateService.isDarkMode) {
const fontColor = '#eeeeee';
this.layout.plot_bgcolor = darkBackgroundColor;
this.layout.paper_bgcolor = darkBackgroundColor;
if (this.data) {
this.data.forEach(data => {
data.textfont = {
color: fontColor,
};
});
}
if (this.layout) {
if (this.layout.xaxis) {
this.layout.xaxis.color = fontColor;
}
if (this.layout.yaxis) {
this.layout.yaxis.color = fontColor;
}
if (this.layout.legend?.font) {
this.layout.legend.font.color = fontColor;
}
}
(this.layout.title as DataTitle).font.color = fontColor;
} else {
this.layout.plot_bgcolor = lightBackgroundColor;
this.layout.paper_bgcolor = lightBackgroundColor;
if (this.data) {
this.data.forEach(data => {
data.textfont = {
color: black,
};
});
}
if (this.layout) {
if (this.layout.xaxis) {
this.layout.xaxis.color = black;
}
if (this.layout.yaxis) {
this.layout.yaxis.color = black;
}
if (this.layout.legend?.font) {
this.layout.legend.font.color = black;
}
}
(this.layout.title as DataTitle).font.color = black;
}
}
/**
* Update the height of the plot based on the width and aspect ratio.
*/
private updateDimensions(): void {
const aspectRatio = this.layout?.aspectRatio ?? '16:9';
const nshmpEl = this.nshmpPlotlyPlotEl().nativeElement;
const plotEl: HTMLElement = (this.plotlyComponent().plotEl as ElementRef<HTMLElement>)
.nativeElement;
const legendEl: SVGGElement | null | undefined = plotEl
.querySelector('.legend')
?.querySelector('rect.bg');
const layout = this.isSmallScreen ? this.plot().mobileLayout : this.layout;
let height = heightFromAspectRatio(nshmpEl, aspectRatio);
if (legendEl) {
const legendHeight =
(layout?.legend?.y ?? 1 <= 0) ? legendEl?.getBoundingClientRect()?.height : 0;
height = this.isSmallScreen && legendHeight ? height + legendHeight : height;
}
nshmpEl.style.height = `${height}px`;
}
}
<div>
<div #nshmpPlotlyPlot class="nshmp-plotly-plot" [ngClass]="{'small-screen': isSmallScreen}">
<div class="skeleton-container" [ngClass]="{hide: !showSkeleton}">
<ngx-skeleton-loader class="skeleton-title" />
<div class="skeleton grid-row">
<div class="skeleton-border" #skeleton>
<ngx-skeleton-loader class="skeleton-line line-1" />
<ngx-skeleton-loader class="skeleton-line line-2" />
</div>
<div class="skeleton-legend">
<ngx-skeleton-loader class="entry" />
<ngx-skeleton-loader class="entry" />
</div>
</div>
</div>
<plotly-plot
#plotlyComponent
[config]="config"
[className]="config.className"
[data]="data"
[debug]="config.debug ?? false"
[divId]="plot().id"
[layout]="layout"
[revision]="config.revision ?? 1"
[style]="config.style"
[updateOnLayoutChange]="config.updateOnLayoutChange ?? true"
[updateOnDataChange]="config.updateOnDataChange ?? true"
[useResizeHandler]="true"
(autoSize)="updatePlot()"
(initialized)="onPlotInitialized()"
(legendClick)="onLegendClick($event.curveNumber)"
(update)="onUpdate()"
/>
</div>
</div>