Add support for column mapping

This commit is contained in:
Hao Jiang 2018-02-23 08:47:23 +09:00
parent a333abcaa5
commit e78bfb2e9a
2 changed files with 79 additions and 32 deletions

View File

@ -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>

View File

@ -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({