function TreeMultiValue(
data,
{
path,
id = Array.isArray(data) ? (d) => d.id : null,
parentId = Array.isArray(data) ? (d) => d.parentId : null,
children,
format = ",",
categories = ["value"],
values = categories.map((c) => (d) => d[c]),
sort = (a, b) => d3.descending(a.value, b.value),
label,
title,
link,
linkTarget = "_blank",
width = 640,
height = 400,
marginTop = 0,
marginRight = 100,
marginBottom = 0,
marginLeft = 0,
padding = 1,
round = false,
color = d3.schemeCategory10,
fill = "#ccc",
fillOpacity = 0.8,
stroke = "#555",
strokeWidth = 1.5,
strokeOpacity = 0.4,
strokeLinejoin, // stroke line join for links
strokeLinecap, // stroke line cap for links
halo = "#fff", // color of label halo
haloWidth = 3, // padding around the labels
parentR = 3, // Radius for inner nodes
drawLayout = false,
minHeightForLabel = 1,
fontSizeRange = [1, 18],
fontSizeDomain = [1, 40], //
minHeightForGlyph = 1,
colorScaleTitle = "Color Scale",
drawBarAxis = true,
drawBarValues = true,
normalizeBars = true, // If true the height will be adjusted per cell
getChartGlyph = getBarChartGlyph
} = {}
) {
// If id and parentId options are specified, or the path option, use d3.stratify
// to convert tabular data to a hierarchy; otherwise we assume that the data is
// specified as an object {children} with nested objects (a.k.a. the “flare.json”
// format), and use d3.hierarchy.
let root =
path != null
? d3.stratify().path(path)(data)
: id != null || parentId != null
? d3.stratify().id(id).parentId(parentId)(data)
: d3.hierarchy(data, children).sort(sort);
// value == null ? root.count() : root.sum(d => Math.max(0, value(d)));
// Compute the values of internal nodes by aggregating from the leaves
// the space necessary for the bars, i.e. possitive + negative bar sizes
values === null
? root.count()
: root.sum((d) => {
const [min, max] = d3.extent(values.map((v) => v(d)));
const maxNegBar = min < 0 ? 0 - min : 0;
const maxPosBar = max > 0 ? max - 0 : 0;
return maxNegBar + maxPosBar;
});
// Compute formats.
if (typeof format !== "function") format = d3.format(format);
// Sort the leaves (typically by descending value for a pleasing layout).
if (sort != null) root.sort(sort);
// Compute the partition layout. Note that x and y are swapped!
d3
.partition()
.size([height - marginTop - marginBottom, width - marginLeft - marginRight])
.padding(padding)
.round(round)(root);
// Construct a color scale.
if (color != null) {
if (typeof color == "function") {
color = d3.quantize(color, categories.length);
}
color = d3.scaleOrdinal(color).domain(categories);
}
const descendants = root.descendants();
const leaves = root.leaves();
const maxLeafHeight = d3.max(leaves, (d) => d.x1 - d.x0);
const maxLeafWidth = d3.max(leaves, (d) => (d.y1 - d.y0) / 2);
leaves.forEach((d, i) => {
d._valueExtent = d3.extent(values.map((v) => v(d.data)));
d.i = i;
});
const valueExtent = d3.extent(
// values.map((v) => d3.extent(leaves, (d) => v(d.data))).flat()
leaves.map((d) => d._valueExtent).flat()
);
const svg = d3
.create("svg")
.attr("viewBox", [-marginLeft, -marginTop, width, height])
.attr("width", width)
.attr("height", height)
.attr("overflow", "visible")
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.attr("font-family", "sans-serif")
.attr("font-size", 10);
// Draw the Icicle
if (drawLayout) {
svg
.append("g")
.selectAll("rect")
.data(descendants)
.join("rect")
.attr("y", (d) => d.x0)
.attr("x", (d) => d.y0)
.attr("height", (d) => d.x1 - d.x0)
.attr("width", (d) => d.y1 - d.y0)
.attr("fill", "none")
.attr("stroke", stroke)
.attr("stroke-opacity", strokeOpacity)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-width", strokeWidth);
}
console.log("links", root.links());
// Links
svg
.append("g")
.attr("fill", "none")
.attr("stroke", stroke)
.attr("stroke-opacity", strokeOpacity)
.attr("stroke-linecap", strokeLinecap)
.attr("stroke-linejoin", strokeLinejoin)
.attr("stroke-width", strokeWidth)
.selectAll("path")
.data(root.links())
.join("path")
.attr(
"d",
d3
.linkHorizontal()
.x((d) => d.y0 + (d.y1 - d.y0) / 2)
.y((d) => d.x0 + (d.x1 - d.x0) / 2)
);
const cell = svg
.append("g")
.attr("class", "cells")
.selectAll("a")
.data(descendants)
.join("a")
.attr("class", "cell")
.attr("xlink:href", link == null ? null : (d) => link(d.data, d))
.attr("target", link == null ? null : linkTarget)
.attr(
"transform",
(d) => `translate(${d.y0 + +(d.y1 - d.y0) / 2},${d.x0})`
);
let xScale = d3
.scaleBand()
.padding(0.2)
.domain(categories)
.range([0.1, maxLeafWidth - 1]);
let yScale = d3
.scaleLinear()
.domain([Math.min(valueExtent[0], 0), Math.max(valueExtent[1], 0)])
.range([maxLeafHeight - 1, 1]);
let fontScale = d3.scaleLinear().domain(fontSizeDomain).range(fontSizeRange);
cell.each(function (d) {
if (d.children || d.x1 - d.x0 < minHeightForGlyph) return;
// console.log("cell.each", d, xScale, yScale);
const localData = values.map((v, i) => ({
i,
value: v(d.data),
d: d,
category: categories[i]
}));
getChartGlyph(d3.select(this), localData, {
x: xScale,
y: yScale,
font: fontScale,
color: (d) => color(d.category),
xAttr: (d) => d.category,
yAttr: (d) => d.value,
valueExtent: normalizeBars ? d._valueExtent : null,
halo,
haloWidth,
d,
format,
drawBarAxis,
drawBarValues,
fontSizeRange,
fillOpacity
});
});
const text = cell
.filter((d) => d.x1 - d.x0 > minHeightForLabel)
.append("text")
.attr("text-anchor", "end")
// .attr("text-anchor", (d) => (d.children ? "end" : "start"))
.attr("x", (d) => (!d.children ? -10 : -parentR - 2))
.attr("y", (d) => (d.x1 - d.x0) / 2)
.attr("paint-order", "stroke")
.attr("stroke", halo)
.attr("stroke-width", haloWidth)
.attr("font-size", (d) =>
Math.min(
fontSizeRange[1],
Math.min(fontSizeRange[1], fontScale(d.x1 - d.x0))
)
)
.attr("dy", "0.32em");
if (label != null) text.append("tspan").text((d) => label(d.data, d));
// text
// .append("tspan")
// .attr("fill-opacity", 0.7)
// .attr("dx", label == null ? null : 3)
// .text((d) => format(d.value));
if (title != null) cell.append("title").text((d) => title(d.data, d));
colorLegend(
svg
.append("g")
.attr("class", "color-legend")
.attr("fill-opacity", fillOpacity)
.attr(
"transform",
`translate(${width - marginLeft - marginRight + 20}, ${marginTop + 20})`
),
color,
{ title: colorScaleTitle }
);
svg.node();
const target = htl.html`<div>
<style>
.cell .border {
display:none;
}
.cell:hover .border {
display:block;
}
</style>
${svg.node()}</div>`;
target.root = root;
return target;
}