Skip to content

Commit

Permalink
Merge pull request #131 from jpmorganchase/multi-chart
Browse files Browse the repository at this point in the history
Multi chart mode for Treemap and Sunburst charts
  • Loading branch information
texodus authored Jun 11, 2018
2 parents 61a9033 + 99e8cdf commit 09ac254
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 91 deletions.
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
"_puppeteer": "docker run -it --rm --shm-size=2g -u root -e WRITE_TESTS=${WRITE_TESTS} -v $(pwd):/src -w /src/packages/${PACKAGE} perspective/puppeteer",
"_test": "./node_modules/.bin/jest --runInBand",
"_quiet_test": "lerna run test ${PACKAGE:+--scope=@jpmorganchase/${PACKAGE}}",
"_test_perspective": "PACKAGE=perspective npm run _puppeteer -- npm run _test",
"_test_viewer": "PACKAGE=perspective-viewer npm run _puppeteer -- npm run _test",
"_test_hypergrid": "PACKAGE=perspective-viewer-hypergrid npm run _puppeteer -- npm run _test",
"_test_highcharts": "PACKAGE=perspective-viewer-highcharts npm run _puppeteer -- npm run _test"
"_test_perspective": "PACKAGE=perspective npm run _puppeteer -- npm run test",
"_test_viewer": "PACKAGE=perspective-viewer npm run _puppeteer -- npm run test",
"_test_hypergrid": "PACKAGE=perspective-viewer-hypergrid npm run _puppeteer -- npm run test",
"_test_highcharts": "PACKAGE=perspective-viewer-highcharts npm run _puppeteer -- npm run test"
}
}
100 changes: 65 additions & 35 deletions packages/perspective-viewer-highcharts/src/js/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,22 @@ export const draw = (mode) => async function(el, view, task) {
return;
}

