diff --git a/packages/perspective-viewer-d3fc/src/html/parent-controls.html b/packages/perspective-viewer-d3fc/src/html/parent-controls.html new file mode 100644 index 0000000000..251b739a2d --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/html/parent-controls.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/perspective-viewer-d3fc/src/js/charts/charts.js b/packages/perspective-viewer-d3fc/src/js/charts/charts.js index 9ad16cb3ac..0d1450ef20 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/charts.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/charts.js @@ -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; diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 581a47045f..65a4a44b09 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -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") diff --git a/packages/perspective-viewer-d3fc/src/js/charts/treemap.js b/packages/perspective-viewer-d3fc/src/js/charts/treemap.js new file mode 100644 index 0000000000..b1dd1671f1 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/charts/treemap.js @@ -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; diff --git a/packages/perspective-viewer-d3fc/src/js/layout/gridLayoutMultiChart.js b/packages/perspective-viewer-d3fc/src/js/layout/gridLayoutMultiChart.js new file mode 100644 index 0000000000..127ef97768 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/layout/gridLayoutMultiChart.js @@ -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; +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstColor.js b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstColor.js new file mode 100644 index 0000000000..62d8b8979c --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstColor.js @@ -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)); + } +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstSeries.js b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstSeries.js index 302a099551..ce7817734c 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstSeries.js @@ -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"; @@ -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)); - } -} diff --git a/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapClick.js b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapClick.js new file mode 100644 index 0000000000..050883c974 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapClick.js @@ -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)(); + } +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapColor.js b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapColor.js new file mode 100644 index 0000000000..081916739c --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapColor.js @@ -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; +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapControls.js b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapControls.js new file mode 100644 index 0000000000..6b44e92bd8 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapControls.js @@ -0,0 +1,59 @@ +/****************************************************************************** + * + * 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"; +import template from "../../../html/parent-controls.html"; + +export function parentControls(container) { + let onClick = null; + let text = null; + let hide = true; + + const parent = getOrCreateElement(container, ".parent-controls", () => + container + .append("div") + .attr("class", "parent-controls") + .style("display", hide ? "none" : "") + .html(template) + ); + + const controls = () => { + parent + .style("display", hide ? "none" : "") + .select("#goto-parent") + .html(`⇪ ${text}`) + .on("click", () => onClick()); + }; + + controls.hide = (...args) => { + if (!args.length) { + return hide; + } + hide = args[0]; + return controls; + }; + + controls.text = (...args) => { + if (!args.length) { + return text; + } + text = args[0]; + return controls; + }; + + controls.onClick = (...args) => { + if (!args.length) { + return onClick; + } + onClick = args[0]; + return controls; + }; + + return controls; +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLabel.js b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLabel.js new file mode 100644 index 0000000000..b40e95bdb2 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLabel.js @@ -0,0 +1,69 @@ +/****************************************************************************** + * + * 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 {select} from "d3"; +import {isElementOverlapping} from "../../utils/utils"; +import {calcWidth, calcHeight} from "./treemapSeries"; + +export const drawLabels = (nodes, treemapLevel, crossValues) => { + nodes + .selectAll("text") + .attr("x", d => d.x0 + calcWidth(d) / 2) + .attr("y", d => d.y0 + calcHeight(d) / 2) + .attr("class", d => textLevelHelper(d, treemapLevel, crossValues)); + nodes.selectAll("text").each((_, i, nodes) => centerText(nodes[i])); + preventTextCollisions(nodes); +}; + +export const toggleLabels = (nodes, treemapLevel, crossValues) => { + nodes.selectAll("text").attr("class", d => textLevelHelper(d, treemapLevel, crossValues)); + preventTextCollisions(nodes); +}; + +export const preventTextCollisions = nodes => { + const textCollisionFuzzFactorPx = -2; + const textAdjustPx = 16; + const rect = element => element.getBoundingClientRect(); + + const topNodes = []; + nodes + .selectAll("text") + .filter((_, i, nodes) => select(nodes[i]).attr("class") === textVisability.high) + .each((_, i, nodes) => topNodes.push(nodes[i])); + + nodes + .selectAll("text") + .filter((_, i, nodes) => select(nodes[i]).attr("class") === textVisability.low) + .each((_, i, nodes) => { + const lowerNode = nodes[i]; + topNodes + .filter(topNode => isElementOverlapping("x", rect(topNode), rect(lowerNode)) && isElementOverlapping("y", rect(topNode), rect(lowerNode), textCollisionFuzzFactorPx)) + .forEach(() => select(lowerNode).attr("dy", Number(select(lowerNode).attr("dy")) + textAdjustPx)); + }); +}; + +export const centerText = node => select(node).attr("dx", select(node).attr("dx") - node.getBoundingClientRect().width / 2); + +const textLevelHelper = (d, treemapLevel, crossValues) => { + if (!crossValues.filter(x => x !== "").every(x => d.crossValue.split("|").includes(x))) return textVisability.zero; + switch (d.depth) { + case treemapLevel + 1: + return textVisability.high; + case treemapLevel + 2: + return textVisability.low; + default: + return textVisability.zero; + } +}; + +const textVisability = { + high: "top", + low: "mid", + zero: "lower" +}; diff --git a/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLayout.js b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLayout.js new file mode 100644 index 0000000000..f4a647d14c --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLayout.js @@ -0,0 +1,22 @@ +/****************************************************************************** + * + * 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"; + +export default (width, height) => { + const padding = 30; + const treemapLayout = d3 + .treemap() + .size([width - padding, height - padding]) + .paddingInner(d => 1 + 2 * d.height); + + treemapLayout.tile(d3.treemapBinary); + + return treemapLayout; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLevelCalculation.js b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLevelCalculation.js new file mode 100644 index 0000000000..80b807b7bf --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapLevelCalculation.js @@ -0,0 +1,48 @@ +/****************************************************************************** + * + * 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 {calcWidth, calcHeight} from "./treemapSeries"; + +const includesAllCrossValues = (d, crossValues) => crossValues.every(val => d.crossValue.split("|").includes(val)); + +export function calculateSubTreeMap(d, crossValues, nodesMerge, settings, rootNode) { + const oldDimensions = {x: d.x0, y: d.y0, width: d.x1 - d.x0, height: d.y1 - d.y0}; + const newDimensions = {width: rootNode.x1 - rootNode.x0, height: rootNode.y1 - rootNode.y0}; + const dimensionMultiplier = {width: newDimensions.width / oldDimensions.width, height: newDimensions.height / oldDimensions.height}; + + nodesMerge.each(d => { + const x0 = (d.x0 - oldDimensions.x) * dimensionMultiplier.width; + const y0 = (d.y0 - oldDimensions.y) * dimensionMultiplier.height; + const width = calcWidth(d) * dimensionMultiplier.width; + const height = calcHeight(d) * dimensionMultiplier.height; + const visible = includesAllCrossValues(d, crossValues) && d.data.name != crossValues[settings.treemapLevel - 1]; + d.mapLevel[settings.treemapLevel] = { + x0, + x1: width + x0, + y0, + y1: height + y0, + visible, + opacity: visible ? 1 : 0 + }; + }); +} + +export function calculateRootLevelMap(nodesMerge, settings) { + nodesMerge.each(d => { + d.mapLevel = []; + d.mapLevel[settings.treemapLevel] = { + x0: d.x0, + x1: calcWidth(d) + d.x0, + y0: d.y0, + y1: calcHeight(d) + d.y0, + visible: true, + opacity: 1 + }; + }); +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapSeries.js b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapSeries.js new file mode 100644 index 0000000000..e3fe9769a2 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/treemap/treemapSeries.js @@ -0,0 +1,115 @@ +/****************************************************************************** + * + * 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 {drawLabels} from "./treemapLabel"; +import treemapLayout from "./treemapLayout"; +import {changeLevel} from "./treemapClick"; +import {parentControls} from "./treemapControls"; +import {calculateRootLevelMap} from "./treemapLevelCalculation"; + +export const nodeLevel = {leaf: "leafnode", branch: "branchnode", root: "rootnode"}; +export const calcWidth = d => d.x1 - d.x0; +export const calcHeight = d => d.y1 - d.y0; +const isLeafNode = (maxDepth, d) => d.depth === maxDepth; +const nodeLevelHelper = (maxDepth, d) => (d.depth === 0 ? nodeLevel.root : isLeafNode(maxDepth, d) ? nodeLevel.leaf : nodeLevel.branch); + +export function treemapSeries() { + let settings = null; + let split = null; + let data = null; + let color = null; + let treemapDiv = null; + let parentCtrls = null; + + const _treemapSeries = treemapSvg => { + parentCtrls = parentControls(treemapDiv); + parentCtrls(); + + const maxDepth = data.height; + settings.treemapLevel = 0; + const treemap = treemapLayout(treemapDiv.node().getBoundingClientRect().width, treemapDiv.node().getBoundingClientRect().height); + treemap(data); + + // Draw child nodes first + const nodes = treemapSvg + .selectAll("g") + .data(data.descendants()) + .enter() + .append("g") + .sort((a, b) => b.depth - a.depth); + + nodes.append("rect"); + nodes.append("text"); + + const nodesMerge = nodes.merge(nodes); + + const rects = nodesMerge + .select("rect") + .attr("class", d => `treerect ${nodeLevelHelper(maxDepth, d)}`) + .style("x", d => d.x0) + .style("y", d => d.y0) + .style("width", d => calcWidth(d)) + .style("height", d => calcHeight(d)); + color && rects.style("fill", d => color(d.data.color)); + + const labels = nodesMerge + .select("text") + .attr("x", d => d.x0 + calcWidth(d) / 2) + .attr("y", d => d.y0 + calcHeight(d) / 2) + .text(d => d.data.name); + + drawLabels(nodesMerge, settings.treemapLevel, []); + + calculateRootLevelMap(nodesMerge, settings); + const rootNode = rects.filter(d => d.crossValue === "").datum(); + rects.filter(d => d.children).on("click", d => changeLevel(d, rects, nodesMerge, labels, settings, treemapDiv, treemapSvg, rootNode, parentCtrls)); + }; + + _treemapSeries.settings = (...args) => { + if (!args.length) { + return settings; + } + settings = args[0]; + return _treemapSeries; + }; + + _treemapSeries.split = (...args) => { + if (!args.length) { + return split; + } + split = args[0]; + return _treemapSeries; + }; + + _treemapSeries.data = (...args) => { + if (!args.length) { + return data; + } + data = args[0]; + return _treemapSeries; + }; + + _treemapSeries.color = (...args) => { + if (!args.length) { + return color; + } + color = args[0]; + return _treemapSeries; + }; + + _treemapSeries.container = (...args) => { + if (!args.length) { + return treemapDiv; + } + treemapDiv = args[0]; + return _treemapSeries; + }; + + return _treemapSeries; +} diff --git a/packages/perspective-viewer-d3fc/src/js/utils/utils.js b/packages/perspective-viewer-d3fc/src/js/utils/utils.js index 8e1778d458..a8b1e44f29 100644 --- a/packages/perspective-viewer-d3fc/src/js/utils/utils.js +++ b/packages/perspective-viewer-d3fc/src/js/utils/utils.js @@ -27,3 +27,19 @@ export function isElementOverflowing(containerRect, innerElementRect, direction throw `Direction being checked for overflow is invalid: ${direction}`; } + +export function isElementOverlapping(axis, immovableRect, elementRect, fuzz = 0) { + const dimension = axis === "x" ? "width" : "height"; + + const immovableInnerPoint = immovableRect[axis]; + const immovableOuterPoint = immovableRect[axis] + immovableRect[dimension]; + + const elementInnerPoint = elementRect[axis]; + const elementOuterPoint = elementRect[axis] + elementRect[dimension]; + + const innerPointInside = elementInnerPoint + fuzz > immovableInnerPoint && elementInnerPoint - fuzz < immovableOuterPoint; + const outerPointInside = elementOuterPoint + fuzz > immovableInnerPoint && elementOuterPoint - fuzz < immovableOuterPoint; + const pointsEitherSide = elementInnerPoint + fuzz < immovableInnerPoint && elementOuterPoint - fuzz > immovableOuterPoint; + + return innerPointInside || outerPointInside || pointsEitherSide; +} diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index 7607e2b2dd..63dbf7b6c3 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -54,7 +54,7 @@ text-anchor: middle; user-select: none; pointer-events: none; - fill: var(--d3fc-sunburst--labels, rgb(51, 51, 51)); + fill: var(--d3fc-treedata--labels, rgb(51, 51, 51)); &.title { dominant-baseline: hanging; @@ -63,6 +63,104 @@ } } + &.d3_treemap { + padding: 0; + + & .treemap-container { + position: relative; + } + + & .inner-container { + width: 100%; + height: 100%; + display: inline-grid; + padding: 0; + margin: 0; + overflow-y: auto; + overflow-x: hidden; + + & div { + overflow: hidden; + } + + & svg { + width: 100%; + height: 100%; + } + + & .treerect { + stroke: var(--d3fc-axis--lines, white); + fill: var(--d3fc-series, rgba(31, 119, 180, 0.5)); + &:hover { + cursor: pointer; + stroke: var(--d3fc-treedata--hover-highlight, black); + stroke-opacity: 1; + } + } + & .rootnode { + opacity: 0; + pointer-events: none; + z-index: 0; + } + & .branchnode { + opacity: 0; + &:hover { + fill-opacity: 0.1; + opacity: 1.0; + fill: var(--d3fc-treedata--hover-highlight, black); + } + } + & .leafnode { + fill-opacity: 0.8; + &:hover { + fill-opacity: 0.5; + } + } + & #hidden { + opacity: 0; + pointer-events: none; + z-index: 0; + } + + & .top { + font-size: 15px; + font-weight:bold; + z-index: 5; + pointer-events: none; + } + & .mid { + font-size: 10px; + font-weight:bold; + opacity: 0.7; + z-index: 4; + } + & .lower { + font-size: 10px; + font-weight:bold; + opacity: 0; + z-index: 4; + } + & text { + fill: var(--d3fc-treedata--labels, rgb(51, 51, 51)); + } + + & .parent-controls { + position: absolute; + top: 15px; + right: auto; + left: 30px; + width: auto; + z-index: 4; + transition: box-shadow 1s; + &:hover { + box-shadow: 2px 2px 6px #000; + transition: box-shadow 0.2s; + } + } + } + } + + & .x-label { height: 1.2em !important; } @@ -238,6 +336,10 @@ height: 200px; } + .d3_treemap .legend-container.legend-color { + height: 200px; + } + .zoom-controls { position: absolute; top: 10px; @@ -259,6 +361,27 @@ } } + .parent-controls { + position: absolute; + top: 30px; + right: 145px; + width: 100%; + text-align: right; + + & button { + -webkit-appearance: none; + background: rgb(247, 247, 247); + border: 1px solid rgb(204, 204, 204); + padding: 10px; + opacity: 0.5; + cursor: pointer; + + &:hover { + background: rgb(230, 230, 230); + } + } + } + div.tooltip { position: absolute; text-align: left; @@ -277,4 +400,4 @@ padding: 0; list-style-type: none; } -} +} \ No newline at end of file diff --git a/packages/perspective-viewer/src/themes/material.dark.less b/packages/perspective-viewer/src/themes/material.dark.less index 8780bc8479..b4d9ae2d58 100644 --- a/packages/perspective-viewer/src/themes/material.dark.less +++ b/packages/perspective-viewer/src/themes/material.dark.less @@ -56,7 +56,8 @@ perspective-viewer { --highcharts-tooltip--background: @coolgrey800; --d3fc-legend--text: rgb(187, 187, 187); - --d3fc-sunburst--labels: rgb(187, 187, 187); + --d3fc-treedata--labels: rgb(187, 187, 187); + --d3fc-treedata--hover-highlight: rgb(255, 255, 255); --d3fc-axis--ticks: rgb(187, 187, 187); --d3fc-axis--lines: rgb(85, 85, 85); --d3fc-tooltip--background: #333333;