@deephaven/js-plugin-plotly-express
Version:
Deephaven plotly express plugin
690 lines (689 loc) • 32 kB
JavaScript
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());
}
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) {
var _a, _b;
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);
(_a = this.subscriptionCleanupMap.get(id)) === null || _a === void 0 ? void 0 : _a();
(_b = this.tableSubscriptionMap.get(id)) === null || _b === void 0 ? void 0 : _b.close();
this.subscriptionCleanupMap.delete(id);
this.tableSubscriptionMap.delete(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);
this.subscriptionCleanupMap.set(id, subscription.addEventListener(this.dh.Table.EVENT_UPDATED, e => this.handleFigureUpdated(e, id)));
}
}
removeTable(id) {
var _a, _b;
(_a = this.subscriptionCleanupMap.get(id)) === null || _a === void 0 ? void 0 : _a();
(_b = this.tableSubscriptionMap.get(id)) === null || _b === void 0 ? void 0 : _b.close();
this.tableReferenceMap.delete(id);
this.downsampleMap.delete(id);
this.subscriptionCleanupMap.delete(id);
this.tableSubscriptionMap.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