superset/superset-frontend/plugins/legacy-plugin-chart-treemap/src/Treemap.js

191 lines
5.1 KiB
JavaScript
Raw Normal View History

2019-01-30 18:16:17 -05:00
/**
* 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 */
2019-01-30 21:16:13 -05:00
import { select as d3Select, selectAll as d3SelectAll } from 'd3-selection';
import {
treemap as d3Treemap,
hierarchy as d3Hierarchy,
treemapSquarify,
} from 'd3-hierarchy';
2019-01-30 18:16:17 -05:00
import PropTypes from 'prop-types';
import {
getNumberFormatter,
CategoricalColorNamespace,
} from '@superset-ui/core';
2019-01-30 18:16:17 -05:00
import './Treemap.css';
// Declare PropTypes for recursive data structures
// https://github.com/facebook/react/issues/5676
2019-02-07 20:49:25 -05:00
/* eslint-disable-next-line no-undef */
2019-01-30 18:16:17 -05:00
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,
]),
2019-01-30 18:16:17 -05:00
),
};
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,
};
2019-01-30 21:16:13 -05:00
function hovered(hover) {
return function (node) {
2019-01-30 21:16:13 -05:00
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);
};
2019-01-30 18:16:17 -05:00
}
2019-01-30 21:16:13 -05:00
/* Modified from https://bl.ocks.org/mbostock/911ad09bdead40ec0061 */
2019-01-30 18:16:17 -05:00
function Treemap(element, props) {
const {
data: rawData,
width,
height,
numberFormat,
colorScheme,
treemapRatio,
} = props;
2019-01-30 21:16:13 -05:00
const div = d3Select(element);
2019-01-30 18:16:17 -05:00
div.classed('superset-legacy-chart-treemap', true);
const formatNumber = getNumberFormatter(numberFormat);
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
2019-01-30 21:16:13 -05:00
const rootNodes = rawData;
2019-01-30 18:16:17 -05:00
2019-01-30 21:16:13 -05:00
div.selectAll('*').remove();
2019-01-30 18:16:17 -05:00
2019-01-30 21:16:13 -05:00
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),
);
2019-01-30 18:16:17 -05:00
const svg = div
.append('svg')
.attr('class', 'treemap')
2019-01-30 21:16:13 -05:00
.attr('width', width)
.attr('height', height);
2019-01-30 18:16:17 -05:00
2019-01-30 21:16:13 -05:00
const cell = svg
.selectAll('.node')
.data(root.descendants())
.enter()
2019-01-30 18:16:17 -05:00
.append('g')
2019-01-30 21:16:13 -05:00
.attr('transform', d => `translate(${d.x0},${d.y0})`)
.attr('class', 'node')
.each(function (d) {
2019-01-30 21:16:13 -05:00
d.node = this;
})
.on('mouseover', hovered(true))
.on('mouseout', hovered(false));
cell
2019-01-30 18:16:17 -05:00
.append('rect')
2019-01-30 21:16:13 -05:00
.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})`);
2019-01-30 21:16:13 -05:00
label
.filter(d => d.children)
.selectAll('tspan')
.data(d =>
d.data.name
.slice(Math.max(0, d.data.name.lastIndexOf('.') + 1))
2019-01-30 21:16:13 -05:00
.split(/(?=[A-Z][^A-Z])/g)
.concat(`\u00A0${formatNumber(d.value)}`),
2019-01-30 21:16:13 -05:00
)
.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))
2019-01-30 21:16:13 -05:00
.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)}`);
2019-01-30 18:16:17 -05:00
}
}
Treemap.displayName = 'Treemap';
Treemap.propTypes = propTypes;
export default Treemap;