/* eslint-disable react/sort-prop-types */ /** * 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 no-param-reassign: [2, {"props": false}] */ /* eslint-disable no-plusplus */ import d3 from 'd3'; import PropTypes from 'prop-types'; import { hierarchy } from 'd3-hierarchy'; import { CategoricalColorNamespace } from '@superset-ui/color'; import { getNumberFormatter } from '@superset-ui/number-format'; import { getTimeFormatter } from '@superset-ui/time-format'; import './Partition.css'; // Compute dx, dy, x, y for each node and // return an array of nodes in breadth-first order function init(root) { const flat = []; const dy = 1 / (root.height + 1); let prev = null; root.each(n => { n.y = dy * n.depth; n.dy = dy; if (n.parent) { n.x = prev.depth === n.parent.depth ? 0 : prev.x + prev.dx; n.dx = n.weight / n.parent.sum * n.parent.dx; } else { n.x = 0; n.dx = 1; } prev = n; flat.push(n); }); return flat; } // 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, val: PropTypes.number.isRequired }); const parentShape = { name: PropTypes.string, val: PropTypes.number.isRequired, children: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.shape(lazyFunction(() => parentShape)), leafType])) }; const nodeType = PropTypes.oneOfType([PropTypes.shape(parentShape), leafType]); const propTypes = { data: PropTypes.arrayOf(nodeType), // array of rootNode width: PropTypes.number, height: PropTypes.number, colorScheme: PropTypes.string, dateTimeFormat: PropTypes.string, equalDateSize: PropTypes.bool, levels: PropTypes.arrayOf(PropTypes.string), metrics: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object])), numberFormat: PropTypes.string, partitionLimit: PropTypes.number, partitionThreshold: PropTypes.number, timeSeriesOption: PropTypes.string, useLogScale: PropTypes.bool, useRichTooltip: PropTypes.bool }; // This vis is based on // http://mbostock.github.io/d3/talk/20111018/partition.html function Icicle(element, props) { const { width, height, data, colorScheme, dateTimeFormat, equalDateSize, levels, useLogScale = false, metrics = [], numberFormat, partitionLimit, partitionThreshold, useRichTooltip, timeSeriesOption = 'not_time' } = props; const div = d3.select(element); div.classed('superset-legacy-chart-partition', true); // Chart options const chartType = timeSeriesOption; const hasTime = ['adv_anal', 'time_series'].includes(chartType); const format = getNumberFormatter(numberFormat); const timeFormat = getTimeFormatter(dateTimeFormat); const colorFn = CategoricalColorNamespace.getScale(colorScheme); div.selectAll('*').remove(); const tooltip = div.append('div').classed('partition-tooltip', true); function drawVis(i, dat) { const datum = dat[i]; const w = width; const h = height / data.length; const x = d3.scale.linear().range([0, w]); const y = d3.scale.linear().range([0, h]); const viz = div.append('div').attr('class', 'chart').style('width', w + "px").style('height', h + "px").append('svg:svg').attr('width', w).attr('height', h); // Add padding between multiple visualizations if (i !== data.length - 1 && data.length > 1) { viz.style('padding-bottom', '3px'); } if (i !== 0 && data.length > 1) { viz.style('padding-top', '3px'); } const root = hierarchy(datum); function hasDateNode(n) { return metrics.includes(n.data.name) && hasTime; } // node.name is the metric/group name // node.disp is the display value // node.value determines sorting order // node.weight determines partition height // node.sum is the sum of children weights root.eachAfter(n => { n.disp = n.data.val; n.value = n.disp < 0 ? -n.disp : n.disp; n.weight = n.value; n.name = n.data.name; // If the parent is a metric and we still have // the time column, perform a date-time format if (n.parent && hasDateNode(n.parent)) { // Format timestamp values n.weight = equalDateSize ? 1 : n.value; n.value = n.name; n.name = timeFormat(n.name); } if (useLogScale) n.weight = Math.log(n.weight + 1); n.disp = n.disp && !Number.isNaN(n.disp) && Number.isFinite(n.disp) ? format(n.disp) : ''; }); // Perform sort by weight root.sort((a, b) => { const v = b.value - a.value; if (v === 0) { return b.name > a.name ? 1 : -1; } return v; }); // Prune data based on partition limit and threshold // both are applied at the same time if (partitionThreshold && partitionThreshold >= 0) { // Compute weight sums as we go root.each(n => { n.sum = n.children ? n.children.reduce((a, v) => a + v.weight, 0) || 1 : 1; if (n.children) { // Dates are not ordered by weight if (hasDateNode(n)) { if (equalDateSize) { return; } const removeIndices = []; // Keep at least one child for (let j = 1; j < n.children.length; j++) { if (n.children[j].weight / n.sum < partitionThreshold) { removeIndices.push(j); } } for (let j = removeIndices.length - 1; j >= 0; j--) { n.children.splice(removeIndices[j], 1); } } else { // Find first child that falls below the threshold let j; for (j = 1; j < n.children.length; j++) { if (n.children[j].weight / n.sum < partitionThreshold) { break; } } n.children = n.children.slice(0, j); } } }); } if (partitionLimit && partitionLimit >= 0) { root.each(n => { if (n.children && n.children.length > partitionLimit) { if (!hasDateNode(n)) { n.children = n.children.slice(0, partitionLimit); } } }); } // Compute final weight sums root.eachAfter(n => { n.sum = n.children ? n.children.reduce((a, v) => a + v.weight, 0) || 1 : 1; }); function getCategory(depth) { if (!depth) { return 'Metric'; } if (hasTime && depth === 1) { return 'Date'; } return levels[depth - (hasTime ? 2 : 1)]; } function getAncestors(d) { const ancestors = [d]; let node = d; while (node.parent) { ancestors.push(node.parent); node = node.parent; } return ancestors; } function positionAndPopulate(tip, d) { let t = '
' + '' + ' | ' + ("" + getCategory(n.depth) + " | ") + ("" + n.name + " | ") + ("" + n.disp + " | ") + '
' + ("" + getCategory(d.depth) + "") + ' | |||
' + ("' + ' | ' + ("" + d.name + " | ") + ("" + d.disp + " | ") + '