import { ChartModel, ChartUtils, } from '@deephaven/chart';
import Log from '@deephaven/log';
import memoize from 'memoizee';
import { areSameAxisRange, downsample, getDataMappings, getPathParts, getReplaceableWebGlTraceIndices, getWidgetData, isAutoAxis, isLineSeries, isLinearAxis, removeColorsFromData, setWebGlTraceType, hasUnreplaceableWebGlTraces, isSingleValue, replaceValueFormat, setDefaultValueFormat, getDataTypeMap, IS_WEBGL_SUPPORTED, setRangebreaksFromCalendar, } from './PlotlyExpressChartUtils';
const log = Log.module('@deephaven/js-plugin-plotly-express.ChartModel');
export class PlotlyExpressChartModel extends ChartModel {
    static canFetch(table) {
        return table.size <= PlotlyExpressChartModel.MAX_FETCH_SIZE;
    }
    constructor(dh, widget, refetch) {
        super(dh);
        this.isSubscribed = false;
        /**
         * Map of table index to Table object.
         */
        this.tableReferenceMap = new Map();
        /**
         * Map of downsampled table indexes to original Table object.
         */
        this.downsampleMap = new Map();
        /**
         * Map of table index to TableSubscription object.
         */
        this.tableSubscriptionMap = new Map();
        /**
         * Map of table index to cleanup function for the subscription.
         */
        this.subscriptionCleanupMap = new Map();
        /**
         * Map of table index to map of column names to array of paths where the data should be replaced.
         */
        this.tableColumnReplacementMap = new Map();
        /**
         * Map of table index to ChartData object. Used to handle data delta updates.
         */
        this.chartDataMap = new Map();
        /**
         * Map of table index to object where the keys are column names and the values are arrays of data.
         * This data is the full array of data for the column since ChartData doesn't have a clean way to get it at any time.
         */
        this.tableDataMap = new Map();
        this.plotlyData = [];
        this.layout = {};
        this.isPaused = false;
        this.hasPendingUpdate = false;
        this.hasInitialLoadCompleted = false;
        this.isDownsamplingDisabled = false;
        this.isWebGlSupported = IS_WEBGL_SUPPORTED;
        /**
         * Set of traces that are originally WebGL and can be replaced with non-WebGL traces.
         * These need to be replaced if WebGL is disabled and re-enabled if WebGL is enabled again.
         */
        this.webGlTraceIndices = new Set();
        /**
         * The WebGl warning is only shown once per chart. When the user acknowledges the warning, it will not be shown again.
         */
        this.hasAcknowledgedWebGlWarning = false;
        /**
         * A calendar object that is used to set rangebreaks on a time axis.
         */
        this.calendar = null;
        /**
         * The set of parameters that need to be replaced with the default value format.
         */
        this.defaultValueFormatSet = new Set();
        /**
         * Map of variable within the plotly data to type.
         * For example, '0/value' -> 'int'
         */
        this.dataTypeMap = new Map();
        /**
         * Map of filter column names to their metadata.
         */
        this.filterColumnMap = new Map();
        /**
         * The filter map that is sent to the server.
         * This is a map of column names to filter values.
         */
        this.filterMap = null;
        /**
         * A set of column names that are required for the chart to render.
         * If any of these columns are not in the filter map, the chart will not render.
         */
        this.requiredColumns = new Set();
        this.getTimeZone = memoize((columnType, formatter) => {
            if (formatter != null) {
                const dataFormatter = formatter.getColumnTypeFormatter(columnType);
                if (dataFormatter != null) {
                    return dataFormatter.dhTimeZone;
                }
            }
            return undefined;
        });
        this.getValueTranslator = memoize((columnType, formatter) => {
            const timeZone = this.getTimeZone(columnType, formatter);
            return (value) => this.chartUtils.unwrapValue(value, timeZone);
        });
        this.widget = widget;
        this.refetch = refetch;
        this.chartUtils = new ChartUtils(dh);
        this.handleFigureUpdated = this.handleFigureUpdated.bind(this);
        this.handleWidgetUpdated = this.handleWidgetUpdated.bind(this);
        const widgetData = getWidgetData(widget);
        // Chart only fetches the model layout once on init, so it needs to be set
        // before the widget is subscribed to.
        this.updateLayout(widgetData);
        // The calendar is only fetched once at init.
        this.updateCalendar(widgetData);
        // The input filter columns are set once at init.
        this.updateFilterColumns(widgetData);
        this.setTitle(this.getDefaultTitle());
    }
    cleanupSubscriptions(id) {
        var _a, _b;
        (_a = this.subscriptionCleanupMap.get(id)) === null || _a === void 0 ? void 0 : _a.forEach(cleanup => {
            cleanup();
        });
        try {
            (_b = this.tableSubscriptionMap.get(id)) === null || _b === void 0 ? void 0 : _b.close();
        }
        catch (_c) {
            // ignore
        }
        this.subscriptionCleanupMap.delete(id);
        this.tableSubscriptionMap.delete(id);
    }
    getData() {
        const hydratedData = [...this.plotlyData];
        this.tableColumnReplacementMap.forEach((columnReplacements, tableId) => {
            const tableData = this.tableDataMap.get(tableId);
            if (tableData == null) {
                throw new Error(`No tableData for table ID ${tableId}`);
            }
            // Replace placeholder arrays with actual data
            columnReplacements.forEach((paths, columnName) => {
                paths.forEach(destination => {
                    var _a, _b, _c;
                    // The JSON pointer starts w/ /plotly/data and we don't need that part
                    const parts = getPathParts(destination);
                    const single = isSingleValue(hydratedData, parts);
                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
                    let selector = hydratedData;
                    for (let i = 0; i < parts.length; i += 1) {
                        if (i !== parts.length - 1) {
                            selector = selector[parts[i]];
                        }
                        else if (single) {
                            selector[parts[i]] = (_b = (_a = tableData[columnName]) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : null;
                        }
                        else {
                            selector[parts[i]] = (_c = tableData[columnName]) !== null && _c !== void 0 ? _c : [];
                        }
                    }
                });
            });
        });
        return hydratedData;
    }
    getLayout() {
        return this.layout;
    }
    close() {
        var _a;
        super.close();
        (_a = this.widget) === null || _a === void 0 ? void 0 : _a.close();
        this.widget = undefined;
    }
    async subscribe(callback) {
        var _a;
        if (this.isSubscribed) {
            log.debug('already subscribed');
            return;
        }
        log.debug('subscribing');
        super.subscribe(callback);
        if (this.widget == null) {
            this.widget = await this.refetch();
        }
        const widgetData = getWidgetData(this.widget);
        this.handleWidgetUpdated(widgetData, this.widget.exportedObjects);
        this.isSubscribed = true;
        this.widgetUnsubscribe = this.widget.addEventListener(this.dh.Widget.EVENT_MESSAGE, ({ detail }) => {
            this.handleWidgetUpdated(JSON.parse(detail.getDataAsString()), detail.exportedObjects);
        });
        this.tableReferenceMap.forEach((_, id) => this.subscribeTable(id));
        // If there are no tables to fetch data from, the chart is ready to render
        // Normally this event only fires once at least 1 table has fetched data
        // Without this, the chart shows an infinite loader if there are no tables
        if (this.tableColumnReplacementMap.size === 0) {
            this.fireUpdate(this.getData());
        }
        if (this.filterColumnMap != null) {
            // there are filters, so the server expects the filter to be sent
            this.sendFilterUpdated((_a = this.filterMap) !== null && _a !== void 0 ? _a : new Map());
        }
    }
    unsubscribe(callback) {
        var _a, _b;
        if (!this.isSubscribed) {
            return;
        }
        super.unsubscribe(callback);
        (_a = this.widgetUnsubscribe) === null || _a === void 0 ? void 0 : _a.call(this);
        this.isSubscribed = false;
        this.tableReferenceMap.forEach((_, id) => this.removeTable(id));
        (_b = this.widget) === null || _b === void 0 ? void 0 : _b.close();
        this.widget = undefined;
    }
    setRenderOptions(renderOptions) {
        var _a;
        this.handleWebGlAllowed(renderOptions.webgl, (_a = this.renderOptions) === null || _a === void 0 ? void 0 : _a.webgl);
        super.setRenderOptions(renderOptions);
    }
    /**
     * Handle the WebGL option being set in the render options.
     * If WebGL is enabled, traces have their original types as given.
     * If WebGL is disabled, replace traces that require WebGL with non-WebGL traces if possible.
     * Also, show a dismissible warning per-chart if there are WebGL traces that cannot be replaced.
     * @param webgl The new WebGL value. True if WebGL is enabled.
     * @param prevWebgl The previous WebGL value
     */
    handleWebGlAllowed(webgl = true, prevWebgl = true) {
        setWebGlTraceType(this.plotlyData, webgl && this.isWebGlSupported, this.webGlTraceIndices);
        const needsBlocker = hasUnreplaceableWebGlTraces(this.plotlyData);
        // If WebGL is disabled and there are traces that require WebGL, show a warning that is dismissible on a per-chart basis
        if (needsBlocker && !webgl && !this.hasAcknowledgedWebGlWarning) {
            this.fireBlocker([
                'WebGL is disabled but this chart cannot render without it. Check the Advanced section in the settings to enable WebGL or click below to render with WebGL for this chart.',
            ]);
        }
        else if (webgl && !prevWebgl && needsBlocker) {
            // clear the blocker but not the acknowledged flag in case WebGL is disabled again
            this.fireBlockerClear(false);
        }
    }
    fireBlockerClear(isAcknowledged = true) {
        super.fireBlockerClear();
        this.hasAcknowledgedWebGlWarning =
            isAcknowledged || this.hasAcknowledgedWebGlWarning;
    }
    updateLayout(data) {
        const { figure } = data;
        const { plotly } = figure;
        const { layout: plotlyLayout = {} } = plotly;
        // @deephaven/chart Chart component mutates the layout
        // If we want updates like the zoom range, we must only set the layout once on init
        // The title is currently the only thing that can be updated after init
        if (Object.keys(this.layout).length > 0) {
            return;
        }
        this.layout = Object.assign({}, plotlyLayout);
    }
    /**
     * Check if the timezone has changed in the new formatter
     * @param formatter The new formatter
     * @returns True if the timezone has changed
     */
    timeZoneChanged(formatter) {
        var _a, _b, _c;
        const timeZone = (_b = (_a = this.formatter) === null || _a === void 0 ? void 0 : _a.getColumnTypeFormatter('datetime')) === null || _b === void 0 ? void 0 : _b.dhTimeZone.id;
        const newTimeZone = (_c = formatter.getColumnTypeFormatter('datetime')) === null || _c === void 0 ? void 0 : _c.dhTimeZone.id;
        return timeZone !== newTimeZone && newTimeZone != null;
    }
    /**
     * Update the calendar object from the data
     * @param data The new data to update the calendar from
     */
    updateCalendar(data) {
        const { calendar } = data.figure.deephaven;
        if (calendar != null) {
            // Timezone must be replaced for accurate rangebreaks.
            const timeZone = this.dh.i18n.TimeZone.getTimeZone(calendar.timeZone);
            this.calendar = Object.assign(Object.assign({}, calendar), { timeZone, holidays: calendar.holidays.map((holiday, i) => {
                    const { date } = holiday;
                    // date is a really a string at this point, but it should be a LocalDate object
                    const dateObj = new Date(date);
                    const year = dateObj.getFullYear();
                    const month = dateObj.getMonth();
                    const day = dateObj.getDate();
                    return Object.assign(Object.assign({}, holiday), { date: {
                            valueOf: () => date,
                            getYear: () => year,
                            getMonthValue: () => month,
                            getDayOfMonth: () => day,
                            toString: () => date,
                        } });
                }) });
        }
    }
    /**
     * Fire an event to update the rangebreaks on the chart.
     * @param formatter The formatter to use to set the rangebreaks. If not provided, the current formatter is used.
     */
    fireRangebreaksUpdated(formatter = this.formatter) {
        if (!formatter) {
            return;
        }
        const layoutUpdate = setRangebreaksFromCalendar(formatter, this.calendar, this.layout, this.chartUtils);
        if (layoutUpdate) {
            this.fireLayoutUpdated(layoutUpdate);
        }
    }
    /**
     * Update the filter columns from the data.
  
     * @param data The new data to update the filter columns from
     */
    updateFilterColumns(data) {
        const { deephaven } = data.figure;
        const { filterColumns } = deephaven;
        if (filterColumns != null) {
            this.filterColumnMap = new Map(filterColumns.columns.map(({ name, type }) => [name, { name, type }]));
            // get all columns that have required = true
            this.requiredColumns = new Set(filterColumns.columns
                .filter(({ required }) => required)
                .map(({ name }) => name));
        }
    }
    /**
     * Unsubscribe from a table.
     * @param id The table ID to unsubscribe from
     */
    unsubscribeTable(id) {
        var _a;
        (_a = this.tableSubscriptionMap.get(id)) === null || _a === void 0 ? void 0 : _a.close();
        this.tableSubscriptionMap.delete(id);
    }
    /**
     * Fire an event to update the timezone on the chart data if it has changed.
     * @param formatter The new formatter
     */
    fireTimeZoneUpdated() {
        this.tableDataMap.forEach((_, tableId) => {
            const table = this.tableReferenceMap.get(tableId);
            if (table) {
                // resubscribe to get the data with the new timezone
                this.unsubscribeTable(tableId);
                this.subscribeTable(tableId);
            }
        });
        this.fireUpdate(this.getData());
    }
    setFormatter(formatter) {
        setDefaultValueFormat(this.plotlyData, this.defaultValueFormatSet, this.dataTypeMap, formatter);
        // Only update if isSubscribed because otherwise the events are unnecessary and buggy
        if (this.isSubscribed && this.timeZoneChanged(formatter)) {
            this.fireRangebreaksUpdated(formatter);
            this.fireTimeZoneUpdated();
        }
        super.setFormatter(formatter);
    }
    handleWidgetUpdated(data, references) {
        var _a, _b, _c, _d, _e, _f, _g;
        log.debug('handleWidgetUpdated', data, references);
        const { figure, new_references: newReferences, removed_references: removedReferences, } = data;
        const { plotly, deephaven } = figure;
        const { layout: plotlyLayout = {} } = plotly;
        this.tableColumnReplacementMap = getDataMappings(data);
        this.plotlyData = plotly.data;
        if (!deephaven.is_user_set_template) {
            removeColorsFromData((_c = (_b = (_a = plotlyLayout === null || plotlyLayout === void 0 ? void 0 : plotlyLayout.template) === null || _a === void 0 ? void 0 : _a.layout) === null || _b === void 0 ? void 0 : _b.colorway) !== null && _c !== void 0 ? _c : [], this.plotlyData);
        }
        this.defaultValueFormatSet = replaceValueFormat(this.plotlyData);
        // Retrieve the indexes of traces that require WebGL so they can be replaced if WebGL is disabled
        this.webGlTraceIndices = getReplaceableWebGlTraceIndices(this.plotlyData);
        this.handleWebGlAllowed((_d = this.renderOptions) === null || _d === void 0 ? void 0 : _d.webgl);
        this.fireRangebreaksUpdated();
        newReferences.forEach(async (id, i) => {
            this.tableDataMap.set(id, {}); // Plot may render while tables are being fetched. Set this to avoid a render error
            const table = (await references[i].fetch());
            this.addTable(id, table).then(() => {
                // The data type map requires the table to be added to the reference map
                this.dataTypeMap = getDataTypeMap(deephaven, this.tableReferenceMap);
                setDefaultValueFormat(this.plotlyData, this.defaultValueFormatSet, this.dataTypeMap, this.formatter);
            });
        });
        removedReferences.forEach(id => this.removeTable(id));
        // title and legend title are the only things expected to be updated after init from the layout
        if (typeof plotlyLayout.title === 'object' &&
            plotlyLayout.title.text != null &&
            plotlyLayout.title.text !== ((_e = this.layout.title) === null || _e === void 0 ? void 0 : _e.text)) {
            this.fireLayoutUpdated({ title: plotlyLayout.title });
            // Keep track of the title to make sure it is not unnecessarily updated
            // fireLayoutUpdated does not update this.layout so it must be set here
            this.layout.title = plotlyLayout.title;
        }
        if (((_g = (_f = plotlyLayout.legend) === null || _f === void 0 ? void 0 : _f.title) === null || _g === void 0 ? void 0 : _g.text) != null) {
            this.fireLayoutUpdated({
                legend: Object.assign({ title: Object.assign({ text: plotlyLayout.legend.title.text }, plotlyLayout.legend.title) }, plotlyLayout.legend),
            });
        }
        // If there are no tables to fetch data from, the chart is ready to render
        // Normally this event only fires once at least 1 table has fetched data
        // Without this, the chart shows an infinite loader if there are no tables
        if (this.tableColumnReplacementMap.size === 0) {
            this.fireUpdate(this.getData());
        }
    }
    handleFigureUpdated(event, tableId) {
        const chartData = this.chartDataMap.get(tableId);
        const tableData = this.tableDataMap.get(tableId);
        if (chartData == null) {
            log.warn('Unknown chartData for this event. Skipping update');
            return;
        }
        if (tableData == null) {
            log.warn('No tableData for this event. Skipping update');
            return;
        }
        const { detail: figureUpdateEvent } = event;
        chartData.update(figureUpdateEvent);
        figureUpdateEvent.columns.forEach(column => {
            const valueTranslator = this.getValueTranslator(column.type, this.formatter);
            const columnData = chartData.getColumn(column.name, valueTranslator, figureUpdateEvent);
            tableData[column.name] = columnData;
        });
        if (this.isPaused) {
            this.hasPendingUpdate = true;
            return;
        }
        this.fireUpdate(this.getData());
    }
    async addTable(id, table) {
        if (this.tableReferenceMap.has(id)) {
            return;
        }
        let tableToAdd = table;
        const downsampleInfo = this.getDownsampleInfo(id, table);
        const needsDownsample = table.size > PlotlyExpressChartModel.AUTO_DOWNSAMPLE_SIZE;
        const canDownsample = typeof downsampleInfo !== 'string';
        const canFetch = PlotlyExpressChartModel.canFetch(table);
        const shouldDownsample = needsDownsample && !this.isDownsamplingDisabled;
        if (!canDownsample) {
            if (!canFetch) {
                log.debug(`Table ${id} too big to fetch ${table.size} items`);
                this.fireDownsampleFail(`Too many items to plot: ${Number(table.size).toLocaleString()} items.`);
                return;
            }
            if (shouldDownsample) {
                this.fireDownsampleFail(downsampleInfo);
                return;
            }
        }
        if (canDownsample && needsDownsample) {
            this.downsampleMap.set(id, downsampleInfo);
            try {
                this.fireDownsampleStart(null);
                tableToAdd = await downsample(this.dh, downsampleInfo);
                this.fireDownsampleFinish(null);
            }
            catch (e) {
                this.fireDownsampleFail(e);
                return;
            }
        }
        this.tableReferenceMap.set(id, tableToAdd);
        this.tableDataMap.set(id, {});
        if (this.isSubscribed) {
            this.subscribeTable(id);
        }
    }
    async updateDownsampledTable(id) {
        const oldDownsampleInfo = this.downsampleMap.get(id);
        if (oldDownsampleInfo == null) {
            log.error(`No table found for id ${id}`);
            return;
        }
        const downsampleInfo = this.getDownsampleInfo(id, oldDownsampleInfo.originalTable);
        if (typeof downsampleInfo === 'string') {
            this.fireDownsampleFail(downsampleInfo);
            return;
        }
        if (areSameAxisRange(downsampleInfo.range, oldDownsampleInfo.range) &&
            downsampleInfo.width === oldDownsampleInfo.width) {
            log.debug('Range and width are the same, skipping downsample');
            return;
        }
        log.debug('Updating downsampled table', downsampleInfo);
        this.fireDownsampleStart(null);
        this.cleanupSubscriptions(id);
        this.tableReferenceMap.delete(id);
        this.addTable(id, oldDownsampleInfo.originalTable);
    }
    setDownsamplingDisabled(isDownsamplingDisabled) {
        this.isDownsamplingDisabled = isDownsamplingDisabled;
        if (isDownsamplingDisabled && this.widget != null) {
            const widgetData = getWidgetData(this.widget);
            this.handleWidgetUpdated(widgetData, this.widget.exportedObjects);
            this.fireDownsampleFinish(null);
        }
    }
    /**
     * Gets info on how to downsample a table for plotting.
     * @param tableId The tableId to get downsample info for
     * @param table The table to get downsample info for
     * @returns DownsampleInfo if table can be downsampled.
     *          A string of the reason if the table cannot be downsampled.
     *          Null if the table does not need downsampling.
     */
    getDownsampleInfo(tableId, table) {
        var _a, _b, _c;
        const downsampleFailMessage = `Plotting ${Number(table.size).toLocaleString()} items may be slow.\nAre you sure you want to continue?`;
        const replacementMap = this.tableColumnReplacementMap.get(tableId);
        if (!replacementMap) {
            return 'Nothing to downsample';
        }
        const areAllLines = [...replacementMap.values()]
            .flat()
            .map(path => getPathParts(path)[0])
            .every(seriesIndex => {
            const series = this.plotlyData[parseInt(seriesIndex, 10)];
            return series != null && isLineSeries(series);
        });
        if (!areAllLines) {
            log.debug('Cannot downsample non-line series');
            return downsampleFailMessage;
        }
        let xCol = '';
        let xAxis;
        const yCols = [];
        const replacementEntries = [...replacementMap.entries()];
        for (let i = 0; i < replacementEntries.length; i += 1) {
            const [columnName, paths] = replacementEntries[i];
            const pathParts = paths.map(getPathParts);
            for (let j = 0; j < pathParts.length; j += 1) {
                const [seriesIdx, xOrY] = pathParts[j];
                const series = this.plotlyData[parseInt(seriesIdx, 10)];
                if (xOrY === 'x') {
                    if (xCol !== '' && columnName !== xCol) {
                        log.debug('Cannot downsample multiple x columns');
                        return downsampleFailMessage;
                    }
                    xCol = columnName;
                    const axisName = `${series.xaxis[0]}axis${(_a = series.xaxis[1]) !== null && _a !== void 0 ? _a : ''}`;
                    xAxis = this.layout[axisName]; // The cast makes TS happy
                    if (xAxis != null && !isLinearAxis(xAxis) && !isAutoAxis(xAxis)) {
                        return 'Cannot downsample non-linear x axis';
                    }
                }
                else {
                    yCols.push(columnName);
                    const axisName = `${series.yaxis[0]}axis${(_b = series.yaxis[1]) !== null && _b !== void 0 ? _b : ''}`;
                    const yAxis = this.layout[axisName]; // The cast makes TS happy
                    if (yAxis != null && !isLinearAxis(yAxis) && !isAutoAxis(yAxis)) {
                        return 'Cannot downsample non-linear y axis';
                    }
                }
            }
        }
        if (xAxis == null) {
            return 'Cannot downsample without an x axis';
        }
        // Copy the range in case plotly mutates it
        const range = xAxis.autorange === false ? [...((_c = xAxis.range) !== null && _c !== void 0 ? _c : [])] : null;
        return {
            type: 'linear',
            originalTable: table,
            xCol,
            yCols,
            width: this.getPlotWidth(),
            range,
            rangeType: xAxis.type === 'date' ? 'date' : 'number',
        };
    }
    subscribeTable(id) {
        const table = this.tableReferenceMap.get(id);
        const columnReplacements = this.tableColumnReplacementMap.get(id);
        if (table != null &&
            columnReplacements != null &&
            columnReplacements.size > 0 &&
            !this.tableSubscriptionMap.has(id)) {
            this.chartDataMap.set(id, new this.dh.plot.ChartData(table));
            const columnNames = new Set(columnReplacements.keys());
            const columns = table.columns.filter(({ name }) => columnNames.has(name));
            const subscription = table.subscribe(columns);
            this.tableSubscriptionMap.set(id, subscription);
            if (!this.subscriptionCleanupMap.has(id)) {
                this.subscriptionCleanupMap.set(id, new Set());
            }
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            const cleanupSet = this.subscriptionCleanupMap.get(id);
            cleanupSet.add(subscription.addEventListener(this.dh.Table.EVENT_UPDATED, e => this.handleFigureUpdated(e, id)));
            cleanupSet.add(table.addEventListener(this.dh.Table.EVENT_DISCONNECT, e => this.fireDisconnect()));
        }
    }
    removeTable(id) {
        this.cleanupSubscriptions(id);
        this.tableReferenceMap.delete(id);
        this.downsampleMap.delete(id);
        this.chartDataMap.delete(id);
        this.tableDataMap.delete(id);
        this.tableColumnReplacementMap.delete(id);
    }
    fireUpdate(data) {
        super.fireUpdate(data);
        this.hasPendingUpdate = false;
        // TODO: This will fire on first call to `fireUpdate` even though other data
        // may still be loading. We should consider making this smarter to fire after
        // all initial data has loaded.
        // https://github.com/deephaven/deephaven-plugins/issues/267
        // If not subscribed, the fireLoadFinished will not go through since there is no listeners
        // which results in a loading spinner that does not go away on its own
        // isSubscribed can also be checked before calling fireUpdate, but this is a
        // subtle bug that is good to check for here just in case
        if (!this.hasInitialLoadCompleted && this.isSubscribed) {
            this.fireLoadFinished();
            this.hasInitialLoadCompleted = true;
        }
    }
    setDimensions(rect) {
        super.setDimensions(rect);
        ChartUtils.getLayoutRanges(this.layout);
        this.downsampleMap.forEach((_, id) => {
            this.updateDownsampledTable(id);
        });
    }
    getFilterColumnMap() {
        return this.filterColumnMap;
    }
    isFilterRequired() {
        // if any of the required columns are not in the filter map, then filters are still required
        return Array.from(this.requiredColumns).some(column => !this.filterMap || !this.filterMap.has(column));
    }
    setFilter(filterMap) {
        super.setFilter(filterMap);
        this.filterMap = filterMap;
        if (this.isSubscribed) {
            this.sendFilterUpdated(filterMap);
        }
    }
    /**
     * Fire an event to update the filters on the chart.
     * @param filterMap The filter map to send to the server
     */
    sendFilterUpdated(filterMap) {
        var _a;
        // Only send the filter update if filters are not required and the filter columns are set
        // They will either be set or none are required
        if (!this.isFilterRequired() && this.filterColumnMap.size > 0) {
            (_a = this.widget) === null || _a === void 0 ? void 0 : _a.sendMessage(JSON.stringify({
                type: 'FILTER',
                filterMap: Object.fromEntries(filterMap),
            }));
        }
    }
    pauseUpdates() {
        this.isPaused = true;
    }
    resumeUpdates() {
        this.isPaused = false;
        if (this.hasPendingUpdate) {
            this.fireUpdate(this.getData());
        }
    }
    shouldPauseOnUserInteraction() {
        return this.hasScene() || this.hasGeo() || this.hasMap() || this.hasPolar();
    }
    hasScene() {
        return this.plotlyData.some(d => 'scene' in d && d.scene != null);
    }
    hasGeo() {
        return this.plotlyData.some(d => 'geo' in d && d.geo != null);
    }
    hasMap() {
        return this.plotlyData.some(({ type }) => type === null || type === void 0 ? void 0 : type.includes('map'));
    }
    hasPolar() {
        return this.plotlyData.some(({ type }) => type === null || type === void 0 ? void 0 : type.includes('polar'));
    }
    getPlotWidth() {
        var _a, _b, _c, _d;
        if (!this.rect || !this.rect.width) {
            return 0;
        }
        return Math.max(this.rect.width -
            ((_b = (_a = this.layout.margin) === null || _a === void 0 ? void 0 : _a.l) !== null && _b !== void 0 ? _b : 0) -
            ((_d = (_c = this.layout.margin) === null || _c === void 0 ? void 0 : _c.r) !== null && _d !== void 0 ? _d : 0), 0);
    }
    getPlotHeight() {
        var _a, _b, _c, _d;
        if (!this.rect || !this.rect.height) {
            return 0;
        }
        return Math.max(this.rect.height -
            ((_b = (_a = this.layout.margin) === null || _a === void 0 ? void 0 : _a.t) !== null && _b !== void 0 ? _b : 0) -
            ((_d = (_c = this.layout.margin) === null || _c === void 0 ? void 0 : _c.b) !== null && _d !== void 0 ? _d : 0), 0);
    }
}
/**
 * The size at which the chart will automatically downsample the data if it can be downsampled.
 * If it cannot be downsampled, but the size is below MAX_FETCH_SIZE,
 * the chart will show a confirmation to fetch the data since it might be a slow operation.
 */
PlotlyExpressChartModel.AUTO_DOWNSAMPLE_SIZE = 30000;
/**
 * The maximum number of items that can be fetched from a table.
 * If a table is larger than this, the chart will not be fetched.
 * This is to prevent the chart from fetching too much data and crashing the browser.
 */
PlotlyExpressChartModel.MAX_FETCH_SIZE = 1000000;
export default PlotlyExpressChartModel;
//# sourceMappingURL=PlotlyExpressChartModel.js.map