import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms';
import {TableData} from '@ghsc/nshmp-lib-ng/nshmp';
import {PlotlyConfig, PlotlyLayout, PlotlyPlot} from '@ghsc/nshmp-utils-ts/libs/plotly';
import * as d3Color from 'd3-scale-chromatic';
import {AxisType, DataTitle, LayoutAxis} from 'plotly.js';
import {PlotTableDataParams} from '../models';
import {DefaultPlotOptions, PlotOptions} from '../models/default-plot-options.model';
import {
LayoutSettings,
LegendSettings,
NshmpPlot,
NshmpPlotSettingFormGroup,
NshmpPlotSettings,
PlotlyConfigFormGroup,
} from '../models/nshmp-plot.model';
export const COLORWAY = [...d3Color.schemeTableau10];
const plotTableDataParams: PlotTableDataParams = {
addLabel: false,
hideX: false,
hideY: false,
labelTransform: label => label,
xLabelTransform: label => label,
xValueFormat: x => x,
yLabelTransform: label => label,
yValueFormat: y => y,
};
export interface UpdatePlotLabelsProps {
layout: Partial<PlotlyLayout>;
xLabel: string;
yLabel: string;
title?: string;
}
export function defaultPlot(plotOptions: DefaultPlotOptions): PlotlyPlot {
return {
config: {...DEFAULT_PLOTLY_CONFIG, ...plotOptions?.options?.config},
data: [],
id: plotOptions.id,
layout: {
...createLayout(
plotOptions,
plotOptions.options ?? {},
DEFAULT_PLOTLY_LAYOUT,
DEFAULT_PLOTLY_AXIS,
),
},
mobileConfig: {
...DEFAULT_PLOTLY_CONFIG,
...plotOptions?.mobileOptions?.config,
},
mobileLayout: {
...createLayout(
plotOptions,
plotOptions.mobileOptions ?? {},
DEFAULT_PLOTLY_MOBILE_LAYOUT,
DEFAULT_PLOTLY_MOBILE_AXIS,
),
},
panelBreakpoint: plotOptions.panelBreakpoint,
};
}
export function plotlyLayoutToSettings(layout: Partial<PlotlyLayout>): LayoutSettings {
const {aspectRatio, legend, xaxis, yaxis} = layout;
const title = layout.title as DataTitle;
const xTitle = xaxis?.title as DataTitle;
const yTitle = yaxis?.title as DataTitle;
return {
aspectRatio: aspectRatio ?? '16:9',
legend: {
showlegend: layout.showlegend ?? true,
x: legend?.x ?? 0,
y: legend?.y ?? 0,
},
title: {
size: title.font.size ?? 18,
text: title.text,
},
xaxis: {
nticks: xaxis?.nticks ?? 10,
title: {
size: xTitle.font.size ?? 12,
text: xTitle.text,
},
type: xaxis?.type ?? 'linear',
},
yaxis: {
nticks: yaxis?.nticks ?? 10,
title: {
size: yTitle.font.size ?? 10,
text: yTitle.text,
},
type: yaxis?.type ?? 'linear',
},
};
}
export function plotDataToTableData<T extends (string | number)[]>(
plot: NshmpPlot,
params: PlotTableDataParams = plotTableDataParams,
): TableData[] {
if (plot === null) {
return [];
}
params = {
...plotTableDataParams,
...params,
};
const tableData: TableData[] = [];
plot.plotData.data.forEach(data => {
const xTitle = (plot.plotData.layout.xaxis?.title as DataTitle)?.text ?? '';
const yTitle = (plot.plotData.layout.yaxis?.title as DataTitle)?.text ?? '';
const xData: TableData = {
td: params.xValueFormat ? (data.x as T).map(params.xValueFormat) : (data.x as T),
th: params.xLabelTransform ? params.xLabelTransform(xTitle, data) : xTitle,
};
if (params.addLabel) {
xData.label = params.labelTransform ? params.labelTransform(data.name ?? '') : data.name;
}
const yData: TableData = {
td: params.yValueFormat ? (data.y as T).map(params.yValueFormat) : (data.y as T),
th: params.yLabelTransform ? params.yLabelTransform(yTitle, data) : yTitle,
};
if (params.hideX) {
tableData.push(yData);
} else if (params.hideY) {
tableData.push(xData);
} else {
tableData.push(xData, yData);
}
});
return tableData;
}
export function plotSettingsToFormGroup(
settings: NshmpPlotSettings,
): FormGroup<NshmpPlotSettingFormGroup> {
const formGroup = new FormGroup<NshmpPlotSettingFormGroup>({
config: new FormGroup<PlotlyConfigFormGroup>({
toImageButtonOptions: new FormBuilder().nonNullable.group({
...settings.config.toImageButtonOptions,
}),
}),
layout: new FormGroup({
aspectRatio: new FormControl(settings.layout.aspectRatio, {
nonNullable: true,
}),
legend: new FormBuilder().nonNullable.group<LegendSettings>({
...settings.layout.legend,
}),
title: new FormBuilder().nonNullable.group({...settings.layout.title}),
xaxis: new FormGroup({
nticks: new FormControl<number>(settings.layout.xaxis.nticks, {
nonNullable: true,
}),
title: new FormBuilder().nonNullable.group({
...settings.layout.xaxis.title,
}),
type: new FormControl<AxisType>(settings.layout.xaxis.type, {
nonNullable: true,
}),
}),
yaxis: new FormGroup({
nticks: new FormControl<number>(settings.layout.yaxis.nticks, {
nonNullable: true,
}),
title: new FormBuilder().nonNullable.group({
...settings.layout.yaxis.title,
}),
type: new FormControl<AxisType>(settings.layout.yaxis.type, {
nonNullable: true,
}),
}),
}),
});
addValidators(formGroup);
return formGroup;
}
export function updatePlotLabels(props: UpdatePlotLabelsProps): Partial<PlotlyLayout> {
const {layout, xLabel, yLabel, title} = props;
return {
...layout,
title: {
...(layout.title as DataTitle),
text: title ?? (layout.title as DataTitle).text,
} as DataTitle,
xaxis: {
...layout.xaxis,
title: {
...(layout.xaxis?.title as DataTitle),
text: xLabel,
} as DataTitle,
},
yaxis: {
...layout.yaxis,
title: {
...(layout.yaxis?.title as DataTitle),
text: yLabel,
} as DataTitle,
},
};
}
export function updateAppPlotSettings(plots: Map<string, NshmpPlot>): Map<string, NshmpPlot> {
const newPlots = new Map<string, NshmpPlot>();
plots.forEach((plot, id) => {
if (plot.plotData.layout === undefined) {
newPlots.set(id, plot);
} else {
const layoutSettings = plotlyLayoutToSettings(plot.plotData.layout);
plot.settingsForm.patchValue({
config: plot.plotData.config,
layout: layoutSettings,
});
}
});
return newPlots;
}
function addValidators(formGroup: FormGroup<NshmpPlotSettingFormGroup>): void {
const {config, layout} = formGroup.controls;
config.controls.toImageButtonOptions.controls.height?.addValidators(control =>
Validators.required(control),
);
config.controls.toImageButtonOptions.controls.format?.addValidators(control =>
Validators.required(control),
);
config.controls.toImageButtonOptions.controls.scale?.addValidators(control =>
Validators.required(control),
);
config.controls.toImageButtonOptions.controls.width?.addValidators(control =>
Validators.required(control),
);
layout.controls.aspectRatio.addValidators(control => Validators.required(control));
layout.controls.xaxis.controls.type.addValidators(control => Validators.required(control));
layout.controls.yaxis.controls.type.addValidators(control => Validators.required(control));
}
function createLayout(
plotOptions: DefaultPlotOptions,
options: PlotOptions,
defaultLayout: Partial<PlotlyLayout>,
defaultAxis: Partial<LayoutAxis>,
): Partial<PlotlyLayout> {
const {title, xLabel, yLabel} = plotOptions;
return {
...defaultLayout,
...options?.layout,
legend: {
...defaultLayout.legend,
...options?.layout?.legend,
},
margin: {
...defaultLayout?.margin,
...options?.layout?.margin,
},
title: {
...(defaultLayout.title as Partial<DataTitle>),
text: title,
...(options?.layout?.title as Partial<DataTitle>),
},
xaxis: {
...defaultAxis,
nticks: 40,
range: DEFAULT_XAXIS_RANGE,
...options?.layout?.xaxis,
title: {
...(defaultAxis.title as Partial<DataTitle>),
standoff: 10,
text: xLabel,
...(options?.layout?.xaxis?.title as Partial<DataTitle>),
},
},
yaxis: {
...defaultAxis,
nticks: 10,
range: DEFAULT_YAXIS_RANGE,
...options?.layout?.yaxis,
title: {
...(defaultAxis.title as Partial<DataTitle>),
standoff: 5,
text: yLabel,
...(options?.layout?.yaxis?.title as Partial<DataTitle>),
},
},
};
}
const DEFAULT_XAXIS_RANGE = [Math.log10(10e-3), Math.log10(10)];
const DEFAULT_YAXIS_RANGE = [Math.log10(10e-6), Math.log10(10)];
const DEFAULT_PLOTLY_AXIS: Partial<LayoutAxis> = {
autorange: true,
dtick: 1,
exponentformat: 'power',
linecolor: '#dfe1e2',
mirror: true,
showexponent: 'all',
tickfont: {
size: 10,
},
tickmode: 'auto',
title: {
font: {
size: 14,
},
},
type: 'log',
};
const DEFAULT_PLOTLY_MOBILE_AXIS: Partial<LayoutAxis> = {
...DEFAULT_PLOTLY_AXIS,
tickfont: {
size: 6,
},
title: {
font: {
size: 8,
},
},
};
const DEFAULT_PLOTLY_CONFIG: PlotlyConfig = {
displaylogo: false,
displayModeBar: true,
editable: false,
style: {
height: '100%',
position: 'relative',
width: '100%',
},
toImageButtonOptions: {
format: 'png',
height: Math.floor((1000 * 9) / 16),
scale: 10,
width: 1000,
},
updateOnDataChange: true,
updateOnLayoutChange: true,
useResizeHandler: true,
};
const DEFAULT_PLOTLY_LAYOUT: PlotlyLayout = {
aspectRatio: '16:9',
autosize: true,
colorway: COLORWAY,
hovermode: 'closest',
legend: {
bordercolor: 'rgb(223, 225, 226)',
borderwidth: 1,
font: {
size: 12,
},
itemclick: false,
itemdoubleclick: false,
x: 1.01,
xanchor: 'left',
y: 1,
yanchor: 'top',
},
margin: {
b: 50,
l: 65,
r: 10,
t: 80,
},
showlegend: true,
title: {
font: {
size: 18,
},
pad: {
b: 30,
},
text: '',
y: 1,
yanchor: 'bottom',
yref: 'paper',
},
};
const DEFAULT_PLOTLY_MOBILE_LAYOUT: PlotlyLayout = {
aspectRatio: '1:1',
autosize: true,
hovermode: 'closest',
legend: {
bordercolor: 'rgb(223, 225, 226)',
borderwidth: 1,
font: {
size: 8,
},
itemclick: false,
itemdoubleclick: false,
x: 0,
xanchor: 'left',
y: -0.2,
yanchor: 'top',
},
margin: {
b: 20,
l: 40,
r: 10,
t: 50,
},
showlegend: true,
title: {
font: {
size: 10,
},
pad: {
b: 10,
},
text: '',
y: 1,
yanchor: 'bottom',
yref: 'paper',
},
};