+ );
+ }
+}
+
+StepsControl.propTypes = propTypes;
+StepsControl.defaultProps = defaultProps;
+
+function mapStateToProps({ charts, explore }) {
+ const chartKey = getChartKey(explore);
+ const chart = charts[chartKey] || charts[0] || {};
+
+ return {
+ annotationError: chart.annotationError,
+ annotationQuery: chart.annotationQuery,
+ vizType: explore.controls.viz_type.value,
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ refreshAnnotationData: annotationLayer => dispatch(runAnnotationQuery(annotationLayer)),
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(StepsControl);
diff --git a/superset/assets/src/explore/components/controls/index.js b/superset/assets/src/explore/components/controls/index.js
index a5800f2d11c69..a505e02a83c01 100644
--- a/superset/assets/src/explore/components/controls/index.js
+++ b/superset/assets/src/explore/components/controls/index.js
@@ -41,6 +41,8 @@ import AdhocFilterControl from './AdhocFilterControl';
import FilterPanel from './FilterPanel';
import FilterBoxItemControl from './FilterBoxItemControl';
import withVerification from './withVerification';
+import StepsControl from './StepsControl';
+import FilterControl from './FilterControl';
const controlMap = {
AnnotationLayerControl,
@@ -70,5 +72,7 @@ const controlMap = {
MetricsControlVerifiedOptions: withVerification(MetricsControl, 'metric_name', 'savedMetrics'),
SelectControlVerifiedOptions: withVerification(SelectControl, 'column_name', 'options'),
AdhocFilterControlVerifiedOptions: withVerification(AdhocFilterControl, 'column_name', 'columns'),
+ StepsControl,
+ FilterControl,
};
export default controlMap;
diff --git a/superset/assets/src/explore/controlPanels/Funnel.js b/superset/assets/src/explore/controlPanels/Funnel.js
new file mode 100644
index 0000000000000..5fad6cb8d7c20
--- /dev/null
+++ b/superset/assets/src/explore/controlPanels/Funnel.js
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@superset-ui/translation';
+
+export default {
+ requiresTime: true,
+ controlPanelSections: [
+ {
+ label: t('Chart Options'),
+ expanded: true,
+ controlSetRows: [
+ ['funnel_mode'],
+ ['show_delta'],
+ ['x_axis_label'],
+ ],
+ },
+ {
+ label: t('Steps'),
+ description: t(
+ 'Add filters and metrics for each step.' +
+ 'The steps combine to tell a story.'),
+ expanded: true,
+ controlSetRows: [
+ ['funnel_steps'],
+ ],
+ },
+ {
+ label: t('Absolute Filter'),
+ description: t(
+ 'You may now apply an absolute filter ' +
+ 'that applies for all steps.'),
+ expanded: false,
+ controlSetRows: [
+ ['abs_filter'],
+ ],
+ },
+ ],
+ controlOverrides: {
+ adhoc_filters: {
+ renderTrigger: true,
+ },
+ },
+
+ sectionOverrides: {
+ filters: [],
+ sqlaTimeSeries: {
+ controlSetRows: [
+ ['time_range'],
+ ],
+ },
+ druidTimeSeries: {
+ controlSetRows: [
+ ['time_range'],
+ ],
+ },
+ },
+};
diff --git a/superset/assets/src/explore/controlPanels/sections.jsx b/superset/assets/src/explore/controlPanels/sections.jsx
index 7df0488697317..a81507b4c60c9 100644
--- a/superset/assets/src/explore/controlPanels/sections.jsx
+++ b/superset/assets/src/explore/controlPanels/sections.jsx
@@ -56,6 +56,12 @@ export const sqlaTimeSeries = {
],
};
+export const simpleFilter = {
+ label: t('Filters'),
+ expanded: true,
+ controlSetRows: [['filters']],
+};
+
export const filters = {
label: t('Filters'),
expanded: true,
@@ -72,6 +78,17 @@ export const annotations = {
],
};
+export const steps = {
+ label: t('Steps'),
+ description: t(
+ 'Add filters and metrics for each step.' +
+ 'The steps combine to tell a story.'),
+ expanded: true,
+ controlSetRows: [
+ ['funnel_steps'],
+ ],
+};
+
export const NVD3TimeSeries = [
{
label: t('Query'),
diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx
index 8f06ae6171468..4c3cdac1b6263 100644
--- a/superset/assets/src/explore/controls.jsx
+++ b/superset/assets/src/explore/controls.jsx
@@ -476,6 +476,22 @@ export const controls = {
description: null,
},
+ funnel_mode: {
+ type: 'CheckboxControl',
+ label: t('Activate Funnel Mode'),
+ renderTrigger: true,
+ default: true,
+ description: null,
+ },
+
+ show_delta: {
+ type: 'CheckboxControl',
+ label: t('Show Delta Between Values'),
+ renderTrigger: true,
+ default: false,
+ description: null,
+ },
+
pivot_margins: {
type: 'CheckboxControl',
label: t('Show totals'),
@@ -1988,6 +2004,39 @@ export const controls = {
tabOverride: 'data',
},
+ funnel_steps: {
+ type: 'StepsControl',
+ label: '',
+ default: [],
+ description: 'Steps',
+ renderTrigger: false,
+ tabOverride: 'data',
+ mapStateToProps: (state) => {
+ const datasource = state.datasource;
+ return {
+ columns: datasource ? datasource.columns : [],
+ savedMetrics: datasource ? datasource.metrics : [],
+ datasourceType: datasource && datasource.type,
+ datasource: state.datasource,
+ };
+ },
+ },
+
+ step_label: {
+ type: 'TextControl',
+ label: t('Label'),
+ renderTrigger: true,
+ default: '',
+ },
+
+ formatter: {
+ type: 'CheckboxControl',
+ label: t('Enable Formatter'),
+ renderTrigger: true,
+ default: true,
+ description: t('This enable formatter for the values for steps'),
+ },
+
adhoc_filters: {
type: 'AdhocFilterControl',
label: t('Filters'),
@@ -2001,10 +2050,33 @@ export const controls = {
provideFormDataToProps: true,
},
+ abs_filter: {
+ type: 'FilterControl',
+ label: '',
+ default: [],
+ description: 'Abs Filter for Steps',
+ renderTrigger: false,
+ mapStateToProps: state => ({
+ datasource: state.datasource,
+ }),
+ },
+
filters: {
type: 'FilterPanel',
},
+ having_filters: {
+ type: 'FilterControl',
+ label: '',
+ default: [],
+ description: '',
+ mapStateToProps: state => ({
+ choices: (state.datasource) ? state.datasource.metrics_combo
+ .concat(state.datasource.filterable_cols) : [],
+ datasource: state.datasource,
+ }),
+ },
+
slice_id: {
type: 'HiddenControl',
label: t('Chart ID'),
diff --git a/superset/assets/src/setup/setupPlugins.ts b/superset/assets/src/setup/setupPlugins.ts
index 57fb8dd9c127b..d0c15253801e5 100644
--- a/superset/assets/src/setup/setupPlugins.ts
+++ b/superset/assets/src/setup/setupPlugins.ts
@@ -45,6 +45,7 @@ import DistBar from '../explore/controlPanels/DistBar';
import DualLine from '../explore/controlPanels/DualLine';
import EventFlow from '../explore/controlPanels/EventFlow';
import FilterBox from '../explore/controlPanels/FilterBox';
+import Funnel from '../explore/controlPanels/Funnel';
import Heatmap from '../explore/controlPanels/Heatmap';
import Histogram from '../explore/controlPanels/Histogram';
import Horizon from '../explore/controlPanels/Horizon';
@@ -90,6 +91,7 @@ export default function setupPlugins() {
.registerValue('dual_line', DualLine)
.registerValue('event_flow', EventFlow)
.registerValue('filter_box', FilterBox)
+ .registerValue('funnel', Funnel)
.registerValue('heatmap', Heatmap)
.registerValue('histogram', Histogram)
.registerValue('horizon', Horizon)
diff --git a/superset/assets/src/visualizations/Funnel/Funnel.css b/superset/assets/src/visualizations/Funnel/Funnel.css
new file mode 100644
index 0000000000000..f27a0659d9422
--- /dev/null
+++ b/superset/assets/src/visualizations/Funnel/Funnel.css
@@ -0,0 +1,21 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+.m-b-5 {
+ margin-bottom: 5px;
+}
diff --git a/superset/assets/src/visualizations/Funnel/Funnel.jsx b/superset/assets/src/visualizations/Funnel/Funnel.jsx
new file mode 100644
index 0000000000000..a29be0fcf1248
--- /dev/null
+++ b/superset/assets/src/visualizations/Funnel/Funnel.jsx
@@ -0,0 +1,135 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+/* eslint camelcase: 0 */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { map } from 'lodash';
+
+import './Funnel.css';
+import ChartRenderer from '../../chart/ChartRenderer';
+
+const propTypes = {
+ rawFormData: PropTypes.object,
+ width: PropTypes.number,
+ height: PropTypes.number,
+ queryData: PropTypes.array.isRequired,
+ funnelSteps: PropTypes.array.isRequired,
+ datasource: PropTypes.object.isRequired,
+ actions: PropTypes.object.isRequired,
+};
+
+const defaultFormData = {
+ barstacked: false,
+ bottomMargin: 'auto',
+ colorScheme: 'd3Category10',
+ contribution: false,
+ orderBars: false,
+ reduceXTicks: false,
+ showBarValue: false,
+ showControls: false,
+ showLegend: true,
+ vizType: 'dist_bar',
+ xTicksLayout: 'auto',
+ yAxisFormat: ',d',
+};
+
+class Funnel extends React.Component {
+ constructor(props) {
+ super(props);
+ this.formatQueryResponse = this.formatQueryResponse.bind(this);
+ this.formatValues = this.formatValues.bind(this);
+ }
+
+ formatQueryResponse(funnelSteps) {
+ const selectedValues = funnelSteps && funnelSteps.selectedValues;
+ const selValues = map(selectedValues, item => item);
+ const values = [];
+ let prevValue = 0;
+ Object.values(this.props.queryData.data).forEach((value, index) => {
+ const label = selValues && selValues[index] && selValues[index].step_label;
+
+ // Return Delta between each step Visualization
+ const roundedValue = value > 0 ? 100 : 0;
+ const delta = prevValue > 0 ?
+ Math.round(100 * value / prevValue, 1) - 100
+ : roundedValue;
+ const deltaStr = `${delta}%`;
+ const tag_label = label || `Step ${index + 1}`;
+ const valueObj = this.formatValues({ tag_label, index, value, deltaStr });
+ values.push(valueObj);
+ prevValue = value;
+ });
+ return values;
+ }
+
+ formatValues(formatObj, formatOptions = this.props.rawFormData) {
+ const { tag_label, index, value, deltaStr } = formatObj;
+ const { funnel_mode, x_axis_label, show_delta } = formatOptions;
+
+ const chart_label = funnel_mode ?
+ x_axis_label || 'Funnel/Step Visualizaiton'
+ : tag_label;
+
+ const tagLabel = index > 0 && show_delta ?
+ `${tag_label}, ${deltaStr}`
+ : tag_label;
+
+ const valueOutput = { key: tagLabel, values: [{ y: value, x: chart_label }] };
+
+ if (funnel_mode) {
+ valueOutput.values.push({ y: -value, x: chart_label });
+ }
+ return valueOutput;
+ }
+ render() {
+ const { funnelSteps,
+ width,
+ height,
+ datasource,
+ actions,
+ queryData } = this.props;
+
+ const formatedData = this.formatQueryResponse(funnelSteps);
+
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+Funnel.propTypes = propTypes;
+
+export default Funnel;
diff --git a/superset/assets/src/visualizations/Funnel/FunnelChartPlugin.js b/superset/assets/src/visualizations/Funnel/FunnelChartPlugin.js
new file mode 100644
index 0000000000000..67a5bf618ffee
--- /dev/null
+++ b/superset/assets/src/visualizations/Funnel/FunnelChartPlugin.js
@@ -0,0 +1,38 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t } from '@superset-ui/translation';
+import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
+import transformProps from './transformProps';
+import thumbnail from './images/thumbnail.png';
+
+const metadata = new ChartMetadata({
+ name: t('Funnel'),
+ description: 'Funnel',
+ thumbnail,
+});
+
+export default class FunnelChartPlugin extends ChartPlugin {
+ constructor() {
+ super({
+ metadata,
+ transformProps,
+ loadChart: () => import('./FunnelContainer.js'),
+ });
+ }
+}
diff --git a/superset/assets/src/visualizations/Funnel/FunnelContainer.js b/superset/assets/src/visualizations/Funnel/FunnelContainer.js
new file mode 100644
index 0000000000000..41f68b83e19aa
--- /dev/null
+++ b/superset/assets/src/visualizations/Funnel/FunnelContainer.js
@@ -0,0 +1,34 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import * as actions from 'src/chart/chartAction';
+import { logEvent } from 'src/logger/actions';
+import Funnel from './Funnel';
+
+function mapDispatchToProps(dispatch) {
+ return {
+ actions: bindActionCreators({
+ ...actions,
+ logEvent,
+ }, dispatch),
+ };
+}
+
+export default connect(null, mapDispatchToProps)(Funnel);
diff --git a/superset/assets/src/visualizations/Funnel/images/thumbnail.png b/superset/assets/src/visualizations/Funnel/images/thumbnail.png
new file mode 100644
index 0000000000000..399344e398633
Binary files /dev/null and b/superset/assets/src/visualizations/Funnel/images/thumbnail.png differ
diff --git a/superset/assets/src/visualizations/Funnel/images/thumbnailLarge.png b/superset/assets/src/visualizations/Funnel/images/thumbnailLarge.png
new file mode 100644
index 0000000000000..399344e398633
Binary files /dev/null and b/superset/assets/src/visualizations/Funnel/images/thumbnailLarge.png differ
diff --git a/superset/assets/src/visualizations/Funnel/transformProps.js b/superset/assets/src/visualizations/Funnel/transformProps.js
new file mode 100644
index 0000000000000..e78053d5e685b
--- /dev/null
+++ b/superset/assets/src/visualizations/Funnel/transformProps.js
@@ -0,0 +1,43 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export default function transformProps(chartProps) {
+ const {
+ datasource,
+ filters,
+ formData,
+ rawDatasource,
+ rawFormData,
+ queryData,
+ width,
+ height,
+ hooks,
+ } = chartProps;
+
+ return {
+ verboseMap: datasource.verboseMap,
+ datasource: rawDatasource,
+ funnelSteps: formData.funnelSteps,
+ origSelectedValues: filters || {},
+ rawFormData,
+ queryData,
+ height,
+ width,
+ hooks,
+ };
+}
diff --git a/superset/assets/src/visualizations/presets/MainPreset.js b/superset/assets/src/visualizations/presets/MainPreset.js
index 3ea5592339c16..54969a6075302 100644
--- a/superset/assets/src/visualizations/presets/MainPreset.js
+++ b/superset/assets/src/visualizations/presets/MainPreset.js
@@ -53,6 +53,7 @@ import { DeckGLChartPreset } from '@superset-ui/legacy-preset-chart-deckgl';
import FilterBoxChartPlugin from '../FilterBox/FilterBoxChartPlugin';
import TimeTableChartPlugin from '../TimeTable/TimeTableChartPlugin';
+import FunnelChartPlugin from '../Funnel/FunnelChartPlugin';
export default class MainPreset extends Preset {
constructor() {
@@ -75,6 +76,7 @@ export default class MainPreset extends Preset {
new CountryMapChartPlugin().configure({ key: 'country_map' }),
new DistBarChartPlugin().configure({ key: 'dist_bar' }),
new DualLineChartPlugin().configure({ key: 'dual_line' }),
+ new FunnelChartPlugin().configure({ key: 'funnel' }),
new EventFlowChartPlugin().configure({ key: 'event_flow' }),
new FilterBoxChartPlugin().configure({ key: 'filter_box' }),
new ForceDirectedChartPlugin().configure({ key: 'directed_force' }),
diff --git a/superset/viz.py b/superset/viz.py
index edd5b1caa4344..c6be1fda7129a 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -357,6 +357,7 @@ def cache_key(self, query_obj, **extra):
different time shifts wil differ only in the `from_dttm` and `to_dttm`
values which are stripped.
"""
+
cache_dict = copy.copy(query_obj)
cache_dict.update(extra)
@@ -603,6 +604,146 @@ def json_dumps(self, obj, sort_keys=False):
)
+class FunnelViz(BaseViz):
+ """A Bar graph or 'STEP' Visualization that makes story telling easy through multipass queries"""
+
+ viz_type = "funnel"
+ verbose_name = _("Funnel")
+ is_timeseries = False
+ credits = 'a Superset original'
+ cache_type = "get_data"
+ filter_row_limit = 1000
+ is_multipass = True
+
+ def _query_obj(self):
+ return super().query_obj()
+
+ def get_samples(self):
+ query_obj_array = self.query_obj()
+ step_count = len(self.form_data["funnel_steps"]["queries"])
+ for index in range(0, step_count):
+ query_obj = query_obj_array[str(index)]
+ query_obj.update(
+ {
+ "groupby": [],
+ "metrics": [],
+ "row_limit": 1000,
+ "columns": [o.column_name for o in self.datasource.columns],
+ }
+ )
+
+ df = self.get_df(query_obj)
+
+ return df.to_dict(orient="records")
+
+ def query_obj(self):
+ if not self.form_data.get("funnel_steps"):
+ raise Exception(_("Add at least one Step"))
+
+ sel_values = self.form_data["funnel_steps"]["selectedValues"]
+ step_count = len(sel_values)
+
+ filter_store = []
+ self.form_data["granularity"] = "all"
+ if self.form_data.get("filters"):
+ filter_store = self.form_data["filters"]
+
+ result_query = {}
+ for i in range(step_count):
+ index = str(i)
+ mod_form_data = self.form_data.copy()
+ mod_form_data["metrics"] = []
+ mod_form_data["adhoc_filters"] = []
+ mod_form_data["filters"] = []
+ mod_form_data["metrics"].append(sel_values[index]["metric"])
+ adhoc_filters = sel_values[index].get("adhoc_filters")
+ if adhoc_filters:
+ mod_form_data["adhoc_filters"] = adhoc_filters
+
+ qry = self.__class__(
+ form_data=mod_form_data, datasource=self.datasource
+ )._query_obj()
+
+ # Apply 'Absolute Filter' to Query
+ if filter_store:
+ for ifilter in filter_store:
+ qry["filter"].append(ifilter)
+
+ result_query[str(i)] = qry
+ return result_query
+
+ def cache_key(self, query_obj, **extra):
+ if "0" in query_obj:
+ query_obj = query_obj["0"]
+
+ cache_dict = copy.copy(query_obj)
+ cache_dict.update(extra)
+
+ for k in ["from_dttm", "to_dttm"]:
+ del cache_dict[k]
+
+ cache_dict["time_range"] = self.form_data.get("time_range")
+ cache_dict["datasource"] = self.datasource.uid
+ cache_dict["extra_cache_keys"] = self.datasource.get_extra_cache_keys(query_obj)
+ json_data = self.json_dumps(cache_dict, sort_keys=True)
+ return hashlib.md5(json_data.encode("utf-8")).hexdigest()
+
+ def get_df_payload(self, query_obj=None, **kwargs):
+ """Special handeling of Cache due to multipass nature of query """
+ if not query_obj:
+ query_obj = self.query_obj()
+ cache_key = self.cache_key(query_obj, **kwargs) if query_obj else None
+ logging.info("Cache key: {}".format(cache_key))
+ stacktrace = None
+ df = None
+ if cache_key and cache and not self.force:
+ cache_value = cache.get(cache_key)
+ if cache_value:
+ stats_logger.incr("loaded_from_cache")
+ try:
+ cache_value = pkl.loads(cache_value)
+ df = cache_value["df"]
+ self.query = cache_value["query"]
+ self._any_cached_dttm = cache_value["dttm"]
+ self._any_cache_key = cache_key
+ self.status = utils.QueryStatus.SUCCESS
+ except Exception as e:
+ logging.exception(e)
+ logging.error(
+ "Error reading cache: " + utils.error_msg_from_exception(e)
+ )
+ logging.info("Serving from cache")
+
+ if query_obj:
+ return {
+ "cache_key": self._any_cache_key,
+ "cached_dttm": self._any_cached_dttm,
+ "cache_timeout": self.cache_timeout,
+ "df": df,
+ "error": self.error_message,
+ "form_data": self.form_data,
+ "is_cached": self._any_cache_key is not None,
+ "query": self.query,
+ "status": self.status,
+ "stacktrace": stacktrace,
+ "rowcount": len(df.index) if df is not None else 0,
+ }
+
+ def get_data(self, df):
+ agg_queries = {}
+ all_queries = self.query_obj()
+ for query in all_queries:
+ df = self.get_df(query_obj=all_queries[query])
+ obj = df.to_dict(orient="records")[0]
+ key = list(obj.keys())[0]
+ if key not in agg_queries:
+ agg_queries.update(obj)
+ else:
+ new_key = key + "_" + query
+ agg_queries.update({new_key: obj[key]})
+ return agg_queries
+
+
class TimeTableViz(BaseViz):
"""A data table with rich time-series related columns"""