Source: utils/template/functions/custom/measurementsLayerOverview.js

import Plotly from 'plotly.js-dist';

import { filterDataFrame } from '../../../../data/utils.js';
import "./measurementsLayerOverview.css";

// static variables for keeping the plot type
let modeIndex = 0;
const plotModes = ["markers", "lines", "lines+markers"]

// static plot container
let plotInterval = null;
let plotTimeout = null;

function clearTimers() {
    if (plotInterval) clearInterval(plotInterval);
    if (plotTimeout) clearTimeout(plotTimeout);
    plotInterval = null;
    plotTimeout = null;
}

/** 
 * Render the plot on the given HTMLElement.
 * Also used for re rendering, when the axis changes
 * 
 * @param {HTMLElement} container (optional)
 * @param {string} labelX label of x column
 * @param {string} labelY label of y column
 * @param {object[]} data
 * @param {boolean} invertXAxis
 * @param {boolean} invertYAxis
 * 
 * @memberof vef.plot
 * @returns {HTMLElement} container
 */
function renderPlot(container, labelX, labelY, data, invertXAxis, invertYAxis, hoverY) {
    if (!container) container = document.createElement("div");
    container.classList.add("vef-plot");

    for (let i = 0; i < data.length; ++i) {
        if (modeIndex >= plotModes.length) modeIndex = 0;
        data[i].mode = plotModes[modeIndex];
        data[i].type = "scattergl";
    }

    Plotly.purge(container);
    Plotly.newPlot(container, data, {
        spikedistance: -1,
        hovermode: (hoverY) ? "y" : "x",
        xaxis: { title: { text: labelX }, showspikes: true, spikethickness: 2, autorange: (invertXAxis) ? "reversed" : true },
        yaxis: { title: { text: labelY }, showspikes: true, spikethickness: 2, autorange: (invertYAxis) ? "reversed" : true },
        margin: {
            l: 60,
            r: 10,
            b: 50,
            t: 80,
            pad: 0
        }
    }, {
        displayModeBar: true,
        displaylogo: false,
        responsive: true,
        modeBarButtonsToRemove: ["select2d", "lasso2d", "hoverClosestGl2d", "hoverClosestPie", "toggleHover", "sendDataToCloud", "toggleSpikelines", "hoverClosestCartesian", "hoverCompareCartesian", "autoScale2d"],
        modeBarButtonsToAdd: [
            {
                name: 'Change Mode',
                icon: () => {
                    const icon = document.createElement("i");
                    icon.classList.add("fas", "fa-project-diagram");
                    return icon;
                },
                click: gd => {
                    modeIndex = (modeIndex + 1) % plotModes.length;
                    Plotly.restyle(gd, 'mode', plotModes[modeIndex])
                }
            }
        ]
    });

    return container;
}

/** 
 * Render the plot on the given HTMLElement.
 * Also used for re rendering, when the axis changes
 * 
 * @param {HTMLElement} container (optional)
 * @param {DataFrame} dataFrame 
 * @param {number} iX index of x column
 * @param {string} labelX label of x column
 * @param {number} iY index of y column
 * @param {string} labelY label of y column
 * @param {boolean} invertXAxis
 * @param {boolean} invertYAxis
 * @param {boolean} hoverY
 * 
 * @memberof vef.plot
 * @returns {HTMLElement} container
 */
function renderDataframePlot(container, dataFrame, iX, labelX, iY, labelY, invertXAxis, invertYAxis, hoverY) {
    const dataX = dataFrame.series[iX];
    const dataY = dataFrame.series[iY];
    return renderPlot(container, labelX, labelY, [{ x: dataX, y: dataY }], invertXAxis, invertYAxis, hoverY);
}

/**
 * Create the content for the visualizazion of plots in the sidebar
 * 
 * @param {HTMLElement} content container element
 * @param {object} event "visualize" event of the MeasurementsLayer
 * @param {DataFrame} dataFrame
 * @param {string} options {groupBy, defaultYAxis, invertYAxis, defaultXAxis, invertXAxis, hiddenColumns}
 * 
 * @memberof vef.plot
 * @returns {HTMLElement} content
 */