if (this.hasAttribute('updating') && this._chart) {
chart = this._chart
this._chart = undefined;
try {
chart.destroy();
} catch (e) {
console.warn("Scatter plot destroy() call failed - this is probably leaking memory");
if (!this._charts) {
this._charts = [];
}

if (this.hasAttribute('updating') && this._charts.length > 0) {
for (let chart of this._charts) {
try {
chart.destroy();
} catch (e) {
console.warn("Scatter plot destroy() call failed - this is probably leaking memory");
}
}
this._charts = [];
}

let config = default_config(aggregates, mode, js, col_pivots),
let configs = [],
xaxis_name = aggregates.length > 0 ? aggregates[0].column : undefined,
xaxis_type = schema[xaxis_name],
yaxis_name = aggregates.length > 1 ? aggregates[1].column : undefined,
Expand All @@ -50,6 +55,7 @@ export const draw = (mode) => async function(el, view, task) {
num_aggregates = aggregates.length - hidden.length;

if (mode === 'scatter') {
let config = configs[0] = default_config(aggregates, mode, js, col_pivots);
let [series, xtop, colorRange, ytop] = make_xy_data(js, schema, aggregates.map(x => x.column), row_pivots, col_pivots, hidden);
config.legend.floating = series.length <= 20;
config.legend.enabled = col_pivots.length > 0;
Expand All @@ -70,6 +76,7 @@ export const draw = (mode) => async function(el, view, task) {
set_both_axis(config, 'yAxis', yaxis_name, yaxis_type, ytree_type, ytop);
set_tick_size.call(this, config);
} else if (mode === 'heatmap') {
let config = configs[0] = default_config(aggregates, mode, js, col_pivots);
let [series, top, ytop, colorRange] = make_xyz_data(js, row_pivots, hidden);
config.series = [{
name: null,
Expand All @@ -85,14 +92,22 @@ export const draw = (mode) => async function(el, view, task) {
set_category_axis(config, 'yAxis', ytree_type, ytop);

} else if (mode === "treemap" || mode === "sunburst") {
let [series, , colorRange] = make_tree_data(js, row_pivots, hidden, aggregates);
config.series = series;
config.plotOptions.series.borderWidth = 1;
config.legend.floating = false;
if (colorRange) {
color_axis.call(this, config, colorRange);
let [charts, , colorRange] = make_tree_data(js, row_pivots, hidden, aggregates, mode === "treemap");
for (let series of charts) {
let config = default_config(aggregates, mode, js, col_pivots);
config.series = [series];
if (charts.length > 1) {
config.title.text = series.title;
}
config.plotOptions.series.borderWidth = 1;
config.legend.floating = false;
if (colorRange) {
color_axis.call(this, config, colorRange);
}
configs.push(config);
}
} else if (mode === 'line') {
let config = configs[0] = default_config(aggregates, mode, js, col_pivots);
let [series, xtop, , ytop] = make_xy_data(js, schema, aggregates.map(x => x.column), row_pivots, col_pivots, hidden);
const colors = series.length <= 10 ? COLORS_10 : COLORS_20;
config.legend.floating = series.length <= 20;
Expand All @@ -106,6 +121,7 @@ export const draw = (mode) => async function(el, view, task) {
set_both_axis(config, 'xAxis', xaxis_name, xaxis_type, xtree_type, xtop);
set_both_axis(config, 'yAxis', yaxis_name, yaxis_type, ytree_type, ytop);
} else {
let config = configs[0] = default_config(aggregates, mode, js, col_pivots);
let [series, top, ] = make_y_data(js, row_pivots, hidden);
config.series = series;
config.colors = series.length <= 10 ? COLORS_10 : COLORS_20;
Expand All @@ -129,33 +145,47 @@ export const draw = (mode) => async function(el, view, task) {
});
}

if (this._chart) {
if (mode === 'scatter') {
let conf = {
series: config.series,
plotOptions: {}
};
set_tick_size.call(this, conf);
this._chart.update(conf);
} else if (mode.indexOf('line') > -1) {
this._chart.update({
series: config.series
});
} else {
let opts = {series: config.series, xAxis: config.xAxis, yAxis: config.yAxis};
this._chart.update(opts);
if (this._charts.length > 0) {
let idx = 0;
for (let chart of this._charts) {
let config = configs[idx++];
if (mode === 'scatter') {
let conf = {
series: config.series,
plotOptions: {}
};
set_tick_size.call(this, conf);
chart.update(conf);
} else if (mode.indexOf('line') > -1) {
chart.update({
series: config.series
});
} else {
let opts = {series: config.series, xAxis: config.xAxis, yAxis: config.yAxis};
chart.update(opts);
}
}
} else {
var chart = document.createElement('div');
chart.className = 'chart';
this._charts = [];
for (let e of Array.prototype.slice.call(el.children)) { el.removeChild(e); }
el.appendChild(chart);
this._chart = Highcharts.chart(chart, config);
for (let config of configs) {
let chart = document.createElement('div');
chart.className = 'chart';
el.appendChild(chart);
this._charts.push(() => Highcharts.chart(chart, config));
}

this._charts = this._charts.map(x => x());
}

if (!document.contains(this._chart.renderTo)) {
if (!this._charts.every(x => document.contains(x.renderTo))) {
for (let e of Array.prototype.slice.call(el.children)) { el.removeChild(e); }
el.appendChild(this._chart.renderTo);
this._charts.map(x => el.appendChild(x.renderTo));
}

// TODO resize bug in Highcharts?
if (configs.length > 1) {
this._charts.map(x => x.reflow());
}
}

14 changes: 7 additions & 7 deletions packages/perspective-viewer-highcharts/src/js/highcharts.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@
import {draw} from "./draw.js";

function resize(immediate) {
if (this._chart && !this._resize_timer) {
this._chart.reflow();
if (this._charts && this._charts.length > 0 && !this._resize_timer) {
this._charts.map(x => x.reflow());
}
if (this._resize_timer) {
clearTimeout(this._resize_timer);
this._debounce_resize = true;
}
this._resize_timer = setTimeout(() => {
if (this._chart && !document.hidden && this.offsetParent && document.contains(this) && this._debounce_resize) {
this._chart.reflow();
if (this._charts && this._charts.length > 0 && !document.hidden && this.offsetParent && document.contains(this) && this._debounce_resize) {
this._charts.map(x => x.reflow());
}
this._resize_timer = undefined;
this._debounce_resize = false;
Expand All @@ -28,9 +28,9 @@ function resize(immediate) {
}

function delete_chart() {
if (this._chart) {
this._chart.destroy();
this._chart = undefined;
if (this._charts && this._charts.length > 0) {
this._charts.map(x => x.destroy());
this._charts = [];
}
}

Expand Down
132 changes: 96 additions & 36 deletions packages/perspective-viewer-highcharts/src/js/series.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@
* Y
*/

import {COLORS_10, COLORS_20} from "./externals.js";

function row_to_series(series, sname, gname) {
let s;
for (var sidx = 0; sidx < series.length; sidx++) {
Expand Down Expand Up @@ -88,7 +86,6 @@ class ColumnsIterator {
}

*[Symbol.iterator]() {
let series = [];
for (let row of this.rows) {
if (this.columns === undefined) {
this.columns = Object.keys(row).filter(prop => {
Expand Down Expand Up @@ -322,40 +319,101 @@ export function make_xyz_data(js, pivots, hidden) {
* Tree
*/

function recolor(aggregates, series) {
let color, colorAxis, colorRange;
function recolor(aggregates, all, leaf_only) {
let colorRange;
if (aggregates.length >= 2) {
color = aggregates[1]['column'];
let colorvals = series[1]['data'];
colorRange = [Infinity, -Infinity];
for (let i = 0; i < colorvals.length; ++i) {
colorRange[0] = Math.min(colorRange[0], colorvals[i]);
colorRange[1] = Math.max(colorRange[1], colorvals[i]);
for (let series of all) {
let colorvals = series['data'];
for (let i = 1; i < colorvals.length; ++i) {
if ((leaf_only && colorvals[i].leaf) || !leaf_only) {
colorRange[0] = Math.min(colorRange[0], colorvals[i].colorValue);
colorRange[1] = Math.max(colorRange[1], colorvals[i].colorValue);
}
}
if (colorRange[0] * colorRange[1] < 0) {
let cmax = Math.max(Math.abs(colorRange[0]), Math.abs(colorRange[1]));
colorRange = [-cmax, cmax];
}
}
}
return colorRange;
}

class TreeIterator {

constructor(depth, json) {
this.depth = depth;
this.json = json;
this.top = {name: "", depth: 0, categories: []};
}

add_label(path) {
let label = {
name: path[path.length - 1],
depth: path.length,
categories: []
}
if (colorRange[0] * colorRange[1] < 0) {
let cmax = Math.max(Math.abs(colorRange[0]), Math.abs(colorRange[1]));
colorRange = [-cmax, cmax];

// Find the correct parent
var parent = this.top;
for (var lidx = 0; lidx < path.length - 1; lidx++) {
for (var cidx = 0; cidx < parent.categories.length; cidx++) {
if (parent.categories[cidx].name === path[lidx]) {
parent = parent.categories[cidx];
break;
}
}
}
parent.categories.push(label);
return label;
}

*[Symbol.iterator]() {
let label = this.top;
for (let row of this.json) {
let path = row.__ROW_PATH__ || [''];
if (path.length > 0 && path.length < this.depth) {
label = this.add_label(path);
} else if (path.length >= this.depth) {
label.categories.push(path[path.length - 1]);
}
yield row;
}
}
return [color, colorRange];
}

function repivot(aggregates, js, row_pivots, color) {
let data = [];
let size = aggregates[0]['column'];
export function make_tree_data(js, row_pivots, hidden, aggregates, leaf_only) {
let rows = new TreeIterator(row_pivots.length, js);
let rows2 = new ColumnsIterator(rows, hidden);
var series = [];
let configs = [];

for (let row of js.slice(1)) {
for (let row of rows2) {
let rp = row['__ROW_PATH__'];
let id = rp.join("_");
let name = rp.slice(-1)[0];
let parent = rp.slice(0, -1).join("_");
let value = row[size];
let colorValue = row[color];

data.push({
id: id, name: name, value: value, colorValue: colorValue, parent: parent}
);

for (let idx = 0; idx < rows2.columns.length; idx++) {
let prop = rows2.columns[idx];
let sname = prop.split(',');
let gname = sname[sname.length - 1];
sname = sname.slice(0, sname.length - 1).join(", ") || " ";
if (idx % aggregates.length === 0) {
let s = row_to_series(series, sname, gname);
s.data.push({
id: id,
name: name,
value: row[prop],
colorValue: aggregates.length > 1 ? row[rows2.columns[idx + 1]] : undefined,
parent: parent,
leaf: row.__ROW_PATH__.length === row_pivots.length
});
}
}
}

let levels = [];
for (let i = 0; i < row_pivots.length; i++) {
levels.push({
Expand All @@ -372,18 +430,20 @@ function repivot(aggregates, js, row_pivots, color) {
},
});
}
return [{
layoutAlgorithm: 'squarified',
allowDrillToNode: true,
alternateStartingDirection: true,
data: data,
levels: levels
}];
}

export function make_tree_data(js, row_pivots, hidden, aggregates) {
let [series, top] = make_y_data(js, row_pivots, hidden);
let [color, colorRange] = recolor(aggregates, series);
return [repivot(aggregates, js, row_pivots, color), top, colorRange];
for (let data of series) {
let title = data.name.split(',');
configs.push({
layoutAlgorithm: 'squarified',
allowDrillToNode: true,
alternateStartingDirection: true,
data: data.data.slice(1),
levels: levels,
title: title
});
}

let colorRange = recolor(aggregates, series, leaf_only, row_pivots);
return [configs, rows.top, colorRange];
}

Loading

0 comments on commit 09ac254

Please sign in to comment.