Skip to content

Commit

Permalink
Merge pull request #168 from redbearsam/feature/treemap_chart
Browse files Browse the repository at this point in the history
Feature/treemap chart
  • Loading branch information
matt-hooper authored Apr 25, 2019
2 parents 9503c18 + 814b84c commit 4edec9e
Show file tree
Hide file tree
Showing 17 changed files with 744 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<button id="goto-parent">Goto parent</button>
3 changes: 2 additions & 1 deletion packages/perspective-viewer-d3fc/src/js/charts/charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import heatmap from "./heatmap";
import ohlc from "./ohlc";
import candlestick from "./candlestick";
import sunburst from "./sunburst";
import treemap from "./treemap";

const chartClasses = [barChart, columnChart, lineChart, areaChart, yScatter, xyScatter, heatmap, ohlc, candlestick, sunburst];
const chartClasses = [barChart, columnChart, lineChart, areaChart, yScatter, xyScatter, heatmap, ohlc, candlestick, sunburst, treemap];

export default chartClasses;
55 changes: 17 additions & 38 deletions packages/perspective-viewer-d3fc/src/js/charts/sunburst.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,55 +8,34 @@
*/

import {select} from "d3";
import {treeColor} from "../series/sunburst/sunburstColor";
import {treeData} from "../data/treeData";
import {sunburstSeries, treeColor} from "../series/sunburst/sunburstSeries";
import {colorRangeLegend} from "../legend/colorRangeLegend";
import {sunburstSeries} from "../series/sunburst/sunburstSeries";
import {tooltip} from "../tooltip/tooltip";
import {getOrCreateElement} from "../utils/utils";
import {gridLayoutMultiChart} from "../layout/gridLayoutMultiChart";
import {colorRangeLegend} from "../legend/colorRangeLegend";