function createPlotSidebar(content, event, dataFrame, options) {
    if (!content) content = document.createElement("div");

    let invertYAxis = options.invertYAxis;
    let invertXAxis = options.invertXAxis;

    content.classList.add("plot-sidebar");
    content.innerHTML = `
        <h3>Diagram</h3>
        <div class="plot"></div>
        <h3>Axis Selection</h3>
        <div class="axis-select-container">
            <label class="axis-select-label">Y-Axis:</label><select class="axis-select axis-y"></select>
        </div>
        <div class="axis-select-container">
            <label class="axis-select-label">X-Axis:</label><select class="axis-select axis-x"></select>
        </div>
        <a class="invert-y-axis" href="#"><i class="fas fa-exchange-alt"></i> Invert Y-Axis</a>
        <a class="invert-x-axis" href="#"><i class="fas fa-exchange-alt"></i> Invert X-Axis</a>
    `;

    // filter
    let value = null;
    if (options.groupBy in dataFrame.columnMap) {
        value = event.row[dataFrame.columnMap[options.groupBy]];
        dataFrame = filterDataFrame(dataFrame, options.groupBy, value);
    }

    // get axis inidices
    let x, y;
    if (options.defaultXAxis) x = event.layer.getColumnIndex(dataFrame, options.defaultXAxis);
    if (options.defaultYAxis) y = event.layer.getColumnIndex(dataFrame, options.defaultYAxis);
    if (!Number.isFinite(x)) x = event.layer.getColumnIndex(dataFrame, event.layer.columnData) || 0;
    if (!Number.isFinite(y)) y = event.layer.getColumnIndex(dataFrame, event.layer.columnData) || 0;
    let labelX = event.layer.getColumnName(dataFrame, x);
    let labelY = event.layer.getColumnName(dataFrame, y);

    // render plot
    const plotContainer = content.querySelector(".plot");
    const plot = () => renderDataframePlot(plotContainer, dataFrame, x, labelX, y, labelY, invertXAxis, invertYAxis);
    plot();

    // invert y-axis button
    const invertYButton = content.querySelector(".invert-y-axis");
    invertYButton.addEventListener("click", (e) => {
        e.preventDefault();
        invertYAxis = !invertYAxis;
        plot();
    });

    // invert x-axis button
    const invertXButton = content.querySelector(".invert-x-axis");
    invertXButton.addEventListener("click", (e) => {
        e.preventDefault();
        invertXAxis = !invertXAxis;
        plot();
    });

    // axis selection
    let selectX = content.querySelector(".axis-x");
    let selectY = content.querySelector(".axis-y");
    for (let i in dataFrame.columnMap) {
        if (options.hiddenColumns && options.hiddenColumns.includes(i)) continue;
        const html = `<option value="${i}">${i}</option>`;
        selectX.innerHTML += html;
        selectY.innerHTML += html;
    }
    selectX.value = labelX;
    selectY.value = labelY;
    selectX.addEventListener("change", () => {
        labelX = selectX.value;
        x = event.layer.getColumnIndex(dataFrame, labelX);
        plot();
    });
    selectY.addEventListener("change", () => {
        labelY = selectY.value;
        y = event.layer.getColumnIndex(dataFrame, labelY);
        plot();
    });

    return content;
}

function plot(target, layer, data) {
    clearTimers();

    const createPlot = () => {
        clearTimers();
        createPlotSidebar(target, {
            layer: layer,
            row: data || {}
        }, layer.dataFrameOriginal, {
            defaultYAxis: layer.defaultYAxis,
            invertYAxis: layer.invertYAxis,
            defaultXAxis: layer.defaultXAxis,
            invertXAxis: layer.invertXAxis,
            groupBy: layer.groupByColumn,
            hiddenColumns: layer.hiddenColumns
        });

        if (layer.type == "dws") {
            target.insertAdjacentHTML("afterbegin", `
                <h3>PLEASE NOTE</h3>
                <div>Data is aggregated by <b>${layer.aggregate}</b> and limited to <b>${layer.limit}</b> points for performance reasons!</div>
            `);
        }
    };

    // only init plot if the target is visible in DOM (offsetParent == null if not displayed)
    // use timeout to push to end of eventloop. If not in DOM check repeatedly
    plotTimeout = setTimeout(() => {
        if (target.offsetParent) {
            createPlot()
        } else {
            plotInterval = setInterval(() => {
                if (target.offsetParent) createPlot();
            }, 700);
        }
    }, 50);
}


/**
 * Creates a container element and plots data from MeasurementsLayer
 * 
 * @param {Object} layer - The layer object to be used for plotting.
 * @param {Object} data - The data to be plotted in the container.
 * @returns {HTMLDivElement} Plot Container
 */
export function measurementsLayerOverview(layer, data) {
    const container = document.createElement("div");

    container.addEventListener("append", () => {
        plot(container, layer, data);
    });

    return container;
}