File

lib/utils/plot.utils.ts

Description

Function properties to update plot label.

Index

Properties

Properties

layout
layout: Partial<PlotlyLayout>
Type : Partial<PlotlyLayout>

Layout

title
title: string
Type : string
Optional

Title

xLabel
xLabel: string
Type : string

X label

yLabel
yLabel: string
Type : string

Y label

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,
};

/**
 * Function properties to update plot label.
 */
export interface UpdatePlotLabelsProps {
  /** Layout */
  layout: Partial<PlotlyLayout>;
  /** X label */
  xLabel: string;
  /** Y label */
  yLabel: string;

  /** Title */
  title?: string;
}

/**
 * Returns a generic Plotly plot.
 *
 * @param plotOptions The plot options
 */
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,
  };
}

/**
 * Convert the Plotly layout to application plot layout settings.
 *
 * @param layout The plotly layout
 */
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',
    },
  };
}

/**
 * Returns the table data corresponding to a plot.
 *
 * @param plot The nshmp plot
 * @param params The transform options
 */
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;
}

/**
 * Convert plot settings to a form group.
 *
 * @param settings The NSHMp plot settings
 */
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;
}

/**
 * Update plot labels.
 *
 * @param props The function properties
 */
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,
    },
  };
}

/**
 * Update the application plot settings based on the Plotly layout settings.
 *
 * For generally updating the xLabel and yLabel based on service response.
 *
 * @param plots The map of plots
 */
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;
}

/**
 * Add validators to plot settings.
 *
 * @param plot The plot
 */
function addValidators(formGroup: FormGroup<NshmpPlotSettingFormGroup>): void {
  const {config, layout} = formGroup.controls;

  // Config validators
  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 validators
  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));
}

/**
 * Returns the layout for Plotly.
 *
 * @param plotOptions The default plot options
 * @param options The plot options
 * @param defaultLayout Default layout
 * @param defaultAxis Default axis options
 */
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',
  },
};

results matching ""

    No results matching ""