function sunburst(container, settings) {
if (settings.crossValues.length === 0) return;
if (settings.crossValues.length === 0) {
console.warn("Unable to render a chart in the absence of any groups.");
return;
}

const sunburstData = treeData(settings);
const innerContainer = getOrCreateElement(container, "div.inner-container", () => container.append("div").attr("class", "inner-container"));
const color = treeColor(settings, sunburstData.map(d => d.extents));
const data = treeData(settings);
const color = treeColor(settings, data.map(d => d.extents));
const sunburstGrid = gridLayoutMultiChart().elementsPrefix("sunburst");

container.datum(data).call(sunburstGrid);

const innerRect = innerContainer.node().getBoundingClientRect();
const containerHeight = innerRect.height;
const containerWidth = innerRect.width - (color ? 70 : 0);
if (color) {
const legend = colorRangeLegend().scale(color);
container.call(legend);
}

const minSize = 500;
const cols = Math.min(sunburstData.length, Math.floor(containerWidth / minSize));
const rows = Math.ceil(sunburstData.length / cols);
const containerSize = {
width: containerWidth / cols,
height: Math.min(containerHeight, Math.max(containerHeight / rows, containerWidth / cols))
};
if (containerHeight / rows > containerSize.height * 0.75) {
containerSize.height = containerHeight / rows;
}

innerContainer.style("grid-template-columns", `repeat(${cols}, ${containerSize.width}px)`);
innerContainer.style("grid-template-rows", `repeat(${rows}, ${containerSize.height}px)`);

const sunburstDiv = innerContainer.selectAll("div.sunburst-container").data(treeData(settings), d => d.split);
sunburstDiv.exit().remove();

const sunburstEnter = sunburstDiv
.enter()
.append("div")
.attr("class", "sunburst-container");

const sunburstContainer = sunburstEnter
.append("svg")
.append("g")
.attr("class", "sunburst");

sunburstContainer.append("text").attr("class", "title");
const sunburstContainer = sunburstGrid.chartContainer();
const sunburstEnter = sunburstGrid.chartEnter();
const sunburstDiv = sunburstGrid.chartDiv();
const containerSize = sunburstGrid.containerSize();

sunburstContainer
.append("circle")
Expand Down
72 changes: 72 additions & 0 deletions packages/perspective-viewer-d3fc/src/js/charts/treemap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/

import * as d3 from "d3";
import {treeColor} from "../series/treemap/treemapColor";
import {treeData} from "../data/treeData";
import {treemapSeries} from "../series/treemap/treemapSeries";
import {tooltip} from "../tooltip/tooltip";
import {gridLayoutMultiChart} from "../layout/gridLayoutMultiChart";
import {colorRangeLegend} from "../legend/colorRangeLegend";

function treemap(container, settings) {
if (settings.crossValues.length === 0) {
console.warn("Unable to render a chart in the absence of any groups.");
return;
}

const data = treeData(settings);
const color = treeColor(settings, data.map(d => d.data));
const treemapGrid = gridLayoutMultiChart().elementsPrefix("treemap");

container.datum(data).call(treemapGrid);

if (color) {
const legend = colorRangeLegend().scale(color);
container.call(legend);
}

const treemapContainer = treemapGrid.chartContainer();
const treemapEnter = treemapGrid.chartEnter();
const treemapDiv = treemapGrid.chartDiv();

treemapContainer.append("text").attr("class", "parent");
treemapEnter
.merge(treemapDiv)
.select("svg")
.select("g.treemap")
.each(function({split, data}) {
const treemapSvg = d3.select(this);
const svgNode = this.parentNode;
const {height} = svgNode.getBoundingClientRect();

const title = treemapSvg.select("text.title").text(split);
title.attr("transform", `translate(0, ${-(height / 2 - 5)})`);

treemapSeries()
.settings(settings)
.split(split)
.data(data)
.container(d3.select(d3.select(this.parentNode).node().parentNode))
.color(color)(treemapSvg);

tooltip().settings(settings)(treemapSvg.selectAll("g"));
});
}

treemap.plugin = {
type: "d3_treemap",
name: "[D3] Treemap",
max_size: 25000,
initial: {
type: "number",
count: 2
}
};
export default treemap;
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/

import {getOrCreateElement} from "../utils/utils";

export function gridLayoutMultiChart() {
let elementsPrefix = "element-prefix-unset";

let chartContainer = null;
let chartEnter = null;
let chartDiv = null;
let color = null;
let containerSize = null;

const _gridLayoutMultiChart = container => {
const innerContainer = getOrCreateElement(container, "div.inner-container", () => container.append("div").attr("class", "inner-container"));

const innerRect = innerContainer.node().getBoundingClientRect();
const containerHeight = innerRect.height;
const containerWidth = innerRect.width - (color ? 70 : 0);

const minSize = 500;
const data = container.datum();
const cols = Math.min(data.length, Math.floor(containerWidth / minSize));
const rows = Math.ceil(data.length / cols);
containerSize = {
width: containerWidth / cols,
height: Math.min(containerHeight, Math.max(containerHeight / rows, containerWidth / cols))
};
if (containerHeight / rows > containerSize.height * 0.75) {
containerSize.height = containerHeight / rows;
}

innerContainer.style("grid-template-columns", `repeat(${cols}, ${containerSize.width}px)`);
innerContainer.style("grid-template-rows", `repeat(${rows}, ${containerSize.height}px)`);

chartDiv = innerContainer.selectAll(`div.${elementsPrefix}-container`).data(data, d => d.split);
chartDiv.exit().remove();

chartEnter = chartDiv
.enter()
.append("div")
.attr("class", `${elementsPrefix}-container`);

chartContainer = chartEnter
.append("svg")
.append("g")
.attr("class", elementsPrefix);

chartContainer.append("text").attr("class", "title");
};

_gridLayoutMultiChart.elementsPrefix = (...args) => {
if (!args.length) {
return elementsPrefix;
}
elementsPrefix = args[0];
return _gridLayoutMultiChart;
};

_gridLayoutMultiChart.chartContainer = () => chartContainer;

_gridLayoutMultiChart.chartEnter = () => chartEnter;

_gridLayoutMultiChart.chartDiv = () => chartDiv;

_gridLayoutMultiChart.containerSize = () => containerSize;

return _gridLayoutMultiChart;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/

import {flattenExtent} from "../../axis/flatten";
import {seriesColorRange} from "../seriesRange";

export function treeColor(settings, extents) {
if (settings.mainValues.length > 1) {
return seriesColorRange(settings, null, null, flattenExtent(extents));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
*
*/

import {flattenExtent} from "../../axis/flatten";
import {seriesColorRange} from "../../series/seriesRange";
import {drawArc, arcVisible} from "./sunburstArc";
import {labelVisible, labelTransform, cropLabel} from "./sunburstLabel";
import {clickHandler} from "./sunburstClick";
Expand Down Expand Up @@ -112,9 +110,3 @@ export function sunburstSeries() {

return _sunburstSeries;
}

export function treeColor(settings, extents) {
if (settings.mainValues.length > 1) {
return seriesColorRange(settings, null, null, flattenExtent(extents));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/

import * as d3 from "d3";
import {calcWidth, calcHeight} from "./treemapSeries";
import {toggleLabels, preventTextCollisions} from "./treemapLabel";
import {calculateSubTreeMap} from "./treemapLevelCalculation";

export function changeLevel(d, rects, nodesMerge, labels, settings, treemapDiv, treemapSvg, rootNode, parentCtrls) {
settings.treemapLevel = d.depth;
const crossValues = d.crossValue.split("|");

if (!d.mapLevel[settings.treemapLevel] || !d.mapLevel[settings.treemapLevel].visible) {
calculateSubTreeMap(d, crossValues, nodesMerge, settings, rootNode);
}

const parent = d.parent;

const t = treemapSvg
.transition()
.duration(350)
.ease(d3.easeCubicOut);

nodesMerge.each(d => (d.target = d.mapLevel[settings.treemapLevel]));

rects
.transition(t)
.filter(d => d.target.visible)
.tween("data", d => {
const i = d3.interpolate(d.current, d.target);
return t => (d.current = i(t));
})
.styleTween("x", d => () => `${d.current.x0}px`)
.styleTween("y", d => () => `${d.current.y0}px`)
.styleTween("width", d => () => `${d.current.x1 - d.current.x0}px`)
.styleTween("height", d => () => `${d.current.y1 - d.current.y0}px`);

labels
.transition(t)
.filter(d => d.target.visible)
.tween("data", d => {
const i = d3.interpolate(d.current, d.target);
return t => (d.current = i(t));
})
.attrTween("x", d => () => d.current.x0 + calcWidth(d.current) / 2)
.attrTween("y", d => () => d.current.y0 + calcHeight(d.current) / 2)
.end()
.then(() => preventTextCollisions(nodesMerge));

// hide hidden svgs
nodesMerge
.transition(t)
.tween("data", d => {
const i = d3.interpolate(d.current, d.target);
return t => (d.current = i(t));
})
.styleTween("opacity", d => () => d.current.opacity)
.attrTween("pointer-events", d => () => (d.target.visible ? "all" : "none"));

toggleLabels(nodesMerge, settings.treemapLevel, crossValues);

if (parent) {
parentCtrls
.hide(false)
.text(d.data.name)
.onClick(() => changeLevel(parent, rects, nodesMerge, labels, settings, treemapDiv, treemapSvg, rootNode, parentCtrls))();
} else {
parentCtrls.hide(true)();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/

import {seriesColorRange} from "../seriesRange";

export function treeColor(settings, data) {
if (settings.mainValues.length <= 1) return;
const colors = data
.filter(x => x.height > 0)
.map(x => getColors(x))
.reduce((a, b) => a.concat(b));
let min = Math.min(...colors);
let max = Math.max(...colors);
return seriesColorRange(settings, null, null, [min, max]);
}

// only get the colors from the bottom level (e.g. nodes with no children)
function getColors(nodes, colors = []) {
nodes.children && nodes.children.length > 0 ? nodes.children.forEach(child => colors.concat(getColors(child, colors))) : colors.push(nodes.data.color);
return colors;
}
Loading

0 comments on commit 4edec9e

Please sign in to comment.