blob: 6d12838e769e28d79f3a883fd69cce42a1ca1b85 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-disable no-param-reassign, func-names */
/* eslint-disable react/sort-prop-types */
import { select as d3Select, selectAll as d3SelectAll } from 'd3-selection';
import { treemap as d3Treemap, hierarchy as d3Hierarchy, treemapSquarify } from 'd3-hierarchy';
import PropTypes from 'prop-types';
import { getNumberFormatter, CategoricalColorNamespace } from '@superset-ui/core';
import './Treemap.css';
// Declare PropTypes for recursive data structures
// https://github.com/facebook/react/issues/5676
/* eslint-disable-next-line no-undef */
const lazyFunction = f => () => f().apply(this, arguments);
const leafType = PropTypes.shape({
name: PropTypes.string,
value: PropTypes.number.isRequired,
});
const parentShape = {
name: PropTypes.string,
children: PropTypes.arrayOf(
PropTypes.oneOfType([PropTypes.shape(lazyFunction(() => parentShape)), leafType]),
),
};
const nodeType = PropTypes.oneOfType([PropTypes.shape(parentShape), leafType]);
const propTypes = {
data: PropTypes.arrayOf(nodeType),
width: PropTypes.number,
height: PropTypes.number,
colorScheme: PropTypes.string,
margin: PropTypes.shape({
top: PropTypes.number,
right: PropTypes.number,
bottom: PropTypes.number,
left: PropTypes.number,
}),
numberFormat: PropTypes.string,
treemapRatio: PropTypes.number,
};
function hovered(hover) {
return function (node) {
d3SelectAll(node.ancestors().map(d => d.node))
.classed('node--hover', hover)
.select('rect')
.attr('width', d => d.x1 - d.x0 - hover)
.attr('height', d => d.y1 - d.y0 - hover);
};
}
/* Modified from https://bl.ocks.org/mbostock/911ad09bdead40ec0061 */
function Treemap(element, props) {
const { data: rawData, width, height, numberFormat, colorScheme, treemapRatio } = props;
const div = d3Select(element);
div.classed('superset-legacy-chart-treemap', true);
const formatNumber = getNumberFormatter(numberFormat);
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
const rootNodes = rawData;
div.selectAll('*').remove();
if (rootNodes.length > 0) {
const [rootNode] = rootNodes;
const treemap = d3Treemap()
.size([width, height])
.paddingOuter(3)
.paddingTop(19)
.paddingInner(1)
.tile(treemapSquarify.ratio(treemapRatio))
.round(true);
const root = treemap(
d3Hierarchy(rootNode)
.sum(d => d.value)
.sort((a, b) => b.height - a.height || b.value - a.value),
);
const svg = div
.append('svg')
.attr('class', 'treemap')
.attr('width', width)
.attr('height', height);
const cell = svg
.selectAll('.node')
.data(root.descendants())
.enter()
.append('g')
.attr('transform', d => `translate(${d.x0},${d.y0})`)
.attr('class', 'node')
.each(function (d) {
d.node = this;
})
.on('mouseover', hovered(true))
.on('mouseout', hovered(false));
cell
.append('rect')
.attr('id', d => `rect-${d.data.name}`)
.attr('width', d => d.x1 - d.x0)
.attr('height', d => d.y1 - d.y0)
.style('fill', d => colorFn(d.depth));
cell
.append('clipPath')
.attr('id', d => `clip-${d.data.name}`)
.append('use')
.attr('xlink:href', d => `#rect-${d.data.name}`);
const label = cell.append('text').attr('clip-path', d => `url(#clip-${d.data.name})`);
label
.filter(d => d.children)
.selectAll('tspan')
.data(d =>
d.data.name
.slice(Math.max(0, d.data.name.lastIndexOf('.') + 1))
.split(/(?=[A-Z][^A-Z])/g)
.concat(`\u00A0${formatNumber(d.value)}`),
)
.enter()
.append('tspan')
.attr('x', (d, i) => (i ? null : 4))
.attr('y', 13)
.text(d => d);
label
.filter(d => !d.children)
.selectAll('tspan')
.data(d =>
d.data.name
.slice(Math.max(0, d.data.name.lastIndexOf('.') + 1))
.split(/(?=[A-Z][^A-Z])/g)
.concat(formatNumber(d.value)),
)
.enter()
.append('tspan')
.attr('x', 4)
.attr('y', (d, i) => 13 + i * 10)
.text(d => d);
cell.append('title').text(d => `${d.data.name}\n${formatNumber(d.value)}`);
}
}
Treemap.displayName = 'Treemap';
Treemap.propTypes = propTypes;
export default Treemap;