diff --git a/client/app/assets/less/main.less b/client/app/assets/less/main.less index 8b0b26f3e6..0f764f8144 100644 --- a/client/app/assets/less/main.less +++ b/client/app/assets/less/main.less @@ -56,7 +56,6 @@ @import 'inc/visualizations/sankey'; @import 'inc/visualizations/pivot-table'; @import 'inc/visualizations/map'; -@import 'inc/visualizations/chart'; @import 'inc/visualizations/sunburst'; @import 'inc/visualizations/cohort'; @import 'inc/visualizations/misc'; diff --git a/client/app/pages/dashboards/dashboard.less b/client/app/pages/dashboards/dashboard.less index e672fada81..97e039c625 100644 --- a/client/app/pages/dashboards/dashboard.less +++ b/client/app/pages/dashboards/dashboard.less @@ -80,7 +80,7 @@ .map-visualization-container, .word-cloud-visualization-container, .box-plot-deprecated-visualization-container, - .plotly-chart-container { + .chart-visualization-container { position: absolute; left: 0; top: 0; diff --git a/client/app/visualizations/EditVisualizationDialog.jsx b/client/app/visualizations/EditVisualizationDialog.jsx index db2a94384c..4316e82757 100644 --- a/client/app/visualizations/EditVisualizationDialog.jsx +++ b/client/app/visualizations/EditVisualizationDialog.jsx @@ -191,6 +191,7 @@ function EditVisualizationDialog({ dialog, visualization, query, queryResult }) options={options} visualizationName={name} onOptionsChange={onOptionsChanged} + context="query" /> diff --git a/client/app/visualizations/chart/Renderer/CustomPlotlyChart.jsx b/client/app/visualizations/chart/Renderer/CustomPlotlyChart.jsx new file mode 100644 index 0000000000..d60a43c213 --- /dev/null +++ b/client/app/visualizations/chart/Renderer/CustomPlotlyChart.jsx @@ -0,0 +1,48 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { RendererPropTypes } from '@/visualizations'; + +import { clientConfig } from '@/services/auth'; +import resizeObserver from '@/services/resizeObserver'; + +import getChartData from '../getChartData'; +import { Plotly, prepareCustomChartData, createCustomChartRenderer } from '../plotly'; + +export default function CustomPlotlyChart({ options, data }) { + if (!clientConfig.allowCustomJSVisualizations) { + return null; + } + + const [container, setContainer] = useState(null); + + const renderCustomChart = useMemo( + () => createCustomChartRenderer(options.customCode, options.enableConsoleLogs), + [options.customCode, options.enableConsoleLogs], + ); + + const plotlyData = useMemo( + () => prepareCustomChartData(getChartData(data.rows, options)), + [options, data], + ); + + useEffect(() => { + if (container) { + const unwatch = resizeObserver(container, () => { + // Clear existing data with blank data for succeeding codeCall adds data to existing plot. + Plotly.purge(container); + renderCustomChart(plotlyData.x, plotlyData.ys, container, Plotly); + }); + return unwatch; + } + }, [container, plotlyData]); + + // Cleanup when component destroyed + useEffect(() => { + if (container) { + return () => Plotly.purge(container); + } + }, [container]); + + return
; +} + +CustomPlotlyChart.propTypes = RendererPropTypes; diff --git a/client/app/visualizations/chart/Renderer/PlotlyChart.jsx b/client/app/visualizations/chart/Renderer/PlotlyChart.jsx new file mode 100644 index 0000000000..df96f730a6 --- /dev/null +++ b/client/app/visualizations/chart/Renderer/PlotlyChart.jsx @@ -0,0 +1,51 @@ +import { isArray, isObject } from 'lodash'; +import React, { useState, useEffect } from 'react'; +import { RendererPropTypes } from '@/visualizations'; +import resizeObserver from '@/services/resizeObserver'; + +import getChartData from '../getChartData'; +import { Plotly, prepareData, prepareLayout, updateData, applyLayoutFixes } from '../plotly'; + +export default function PlotlyChart({ options, data }) { + const [container, setContainer] = useState(null); + + useEffect(() => { + if (container) { + const plotlyOptions = { showLink: false, displaylogo: false }; + + const chartData = getChartData(data.rows, options); + const plotlyData = prepareData(chartData, options); + const plotlyLayout = prepareLayout(container, options, plotlyData); + + // It will auto-purge previous graph + Plotly.newPlot(container, plotlyData, plotlyLayout, plotlyOptions).then(() => { + applyLayoutFixes(container, plotlyLayout, (e, u) => Plotly.relayout(e, u)); + }); + + container.on('plotly_restyle', (updates) => { + // This event is triggered if some plotly data/layout has changed. + // We need to catch only changes of traces visibility to update stacking + if (isArray(updates) && isObject(updates[0]) && updates[0].visible) { + updateData(plotlyData, options); + Plotly.relayout(container, plotlyLayout); + } + }); + + const unwatch = resizeObserver(container, () => { + applyLayoutFixes(container, plotlyLayout, (e, u) => Plotly.relayout(e, u)); + }); + return unwatch; + } + }, [options, data, container]); + + // Cleanup when component destroyed + useEffect(() => { + if (container) { + return () => Plotly.purge(container); + } + }, [container]); + + return ; +} + +PlotlyChart.propTypes = RendererPropTypes; diff --git a/client/app/visualizations/chart/Renderer/index.jsx b/client/app/visualizations/chart/Renderer/index.jsx new file mode 100644 index 0000000000..f44f3065af --- /dev/null +++ b/client/app/visualizations/chart/Renderer/index.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { RendererPropTypes } from '@/visualizations'; + +import PlotlyChart from './PlotlyChart'; +import CustomPlotlyChart from './CustomPlotlyChart'; + +import './renderer.less'; + +export default function Renderer({ options, ...props }) { + if (options.globalSeriesType === 'custom') { + return