mirror of
https://github.com/valitydev/redash.git
synced 2024-11-07 09:28:51 +00:00
Add support for column mapping
This commit is contained in:
parent
a333abcaa5
commit
e78bfb2e9a
@ -1,9 +1,34 @@
|
|||||||
<div class="form-horizontal">
|
<div class="form-horizontal">
|
||||||
<div>
|
<div>
|
||||||
This visualization expects the query result to be in the following formats:
|
This visualization constructs funnel chart. Please notice that:
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>steps</strong> - the name of the funnel steps</li>
|
<li>Records would be sorted first</li>
|
||||||
<li><strong>values</strong> - value of the funnel steps</li>
|
<li>Value column only accept number for values</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-lg-6">Step Column Name</label>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<select ng-options="name for name in queryResult.getColumnNames()" ng-model="visualization.options.stepCol.colName" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-lg-6">Step Column Dispaly Name</label>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input type="text" ng-model="visualization.options.stepCol.dispAs" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-lg-6">Funnel Value Column Name</label>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<select ng-options="name for name in queryResult.getColumnNames()" ng-model="visualization.options.valueCol.colName" class="form-control"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-lg-6">Funnel Value Column Dispaly Name</label>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<input type="text" ng-model="visualization.options.valueCol.dispAs" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,19 +1,27 @@
|
|||||||
import { debounce, sortBy } from 'underscore';
|
import { debounce, sortBy, isNumber, every, difference } from 'underscore';
|
||||||
import d3 from 'd3';
|
import d3 from 'd3';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { ColorPalette } from '@/visualizations/chart/plotly/utils';
|
import { ColorPalette, normalizeValue } from '@/visualizations/chart/plotly/utils';
|
||||||
import editorTemplate from './funnel-editor.html';
|
import editorTemplate from './funnel-editor.html';
|
||||||
import './funnel.less';
|
import './funnel.less';
|
||||||
|
|
||||||
|
function isNoneNaNNum(val) {
|
||||||
|
if (!isNumber(val) || isNaN(val)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePercentage(num) {
|
||||||
|
return num < 0.01 ? '<0.01%' : num.toFixed(2) + '%';
|
||||||
|
}
|
||||||
|
|
||||||
function Funnel(scope, element) {
|
function Funnel(scope, element) {
|
||||||
this.element = element;
|
this.element = element;
|
||||||
this.watches = [];
|
this.watches = [];
|
||||||
const vis = d3.select(element);
|
const vis = d3.select(element);
|
||||||
|
const options = scope.visualization.options;
|
||||||
function normalizePercentage(num) {
|
|
||||||
return num < 0.01 ? '<0.01%' : num.toFixed(2) + '%';
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawFunnel(data) {
|
function drawFunnel(data) {
|
||||||
// Table
|
// Table
|
||||||
@ -22,8 +30,8 @@ function Funnel(scope, element) {
|
|||||||
|
|
||||||
// Header
|
// Header
|
||||||
const header = table.append('thead').append('tr');
|
const header = table.append('thead').append('tr');
|
||||||
header.append('th').text('Step');
|
header.append('th').text(options.stepCol.dispAs);
|
||||||
header.append('th');
|
header.append('th').attr('class', 'text-center').text(options.valueCol.dispAs);
|
||||||
header.append('th').attr('class', 'text-center').text('% Total');
|
header.append('th').attr('class', 'text-center').text('% Total');
|
||||||
header.append('th').attr('class', 'text-center').text('% Previous');
|
header.append('th').attr('class', 'text-center').text('% Previous');
|
||||||
|
|
||||||
@ -41,7 +49,6 @@ function Funnel(scope, element) {
|
|||||||
|
|
||||||
// Funnel bars
|
// Funnel bars
|
||||||
const valContainers = trs.append('td')
|
const valContainers = trs.append('td')
|
||||||
.attr('class', 'col-md-5')
|
|
||||||
.append('div')
|
.append('div')
|
||||||
.attr('class', 'container')
|
.attr('class', 'container')
|
||||||
.style('min-width', '200px');
|
.style('min-width', '200px');
|
||||||
@ -73,7 +80,6 @@ function Funnel(scope, element) {
|
|||||||
.text(d => normalizePercentage(d.pctPrevious));
|
.text(d => normalizePercentage(d.pctPrevious));
|
||||||
}
|
}
|
||||||
|
|
||||||
// visualize funnel data
|
|
||||||
function createVisualization(data) {
|
function createVisualization(data) {
|
||||||
drawFunnel(data); // draw funnel
|
drawFunnel(data); // draw funnel
|
||||||
}
|
}
|
||||||
@ -83,32 +89,46 @@ function Funnel(scope, element) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function prepareData(queryData, stepCol, valCol) {
|
function prepareData(queryData, stepCol, valCol) {
|
||||||
// Column validity
|
const data = queryData.map(row => ({
|
||||||
const sortedData = sortBy(queryData, valCol).reverse();
|
step: normalizeValue(row[stepCol]),
|
||||||
return sortedData.map((row, i) => ({
|
value: Number(row[valCol]),
|
||||||
step: row[stepCol],
|
|
||||||
value: row[valCol],
|
|
||||||
pctTotal: row[valCol] / sortedData[0][valCol] * 100.0,
|
|
||||||
pctPrevious: i === 0 ? 100.0 : row[valCol] / sortedData[i - 1][valCol] * 100.0,
|
|
||||||
}), []);
|
}), []);
|
||||||
|
const sortedData = sortBy(data, 'value').reverse();
|
||||||
|
|
||||||
|
// Column validity
|
||||||
|
if (sortedData[0].value === 0 || !every(sortedData, d => isNoneNaNNum(d.value))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sortedData.forEach((d, i) => {
|
||||||
|
d.pctTotal = d.value / sortedData[0].value * 100.0;
|
||||||
|
d.pctPrevious = i === 0 ? 100.0 : d.value / sortedData[i - 1].value * 100.0;
|
||||||
|
});
|
||||||
|
return sortedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(queryData) {
|
function invalidColNames() {
|
||||||
const data = prepareData(queryData, 'steps', 'values'); // build funnel data
|
const colNames = scope.queryResult.getColumnNames();
|
||||||
removeVisualization(); // remove existing visualization if any
|
const colToCheck = [options.stepCol.colName, options.valueCol.colName];
|
||||||
createVisualization(data); // visualize funnel data
|
if (difference(colToCheck, colNames).length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshData() {
|
function refresh() {
|
||||||
|
removeVisualization();
|
||||||
|
if (invalidColNames()) { return; }
|
||||||
|
|
||||||
const queryData = scope.queryResult.getData();
|
const queryData = scope.queryResult.getData();
|
||||||
if (queryData) {
|
const data = prepareData(queryData, options.stepCol.colName, options.valueCol.colName);
|
||||||
render(queryData);
|
if (data) {
|
||||||
|
createVisualization(data); // draw funnel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshData();
|
refresh();
|
||||||
this.watches.push(scope.$watch('visualization.options', refreshData, true));
|
this.watches.push(scope.$watch('visualization.options', refresh, true));
|
||||||
this.watches.push(scope.$watch('queryResult && queryResult.getData()', refreshData));
|
this.watches.push(scope.$watch('queryResult && queryResult.getData()', refresh));
|
||||||
}
|
}
|
||||||
|
|
||||||
Funnel.prototype.remove = function remove() {
|
Funnel.prototype.remove = function remove() {
|
||||||
@ -133,7 +153,7 @@ function funnelRenderer() {
|
|||||||
|
|
||||||
scope.handleResize = debounce(resize, 50);
|
scope.handleResize = debounce(resize, 50);
|
||||||
|
|
||||||
scope.$watch('visualization.options.height', (oldValue, newValue) => {
|
scope.$watch('visualization.options', (oldValue, newValue) => {
|
||||||
if (oldValue !== newValue) {
|
if (oldValue !== newValue) {
|
||||||
resize();
|
resize();
|
||||||
}
|
}
|
||||||
@ -159,7 +179,9 @@ export default function init(ngModule) {
|
|||||||
|
|
||||||
const editTemplate = '<funnel-editor></funnel-editor>';
|
const editTemplate = '<funnel-editor></funnel-editor>';
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
defaultrs: 7,
|
stepCol: { colName: '', dispAs: 'Steps' },
|
||||||
|
valueCol: { colName: '', dispAs: 'Value' },
|
||||||
|
defaultRows: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
VisualizationProvider.registerVisualization({
|
VisualizationProvider.registerVisualization({
|
||||||
|
Loading…
Reference in New Issue
Block a user