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>
This visualization expects the query result to be in the following formats:
This visualization constructs funnel chart. Please notice that:
<ul>
<li><strong>steps</strong> - the name of the funnel steps</li>
<li><strong>values</strong> - value of the funnel steps</li>
<li>Records would be sorted first</li>
<li>Value column only accept number for values</li>
</ul>
</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>

View File

@ -1,19 +1,27 @@
import { debounce, sortBy } from 'underscore';
import { debounce, sortBy, isNumber, every, difference } from 'underscore';
import d3 from 'd3';
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 './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) {
this.element = element;
this.watches = [];
const vis = d3.select(element);
function normalizePercentage(num) {
return num < 0.01 ? '<0.01%' : num.toFixed(2) + '%';
}
const options = scope.visualization.options;
function drawFunnel(data) {
// Table
@ -22,8 +30,8 @@ function Funnel(scope, element) {
// Header
const header = table.append('thead').append('tr');
header.append('th').text('Step');
header.append('th');
header.append('th').text(options.stepCol.dispAs);
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('% Previous');
@ -41,7 +49,6 @@ function Funnel(scope, element) {
// Funnel bars
const valContainers = trs.append('td')
.attr('class', 'col-md-5')
.append('div')
.attr('class', 'container')
.style('min-width', '200px');
@ -73,7 +80,6 @@ function Funnel(scope, element) {
.text(d => normalizePercentage(d.pctPrevious));
}
// visualize funnel data
function createVisualization(data) {
drawFunnel(data); // draw funnel
}
@ -83,32 +89,46 @@ function Funnel(scope, element) {
}
function prepareData(queryData, stepCol, valCol) {
// Column validity
const sortedData = sortBy(queryData, valCol).reverse();
return sortedData.map((row, i) => ({
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 data = queryData.map(row => ({
step: normalizeValue(row[stepCol]),
value: Number(row[valCol]),
}), []);
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) {
const data = prepareData(queryData, 'steps', 'values'); // build funnel data
removeVisualization(); // remove existing visualization if any
createVisualization(data); // visualize funnel data
function invalidColNames() {
const colNames = scope.queryResult.getColumnNames();
const colToCheck = [options.stepCol.colName, options.valueCol.colName];
if (difference(colToCheck, colNames).length > 0) {
return true;
}
return false;
}
function refreshData() {
function refresh() {
removeVisualization();
if (invalidColNames()) { return; }
const queryData = scope.queryResult.getData();
if (queryData) {
render(queryData);
const data = prepareData(queryData, options.stepCol.colName, options.valueCol.colName);
if (data) {
createVisualization(data); // draw funnel
}
}
refreshData();
this.watches.push(scope.$watch('visualization.options', refreshData, true));
this.watches.push(scope.$watch('queryResult && queryResult.getData()', refreshData));
refresh();
this.watches.push(scope.$watch('visualization.options', refresh, true));
this.watches.push(scope.$watch('queryResult && queryResult.getData()', refresh));
}
Funnel.prototype.remove = function remove() {
@ -133,7 +153,7 @@ function funnelRenderer() {
scope.handleResize = debounce(resize, 50);
scope.$watch('visualization.options.height', (oldValue, newValue) => {
scope.$watch('visualization.options', (oldValue, newValue) => {
if (oldValue !== newValue) {
resize();
}
@ -159,7 +179,9 @@ export default function init(ngModule) {
const editTemplate = '<funnel-editor></funnel-editor>';
const defaultOptions = {
defaultrs: 7,
stepCol: { colName: '', dispAs: 'Steps' },
valueCol: { colName: '', dispAs: 'Value' },
defaultRows: 10,
};
VisualizationProvider.registerVisualization({