gobierto-vizzs
Version:
Shared data visualizations for Gobierto projects
1,138 lines (1,077 loc) • 57.4 kB
JavaScript
import "./module.css";
import {select as $5OpyM$select, selectAll as $5OpyM$selectAll, pointer as $5OpyM$pointer} from "d3-selection";
import {scaleOrdinal as $5OpyM$scaleOrdinal, scaleBand as $5OpyM$scaleBand, scaleTime as $5OpyM$scaleTime, scalePow as $5OpyM$scalePow, scaleLinear as $5OpyM$scaleLinear} from "d3-scale";
import {forceSimulation as $5OpyM$forceSimulation, forceX as $5OpyM$forceX, forceY as $5OpyM$forceY, forceCollide as $5OpyM$forceCollide} from "d3-force";
import {axisBottom as $5OpyM$axisBottom, axisLeft as $5OpyM$axisLeft, axisTop as $5OpyM$axisTop} from "d3-axis";
import {extent as $5OpyM$extent, max as $5OpyM$max, sum as $5OpyM$sum, union as $5OpyM$union, group as $5OpyM$group, difference as $5OpyM$difference, groupSort as $5OpyM$groupSort, rollup as $5OpyM$rollup, min as $5OpyM$min} from "d3-array";
import {timeFormat as $5OpyM$timeFormat, timeFormatDefaultLocale as $5OpyM$timeFormatDefaultLocale} from "d3-time-format";
import {easeLinear as $5OpyM$easeLinear} from "d3-ease";
import {timeMonth as $5OpyM$timeMonth, timeYear as $5OpyM$timeYear} from "d3-time";
import "d3-transition";
import {stack as $5OpyM$stack, stackOrderAscending as $5OpyM$stackOrderAscending, stackOrderReverse as $5OpyM$stackOrderReverse, stackOffsetExpand as $5OpyM$stackOffsetExpand, stackOffsetNone as $5OpyM$stackOffsetNone} from "d3-shape";
import {hierarchy as $5OpyM$hierarchy, treemap as $5OpyM$treemap, treemapBinary as $5OpyM$treemapBinary} from "d3-hierarchy";
import {interpolate as $5OpyM$interpolate} from "d3-interpolate";
import {dsvFormat as $5OpyM$dsvFormat} from "d3-dsv";
var $df64573ef6d51081$exports = {};
$df64573ef6d51081$exports = JSON.parse("{\"name\":\"gobierto-vizzs\",\"version\":\"3.2.0\",\"description\":\"Shared data visualizations for Gobierto projects\",\"keywords\":[\"gobierto\",\"visualizations\",\"beeswarm\",\"treemap\",\"gantt\",\"bar\",\"stack\"],\"source\":\"src/index.js\",\"main\":\"dist/index.js\",\"module\":\"dist/module.js\",\"style\":\"dist/index.css\",\"files\":[\"dist\",\"src\"],\"sideEffects\":[\"*.css\"],\"scripts\":{\"watch\":\"parcel watch\",\"prebuild\":\"rm -rf ./dist/* ./.parcel-cache\",\"build\":\"parcel build\",\"postversion\":\"npm run build && git push --follow-tags\"},\"author\":\"populate\",\"license\":\"ISC\",\"dependencies\":{\"d3-array\":\"^3.2.4\",\"d3-axis\":\"^3.0.0\",\"d3-dsv\":\"^3.0.1\",\"d3-ease\":\"^3.0.1\",\"d3-force\":\"^3.0.0\",\"d3-hierarchy\":\"^3.1.2\",\"d3-interpolate\":\"^3.0.1\",\"d3-scale\":\"^4.0.2\",\"d3-selection\":\"^3.0.0\",\"d3-shape\":\"^3.2.0\",\"d3-time\":\"^3.1.0\",\"d3-time-format\":\"^4.1.0\",\"d3-transition\":\"^3.0.1\"},\"devDependencies\":{\"buffer\":\"^6.0.3\",\"parcel\":\"^2.11.0\"}}");
const $873f18828fe6cec2$export$ca5e4045a55e76d2 = {
"dateTime": "%A, %e de %B de %Y, %X",
"date": "%d/%m/%Y",
"time": "%H:%M:%S",
"periods": [
"AM",
"PM"
],
"days": [
"domingo",
"lunes",
"martes",
"mi\xe9rcoles",
"jueves",
"viernes",
"s\xe1bado"
],
"shortDays": [
"dom",
"lun",
"mar",
"mi\xe9",
"jue",
"vie",
"s\xe1b"
],
"months": [
"enero",
"febrero",
"marzo",
"abril",
"mayo",
"junio",
"julio",
"agosto",
"septiembre",
"octubre",
"noviembre",
"diciembre"
],
"shortMonths": [
"ene",
"feb",
"mar",
"abr",
"may",
"jun",
"jul",
"ago",
"sep",
"oct",
"nov",
"dic"
]
};
const $873f18828fe6cec2$export$89e9012e902952b6 = {
"dateTime": "%A, %e de %B de %Y, %X",
"date": "%d/%m/%Y",
"time": "%H:%M:%S",
"periods": [
"AM",
"PM"
],
"days": [
"diumenge",
"dilluns",
"dimarts",
"dimecres",
"dijous",
"divendres",
"dissabte"
],
"shortDays": [
"dg.",
"dl.",
"dt.",
"dc.",
"dj.",
"dv.",
"ds."
],
"months": [
"gener",
"febrer",
"mar\xe7",
"abril",
"maig",
"juny",
"juliol",
"agost",
"setembre",
"octubre",
"novembre",
"desembre"
],
"shortMonths": [
"gen.",
"febr.",
"mar\xe7",
"abr.",
"maig",
"juny",
"jul.",
"ag.",
"set.",
"oct.",
"nov.",
"des."
]
};
const $f9a0fbfe241eb50c$var$DEFAULT_LOCALES = {
"es-ES": (0, $873f18828fe6cec2$export$ca5e4045a55e76d2),
"ca-ES": (0, $873f18828fe6cec2$export$89e9012e902952b6)
};
class $f9a0fbfe241eb50c$export$2e2bcd8739ae039 {
constructor(container, _, options){
this.container = container;
this.version = (0, $df64573ef6d51081$exports.version);
this.locale = options.locale || window.navigator.language;
this.PALETTE = Array.from({
length: 12
}, (_, i)=>`var(--gv-color-${i + 1})`);
window.addEventListener("resize", this.resizeListener.bind(this));
}
async getLocale() {
// unpkg does not keep non-regional locales (2-letters code), so it's worthless make the request
if (this.locale.length > 2) {
// request the locale when it does not exists by default
const i18n = $f9a0fbfe241eb50c$var$DEFAULT_LOCALES[this.locale] || await fetch(`https://unpkg.com/d3-time-format/locale/${this.locale}.json`).then((r)=>r.json());
if (i18n) (0, $5OpyM$timeFormatDefaultLocale)(i18n);
}
}
async setLocale(value) {
this.locale = value;
await this.getLocale();
this.build();
}
// defined in the inherited classes
getDimensions() {}
// defined in the inherited classes
build() {}
resizeListener() {
this.getDimensions();
this.build();
}
remove() {
window.removeEventListener("resize", this.resizeListener.bind(this));
}
isSmallDevice() {
return screen.width < 768;
}
wrap(text, width, marginLeft = 0) {
text.each(function() {
var text = (0, $5OpyM$select)(this), words = text.text().split(/\s+/).reverse(), word, line = [], lineHeight = 1, y = text.attr("y"), dy = 0, tspan = text.text(null).append("tspan").attr("x", marginLeft);
while(word = words.pop()){
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
text.attr("class", "wrap-text");
line.pop();
tspan.text(line.join(" "));
line = [
word
];
tspan = text.append("tspan").attr("x", marginLeft).attr("dy", lineHeight + dy + "em").text(word);
}
}
});
}
tooltipPosition(event, element, offset = 0) {
const [x, y] = (0, $5OpyM$pointer)(event, this.container);
const { width: pW, height: pH } = this.container.getBoundingClientRect();
const { width: width, height: height } = element.getBoundingClientRect();
const isLeft = x < pW * 0.5;
const isTop = y < pH * 0.5;
return isLeft && isTop ? [
x + offset,
y + offset
] : isLeft && !isTop ? [
x + offset,
y - height - offset
] : !isLeft && isTop ? [
x - width - offset,
y + offset
] : [
x - width - offset,
y - height - offset
];
}
seed(len = 24) {
return [
...Array(len)
].map(()=>Math.random().toString(36)[2]).join('');
}
debounce(func, timeout) {
let timer = undefined;
return (...args)=>{
const next = ()=>func(...args);
if (timer) clearTimeout(timer);
timer = setTimeout(next, timeout > 0 ? timeout : 300);
};
}
groupBy(arr, key) {
return arr.reduce((acc, item)=>(acc[item[key]] = [
...acc[item[key]] || [],
item
], acc), {});
}
sortBy(prop) {
return (a, b)=>a[prop] > b[prop] ? 1 : -1;
}
isDate(...value) {
return !Number.isNaN(+new Date(...value));
}
}
class $6fd9c7838fdaf9d6$export$2e2bcd8739ae039 extends (0, $f9a0fbfe241eb50c$export$2e2bcd8739ae039) {
constructor(container, data, options = {}){
super(container, data, options);
this.tooltip = options.tooltip || this.defaultTooltip;
this.margin = {
top: 50,
bottom: 50,
left: 120,
right: 30,
...options.margin
};
this.onClick = options.onClick || (()=>{});
// main properties to display
this.xAxisProp = options.x || "date";
this.yAxisProp = options.y || "group";
this.valueProp = options.value || "value";
this.idProp = options.id || "id";
this.relationProp = options.relation;
// band item height
this.MIN_BLOCK_SIZE = options.minBlockSize || 100;
this.CIRCLE_SIZE = options.circleSize || [
2,
20
];
// chart size
this.getDimensions();
// static elements (do not redraw)
this.setupElements();
if (data.length) this.setData(data);
}
getDimensions() {
const { width: width } = this.container.getBoundingClientRect();
this.width = width - this.margin.left - this.margin.right;
}
setupElements() {
this.svg = (0, $5OpyM$select)(this.container).classed("gv-container", true).append("svg").attr("class", "gv-plot");
this.g = this.svg.append("g").attr("transform", `translate(${this.margin.left} ${this.margin.top})`);
this.g.append("g").attr("class", "axis axis-x");
this.g.append("g").attr("class", "axis axis-y");
this.tooltipContainer = (0, $5OpyM$select)(this.container).append("div").attr("class", "gv-tooltip");
}
build() {
this.setScales();
this.g.select(".axis-x").attr("transform", `translate(0 ${this.height})`).call(this.xAxis.bind(this));
this.g.select(".axis-y").attr("transform", `translate(${-this.margin.left} ${-this.scaleY.bandwidth() / 2})`).call(this.yAxis.bind(this));
(0, $5OpyM$forceSimulation)(this.data).force("x", (0, $5OpyM$forceX)((d)=>this.scaleX(d[this.xAxisProp]))).force("y", (0, $5OpyM$forceY)((d)=>this.scaleY(d[this.yAxisProp]))).force("collide", (0, $5OpyM$forceCollide)().radius((d)=>this.scaleRadius(d[this.valueProp]) + 1)).on("tick", ()=>this.g.selectAll("circle.beeswarm-circle").attr("cx", (d)=>d.x).attr("cy", (d)=>d.y));
this.g.selectAll("circle.beeswarm-circle").data(this.data, (d)=>d[this.idProp]).join((enter)=>enter.append("circle").attr("class", (d)=>this.relationProp ? `beeswarm-circle beeswarm-circle-${d[this.relationProp]}` : "beeswarm-circle").attr("r", (d)=>this.scaleRadius(d[this.valueProp])).attr("fill", (d)=>this.scaleColor(d[this.yAxisProp]))).on("touchmove", (e)=>e.preventDefault()).on("pointermove", this.onPointerMove.bind(this)).on("pointerout", this.onPointerOut.bind(this)).attr("cursor", "pointer").on("click", (...e)=>this.onClick(...e));
}
xAxis(g) {
const months = (0, $5OpyM$timeMonth).count(...this.scaleX.domain());
const hasMultipleYears = months > 24;
const onlyOneYear = months < 12;
g.call((0, $5OpyM$axisBottom)(this.scaleX).tickFormat(hasMultipleYears ? (0, $5OpyM$timeFormat)("%Y") : onlyOneYear ? (0, $5OpyM$timeFormat)("%b") : (0, $5OpyM$timeFormat)("%b-%Y")).tickSize(-this.height).ticks(hasMultipleYears ? 5 : (0, $5OpyM$timeMonth).every(3)));
// remove baseline
g.select(".domain").remove();
// remove default formats
g.attr("font-family", null).attr("font-size", null);
// change line style defaults
g.selectAll("line").remove();
}
yAxis(g) {
g.call((0, $5OpyM$axisLeft)(this.scaleY).tickSize(-this.width));
// remove baseline
g.select(".domain").remove();
// remove default formats
g.attr("font-family", null).attr("font-size", null).attr("text-anchor", "start");
// change line style defaults
g.selectAll("line").attr("transform", `translate(${this.margin.left} 0)`).attr("stroke-dasharray", 1).attr("stroke", "var(--gv-grey)");
// change line style defaults
g.selectAll("text").call(this.wrap, this.margin.left).attr("dy", (_, i, nodes)=>{
const currentNode = nodes[i];
// substract the amount of lines (the children) minus 1 to the default dy (0.32em)
// then, divide by 2 to center vertically
const dy = (0.32 - (currentNode.children.length - 1)) / 2;
return `${dy}em`;
});
}
async setData(data) {
this.rawData = data;
this.data = this.parse(data);
// only set the color scale, as of the first time you get the data
if (!this.scaleColor) this.setColorScale();
// wait for the locales resolution before draw anything
await this.getLocale();
this.build();
}
setColorScale() {
this.scaleColor = (0, $5OpyM$scaleOrdinal)().domain(Array.from(new Set(this.data.map((d)=>d[this.yAxisProp])))).range(this.PALETTE);
}
setScales() {
// group by a prop, sort them by occurrences and get the keys
const groups = Object.entries(this.groupBy(this.data, this.yAxisProp)).sort(([, a], [, b])=>a.length > b.length).map(([key])=>key);
// the chart reflows based on the amount of groups (categories) it has
this.height = groups.length * this.MIN_BLOCK_SIZE - this.MIN_BLOCK_SIZE / 2;
this.svg.attr("viewBox", `0 0 ${this.width + this.margin.left + this.margin.right} ${this.height + this.margin.top + this.margin.bottom}`);
this.scaleY = (0, $5OpyM$scaleBand)().domain(groups).range([
this.height,
0
]);
this.scaleX = (0, $5OpyM$scaleTime)().domain((0, $5OpyM$extent)(this.data, (d)=>d[this.xAxisProp])).rangeRound([
20,
this.width - 20
]);
this.scaleRadius = (0, $5OpyM$scalePow)().exponent(0.5).range(this.CIRCLE_SIZE).domain([
0,
(0, $5OpyM$max)(this.data, (d)=>d[this.valueProp])
]);
}
onPointerMove(event, d) {
if (this.relationProp) (0, $5OpyM$selectAll)("circle.beeswarm-circle").transition().duration(400).style("opacity", 0.1).filter((e)=>e[this.relationProp] === d[this.relationProp]).transition().duration(400).ease((0, $5OpyM$easeLinear)).style("opacity", 1);
const tooltip = this.tooltipContainer.html(this.tooltip(d));
const [x, y] = this.tooltipPosition(event, this.tooltipContainer.node(), 10);
tooltip.style("top", `${y}px`).style("left", `${x}px`).style("pointer-events", "auto").transition().duration(400).style("opacity", 1);
}
onPointerOut() {
this.tooltipContainer.style("pointer-events", "none").transition().delay(1000).duration(400).style("opacity", 0);
(0, $5OpyM$selectAll)("circle.beeswarm-circle").transition().duration(400).style("opacity", 1);
}
parse(data) {
// 1. remove those elements with no X axis data
// 2. enforces the datatypes:
// - X axis is Date
// - Z axis is Number
return data.reduce((acc, d)=>{
return [
...acc,
// https://2ality.com/2017/04/conditional-literal-entries.html
...!!d[this.xAxisProp] ? [
{
...d,
[this.xAxisProp]: new Date(d[this.xAxisProp]),
[this.valueProp]: +d[this.valueProp]
}
] : []
];
}, []);
}
defaultTooltip(d) {
return `
<div class="beeswarm-tooltip-id">${d[this.idProp]}</div>
<div class="beeswarm-tooltip-values">
<span class="beeswarm-tooltip-date">${d[this.xAxisProp].toLocaleDateString()}</span>
<span class="beeswarm-tooltip-radius">${d[this.valueProp].toLocaleString()}</span>
</div>
`;
}
setX(value) {
this.xAxisProp = value;
this.setData(this.rawData);
}
setY(value) {
this.yAxisProp = value;
this.setData(this.rawData);
}
setValue(value) {
this.valueProp = value;
this.setData(this.rawData);
}
setId(value) {
this.idProp = value;
this.build();
}
setRelation(value) {
this.relationProp = value;
this.build();
}
setMinBlockSize(value) {
this.MIN_BLOCK_SIZE = value;
this.build();
}
setCircleSize(value) {
this.CIRCLE_SIZE = value;
this.build();
}
setTooltip(value) {
this.tooltip = value;
this.build();
}
setOnClick(value) {
this.onClick = value;
this.build();
}
setMargin(value) {
this.margin = {
...this.margin,
...value
};
this.container.replaceChildren();
this.getDimensions();
this.setupElements();
this.build();
}
}
class $e6ee73b8cf92221b$export$2e2bcd8739ae039 extends (0, $f9a0fbfe241eb50c$export$2e2bcd8739ae039) {
constructor(container, data, options = {}){
super(container, data, options);
this.tooltip = options.tooltip || this.defaultTooltip;
this.onClick = options.onClick || (()=>{});
// main properties to display
this.xAxisProp = options.x || "date";
this.yAxisProp = options.y || "group";
this.countProp = options.count;
this.showLegend = options.showLegend;
this.sortStack = options.sortStack;
this.ratio = options.ratio || "absolute";
this.xTickFormat = options.xTickFormat || ((d)=>d);
this.yTickFormat = options.yTickFormat || ((d)=>d.toLocaleString());
this.orientationLegend = options.orientationLegend || "left";
this.height = options.height || 400;
this.categories = options.categories;
this.series = options.series;
this.xTickValues = options.xTickValues;
this.yTickValues = options.yTickValues;
this.margin = {
top: 12,
bottom: 36,
left: this.orientationLegend === 'left' ? 240 : 84,
right: this.orientationLegend === 'left' ? 48 : 240,
...options.margin
};
// chart size
this.getDimensions();
// static elements (do not redraw)
this.setupElements();
if (data.length) this.setData(data);
}
getDimensions() {
const { width: width } = this.container.getBoundingClientRect();
this.width = width - this.margin.left - this.margin.right;
}
setupElements() {
this.svg = (0, $5OpyM$select)(this.container).classed("gv-container", true).append("svg").attr("class", "gv-plot");
this.g = this.svg.append("g").attr("transform", `translate(${this.margin.left} ${this.margin.top})`);
this.g.append("g").attr("class", "axis axis-x");
this.g.append("g").attr("class", "axis axis-y");
this.tooltipContainer = (0, $5OpyM$select)(this.container).append("div").attr("class", "gv-tooltip gv-tooltip-bar-stacked");
this.legendContainer = (0, $5OpyM$select)(this.container).append("div").attr("class", "gv-legend-bar-stacked").classed("gv-legend-bar-stacked-right", this.orientationLegend === "right");
this.g.append("text").attr("class", "axis-x-legend").attr("x", this.orientationLegend === "right" ? this.width + 10 : -60).attr("y", this.height + 9).attr("dy", "0.71em").attr("text-anchor", this.orientationLegend === "right" ? "start" : "end").text(this.xAxisProp);
}
build() {
this.setScales();
this.g.select(".axis-x").attr("transform", `translate(0 ${this.height})`).call(this.xAxis.bind(this));
this.g.select(".axis-y").call(this.yAxis.bind(this));
this.g.selectAll(".bar-stacked-group").data(this.stack).join("g").attr("class", "bar-stacked-group").attr("id", ({ key: key })=>key).attr("fill", ({ key: key })=>this.scaleColor(key)).selectAll("rect").data((d)=>d).join("rect").attr("class", "bar-stacked-rect").attr("x", (d)=>this.scaleX(d.data[0])).attr("width", this.scaleX.bandwidth()).transition().duration(400).attr("y", (d)=>this.scaleY(d[1])).attr("height", (d)=>this.scaleY(d[0]) - this.scaleY(d[1])).attr("cursor", "pointer").selection().on("touchmove", (e)=>e.preventDefault()).on("pointermove", this.onPointerMove.bind(this)).on("pointerout", this.onPointerOut.bind(this));
if (this.showLegend) this.buildLegends();
}
buildLegends() {
this.legendContainer.selectAll(".bar-stack-label").remove();
const items = this.sortStack ? this.stack.sort((a, b)=>(0, $5OpyM$sum)(b, (d)=>d[1] - d[0]) - (0, $5OpyM$sum)(a, (d)=>d[1] - d[0])) : this.stack;
this.legendContainer.selectAll(".bar-stack-label").data(items).join((enter)=>{
const g = enter.append("div").attr("class", "bar-stack-label");
g.append("span").attr("class", "bar-stack-label-rect").attr("style", ({ key: key })=>`background-color: ${this.scaleColor(key)}`);
g.append("span").attr("class", "bar-stacked-legend-text").attr("title", ({ key: key })=>key).text(({ key: key })=>key);
return g;
}, (update)=>update, (exit)=>exit.remove()).on("pointermove", function(_, d) {
const { key: key } = d;
const groups = (0, $5OpyM$selectAll)('.bar-stacked-group');
groups.filter(({ key: k })=>k !== key).style("opacity", .1);
groups.filter(({ key: k })=>k === key).style("opacity", 1);
}).on("pointerout", ()=>{
(0, $5OpyM$selectAll)('.bar-stacked-group').style("opacity", 1);
});
}
xAxis(g) {
g.call((0, $5OpyM$axisBottom)(this.scaleX).tickValues(this.xTickValues).tickFormat((d)=>this.xTickFormat(d)).tickPadding(6).tickSize(10));
// remove baseline
g.select(".domain").remove();
// remove default formats
g.attr("font-family", null).attr("font-size", null);
}
yAxis(g) {
g.call((0, $5OpyM$axisLeft)(this.scaleY).tickValues(this.yTickValues || this.scaleY.ticks().filter((x)=>!(this.ratio !== "percentage" && !Number.isInteger(x)))).tickSize(-this.width).tickFormat((d)=>this.ratio === "percentage" ? d.toLocaleString(undefined, {
style: "percent"
}) : this.yTickFormat(d)));
// remove baseline
g.select(".domain").remove();
// remove default formats
g.attr("font-family", null).attr("font-size", null);
// change line style defaults
g.selectAll("line").attr("stroke-dasharray", 1).attr("stroke", "var(--gv-grey)");
}
async setData(data) {
this.rawData = data;
this.data = this.parse(data);
this.series = this.series || (0, $5OpyM$union)(this.data.map((d)=>d[this.yAxisProp]));
// only set the color scale, as of the first time you get the data
if (!this.scaleColor) this.setColorScale();
const grouped = (0, $5OpyM$group)(this.data, (d)=>d[this.xAxisProp], (d)=>d[this.yAxisProp]);
// https://d3js.org/d3-shape/stack#_stack
this.stack = (0, $5OpyM$stack)().keys(this.series).value(([, group], key)=>{
const item = group.get(key);
if (!item) return 0;
return !!this.countProp ? // otherwise, we count the amount of items
item.reduce((acc, d)=>acc + d[this.countProp], 0) : item.length;
}).order(this.sortStack ? (0, $5OpyM$stackOrderAscending) : (0, $5OpyM$stackOrderReverse)).offset(this.ratio === "percentage" ? (0, $5OpyM$stackOffsetExpand) : (0, $5OpyM$stackOffsetNone))(grouped);
await this.getLocale();
this.build();
}
setColorScale() {
this.scaleColor = (0, $5OpyM$scaleOrdinal)().domain(this.series).range(this.PALETTE);
}
setScales() {
this.svg.attr("width", `${this.width + this.margin.left + this.margin.right}`).attr("height", `${this.height + this.margin.top + this.margin.bottom}`);
this.scaleY = (0, $5OpyM$scaleLinear)().domain([
0,
this.ratio === "percentage" ? 1 : (0, $5OpyM$max)(this.stack, (d)=>(0, $5OpyM$max)(d, (d)=>d[1]))
]).nice().range([
this.height,
0
]);
this.scaleX = (0, $5OpyM$scaleBand)().domain(this.categories || [
...new Set(this.data.map((d)=>d[this.xAxisProp]))
]).paddingInner(0.5).rangeRound([
this.width / this.data.map((d)=>d[this.xAxisProp]).length / 2,
this.width - this.width / this.data.map((d)=>d[this.xAxisProp]).length / 2
]);
}
onPointerMove(event, d) {
const tooltip = this.tooltipContainer.html(this.tooltip(d));
const [x, y] = this.tooltipPosition(event, this.tooltipContainer.node(), 10);
tooltip.style("top", `${y}px`).style("left", `${x}px`).style("pointer-events", "auto").transition().duration(200).style("opacity", 1);
}
onPointerOut() {
this.tooltipContainer.style("pointer-events", "none").transition().duration(200).style("opacity", 0);
}
parse(data) {
// 1. remove those elements with no X axis nor Y axis data
// 2. enforce numeric type for countProp
return data.reduce((acc, item)=>{
// enforce data object to define X-axis and Y-axis properties
if (Object.hasOwnProperty(item, this.xAxisProp) || Object.hasOwnProperty(item, this.yAxisProp)) return acc;
if (this.countProp) // whenever this property is defined, ensure to be numeric
item[this.countProp] = +item[this.countProp] || 0;
acc.push(item);
return acc;
}, []).sort(this.sortBy(this.xAxisProp));
}
defaultTooltip(d) {
const tooltipContent = Array.from(d.data[1]).map(([key, values])=>{
const value = this.countProp ? values.reduce((acc, item)=>acc + item[this.countProp], 0) : values.length;
return `
<div class="tooltip-barchart-stacked-grid">
<span style="background-color: ${this.scaleColor(key)}" class="tooltip-barchart-stacked-grid-key-color"></span>
<span class="tooltip-barchart-stacked-grid-key">${key}:</span>
<span class="tooltip-barchart-stacked-grid-value">${value}</span>
</div>`;
});
return `
<span class="tooltip-barchart-stacked-title">${this.xTickFormat(d.data[0])}</span>
${tooltipContent.join("")}
`;
}
setX(value) {
this.xAxisProp = value;
}
setY(value) {
this.yAxisProp = value;
}
setCount(value) {
this.countProp = value;
}
setXTickValues(value) {
this.xTickValues = value;
}
setYTickValues(value) {
this.yTickValues = value;
}
setSortStack(value) {
this.sortStack = value;
}
setTooltip(value) {
this.tooltip = value;
}
setOnClick(value) {
this.onClick = value;
}
setRatio(value) {
this.ratio = value;
}
setCategories(value) {
this.categories = value;
}
setSeries(value) {
this.series = value;
}
setMargin(value) {
this.margin = {
...this.margin,
...value
};
this.container.replaceChildren();
this.getDimensions();
this.setupElements();
this.build();
}
}
class $8e2c647a76098aec$export$2e2bcd8739ae039 extends (0, $f9a0fbfe241eb50c$export$2e2bcd8739ae039) {
constructor(container, data, options = {}){
super(container, data, options);
this.tooltip = options.tooltip || this.defaultTooltip;
// main properties to display
this.xAxisProp = options.x;
this.yAxisProp = options.y;
this.countProp = options.count;
this.ratio = options.ratio || "absolute";
this.height = options.height || 600;
this.moveLabels = options.moveLabels;
this.yTickFormat = options.yTickFormat || ((d)=>d);
this.yTickValues = options.yTickValues;
this.categories = options.categories;
this.series = options.series;
this.margin = {
top: 36,
bottom: 24,
left: 120,
right: 48,
...options.margin
};
// chart size
this.getDimensions();
// static elements (do not redraw)
this.setupElements();
if (data.length) this.setData(data);
}
getDimensions() {
const { width: width } = this.container.getBoundingClientRect();
this.width = width - this.margin.left - this.margin.right;
}
setupElements() {
this.svg = (0, $5OpyM$select)(this.container).classed("gv-container", true).append("svg").attr("class", "gv-plot");
this.tooltipContainer = (0, $5OpyM$select)(this.container).append("div").attr("class", "gv-tooltip gv-tooltip-bar-chart-small");
this.g = this.svg.append("g").attr("transform", `translate(${this.margin.left} ${this.margin.top})`);
this.g.append("g").attr("class", "axis axis-y");
}
build() {
this.setScales();
const dataGroup = (0, $5OpyM$group)(this.data, (d)=>d[this.xAxisProp]);
// in case series are defined, we add empty groups for each missing key
if (this.series) (0, $5OpyM$difference)(this.series, dataGroup.keys()).forEach((key)=>dataGroup.set(key, []));
this.g.select(".axis-y").call(this.yAxis.bind(this));
this.g.selectAll(".title").remove().selectAll(".wrap-text").remove();
const gColumn = this.g.selectAll(".column").data(dataGroup, ([key])=>key).join("g").attr("class", "column").attr("transform", ([key])=>`translate(${this.scaleColumn(key) + 5},0)`);
const maxWidthColumn = this.width / [
...new Set(this.data.map((d)=>d[this.xAxisProp]))
].length;
gColumn.append("text").attr("class", "title").text(([key])=>key).attr("y", -21).call(this.wrap, maxWidthColumn);
gColumn.selectAll(".bar-chart-small-underlying").data(this.scaleY.domain()).join("rect").attr("x", 0).attr("y", (d)=>this.scaleY(d)).attr("width", this.scales.range()[1]).attr("height", this.scaleY.bandwidth()).attr("opacity", ".2").attr("fill", "var(--gv-grey)").attr("class", "bar-chart-small-underlying");
gColumn.selectAll(".bar-chart-small-overlying").data(([, values])=>values).join("rect").attr("x", 0).attr("y", (d)=>isNaN(this.scaleY(d[this.yAxisProp])) ? 0 : this.scaleY(d[this.yAxisProp])).attr("width", (d)=>this.scales(d[this.countProp])).attr("height", this.scaleY.bandwidth()).attr("fill", (d)=>this.scaleColor(d[this.xAxisProp])).attr("class", "bar-chart-small-overlying").attr("cursor", "pointer").on("touchmove", (e)=>e.preventDefault()).on("pointermove", this.onPointerMove.bind(this)).on("pointerout", this.onPointerOut.bind(this));
}
yAxis(g) {
g.call((0, $5OpyM$axisLeft)(this.scaleY).tickValues(this.yTickValues).tickFormat((d)=>this.yTickFormat(d)).tickPadding(6).tickSize(10));
// remove baseline
g.select(".domain").remove();
// remove default formats
g.attr("font-family", null).attr("font-size", null);
}
async setData(data) {
this.rawData = data;
this.data = this.parse(data);
this.groupAxisProps = this.series || [
...new Set(this.data.map((d)=>d[this.xAxisProp]).filter((item)=>item))
];
// only set the color scale, as of the first time you get the data
if (!this.scaleColor) this.setColorScale();
this.build();
}
setScales() {
this.svg.attr("width", `${this.width + this.margin.left + this.margin.right}`).attr("height", `${this.height + this.margin.top + this.margin.bottom}`);
this.scaleY = (0, $5OpyM$scaleBand)().domain(this.categories || [
...new Set(this.data.map((d)=>d[this.yAxisProp]))
].reverse()).range([
this.height,
0
]).padding(0.4);
// https://d3js.org/d3-array/group#groupSort
const dataGroupSorted = this.series || (0, $5OpyM$groupSort)(this.data, (D)=>-1 * D.reduce((acc, { [this.countProp]: count = 0 })=>acc + count, 0), (d)=>d[this.xAxisProp]);
this.scaleColumn = (0, $5OpyM$scaleBand)().domain(dataGroupSorted).range([
0,
this.width
]).paddingInner(0.4);
this.scaleXMax = this.groupAxisProps.map((scale)=>{
return (0, $5OpyM$max)(this.data.filter((element)=>scale.includes(element[this.xAxisProp])), (d)=>d[this.countProp]);
});
this.scales = (0, $5OpyM$scaleLinear)().range([
0,
this.scaleColumn.bandwidth()
]).domain([
0,
(0, $5OpyM$max)(this.scaleXMax)
]).nice();
}
onPointerMove(event, d) {
const tooltip = this.tooltipContainer.html(this.tooltip(d));
const [x, y] = this.tooltipPosition(event, this.tooltipContainer.node(), 10);
tooltip.style("top", `${y}px`).style("left", `${x}px`).style("pointer-events", "auto").transition().duration(400).style("opacity", 1);
}
onPointerOut() {
this.tooltipContainer.style("pointer-events", "none").transition().delay(300).duration(200).style("opacity", 0);
(0, $5OpyM$selectAll)(".bar-chart-small").transition().duration(200).style("opacity", 1);
}
parse(data) {
// Your data can contains multiple elements
// with the same xAxisProp and yAxisProp
// we need to group them and sum their value of countProp
return [
...data.reduce((acc, item)=>{
// enforce data object to define X-axis and Y-axis properties
if (Object.hasOwnProperty(item, this.xAxisProp) || Object.hasOwnProperty(item, this.yAxisProp) || Object.hasOwnProperty(item, this.countProp)) return acc;
const key = `${item[this.xAxisProp]}-${item[this.yAxisProp]}`;
const value = acc.get(key);
if (value) {
item[this.countProp] = value[this.countProp] + item[this.countProp] || 0;
item.count = value.count + 1;
return acc.set(key, item);
}
item[this.countProp] = item[this.countProp] || 0;
return acc.set(key, {
...item,
count: 1
});
}, new Map()).values()
].sort(this.sortBy(this.yAxisProp));
}
setColorScale() {
this.scaleColor = (0, $5OpyM$scaleOrdinal)().domain(Array.from(new Set(this.data.map((d)=>d[this.xAxisProp])))).range(this.moveLabels ? this.PALETTE : this.PALETTE.filter((element)=>element !== 'var(--gv-color-6)'));
}
defaultTooltip(d) {
return `
<div class="bar-chart-small-tooltip">
<h2 class="bar-chart-small-tooltip-title">${d[this.yAxisProp]}</h2>
<span class="bar-chart-small-tooltip-value">${d[this.countProp]}</span>
</div>
`;
}
setX(value) {
this.xAxisProp = value;
}
setY(value) {
this.yAxisProp = value;
}
setCount(value) {
this.countProp = value;
}
setYTickValues(value) {
this.yTickValues = value;
}
setCategories(value) {
this.categories = value;
}
setSeries(value) {
this.series = value;
}
setTooltip(value) {
this.tooltip = value;
}
setMargin(value) {
this.margin = {
...this.margin,
...value
};
this.container.replaceChildren();
this.getDimensions();
this.setupElements();
this.build();
}
}
class $efd41208f9a0ae54$export$2e2bcd8739ae039 extends (0, $f9a0fbfe241eb50c$export$2e2bcd8739ae039) {
constructor(container, data, options = {}){
super(container, data, options);
this.breadcrumb = options.breadcrumb || this.defaultBreadcrumb;
this.itemTemplate = options.itemTemplate || this.defaultItemTemplate;
this.tooltip = options.tooltip || this.defaultTooltip;
this.margin = {
top: 30,
bottom: 0,
left: 0,
right: 0,
...options.margin
};
this.onLeafClick = options.onLeafClick || (()=>{});
// main properties to display
this.groupProp = options.group || "group";
this.valueProp = options.value; // if no value, use length to compute size
this.idProp = options.id || "id";
this.rootTitle = options.rootTitle || "root";
// chart size
this.getDimensions();
// static elements (do not redraw)
this.setupElements();
if (data.length) this.setData(data);
}
getDimensions() {
const { width: width, height: height } = this.container.getBoundingClientRect();
const minHeight = height > 0 ? height : width / 2;
this.width = width - this.margin.left - this.margin.right;
this.height = minHeight - this.margin.top - this.margin.bottom;
}
setupElements() {
this.svg = (0, $5OpyM$select)(this.container).classed("gv-container", true).append("svg").attr("class", "gv-plot").on("pointerleave", this.onPointerLeave.bind(this));
this.tooltipContainer = (0, $5OpyM$select)(this.container).append("div").attr("class", "gv-tooltip");
}
build() {
const TRANSITION_DURATION = 350;
this.svg.attr("viewBox", `0 0 ${this.width + this.margin.left + this.margin.right} ${this.height + this.margin.top + this.margin.bottom}`);
const render = (group, root)=>{
if (root === null) return;
this.getGroupItems = [
...new Set(this.rawData.map((item)=>item[this.groupProp[0]]))
];
let rootNodes = root.children.length > 1 ? root.children.concat(root) : root.children[0].children;
const node = group.selectAll("g").data(rootNodes).join("g");
/*Create a "fake" breadcumb, if the group data only contains one value,
we filter it to go to the next level, but we lose the breadcrumb title,
so in this case we have to add it*/ if (root.children.length === 1) this.svg.append('text').attr('id', 'first-breadcrumb').attr('class', 'treemap-breadcrumb').attr('y', '21px').attr('x', '0').style('text-anchor', 'start').style('font-weight', 'bold').text(this.getGroupItems[0]);
else this.svg.select('#first-breadcrumb').remove();
node.on("touchmove", (e)=>e.preventDefault()).on("pointerenter", (e, d)=>d === root && this.onPointerLeave(e, d)).on("pointermove", this.onPointerMove.bind(this)).attr("cursor", "pointer").attr("class", (d)=>d === root ? "treemap-breadcrumb" : "treemap-item").on("click", (e, d)=>{
//Disable the click on breadcrumbs if we are in the first level.
if (d === root && this.getGroupItems.length === 1 && root.parent.data.title === "root") return;
else return d === root ? zoomout(root) : d.height === 0 ? this.onLeafClick(e, d) : zoomin(d);
});
node.append("rect").attr("id", (d)=>d.leafUid = `tm-leaf-${this.seed()}`).attr("data-id", (d)=>d.data[this.idProp]).attr("fill", (d)=>{
if (d === root) return "transparent";
while(d.depth > 1)d = d.parent;
return this.scaleColor(d.data[this.idProp]);
}).attr("stroke", "#fff");
node.append("clipPath").attr("id", (d)=>d.clipUid = `tm-clip-${this.seed()}`).append("use").attr("xlink:href", (d)=>new URL(`#${d.leafUid}`, location));
node.append("foreignObject").attr("clip-path", (d)=>d.clipUid).append("xhtml:div").attr("class", (d)=>d === root ? "treemap-breadcrumb" : "treemap-item").html((d)=>d === root ? this.breadcrumb(this.nodePath(d)) : this.itemTemplate(d));
group.transition().duration(TRANSITION_DURATION).call(position, root);
};
const position = (group, root)=>{
const g = group.selectAll("g").attr("transform", (d)=>d === root ? `translate(0, 0)` : `translate(${this.scaleX(d.x0)} ${this.scaleY(d.y0) + this.margin.top})`);
g.select("rect").attr("width", (d)=>d === root ? this.width : this.scaleX(d.x1) - this.scaleX(d.x0)).attr("height", (d)=>d === root ? this.margin.top : this.scaleY(d.y1) - this.scaleY(d.y0));
g.select("foreignObject").attr("width", (d)=>d === root ? this.width : this.scaleX(d.x1) - this.scaleX(d.x0)).attr("height", (d)=>d === root ? this.margin.top : this.scaleY(d.y1) - this.scaleY(d.y0)).selectChild().call((e)=>e.style("opacity", 0).transition().duration(TRANSITION_DURATION).style("opacity", 1)).on("end", (d, ix, nodes)=>{
if (d === root) return null;
const node = nodes[ix];
if (node && node.parentNode) {
let { width: w, height: h } = node.getBoundingClientRect();
const { width: pW, height: pH } = node.parentNode.getBoundingClientRect();
// if the template does not fit in the parent
if (w > pW || h > pH) while(w > pW || h > pH){
if (node.lastChild) {
// remove children one by one, until the template fits
node.lastChild.remove();
({ width: w, height: h } = node.getBoundingClientRect());
} else break;
}
}
});
};
const zoomin = (d)=>{
const group0 = group.attr("pointer-events", "none");
const group1 = group = this.svg.append("g").call(render, d);
this.scaleX.domain([
d.x0,
d.x1
]);
this.scaleY.domain([
d.y0,
d.y1
]);
this.svg.transition().duration(TRANSITION_DURATION).call((t)=>group0.transition(t).remove().call(position, d.parent)).call((t)=>group1.transition(t).attrTween("opacity", ()=>(0, $5OpyM$interpolate)(0, 1)).call(position, d));
};
// When zooming out, draw the old nodes on top, and fade them out.
const zoomout = (d)=>{
const group0 = group.attr("pointer-events", "none");
const group1 = group = this.svg.insert("g", "*").call(render, d.parent);
this.scaleX.domain([
d.parent.x0,
d.parent.x1
]);
this.scaleY.domain([
d.parent.y0,
d.parent.y1
]);
this.svg.transition().duration(TRANSITION_DURATION).call((t)=>group0.transition(t).remove().attrTween("opacity", ()=>(0, $5OpyM$interpolate)(1, 0)).call(position, d)).call((t)=>group1.transition(t).call(position, d.parent));
};
// if there's no value to sum, just count the node
const valueFn = this.valueProp ? (0, $5OpyM$hierarchy)(this.data).sum((d)=>d[this.valueProp]).sort((a, b)=>b.value - a.value) : (0, $5OpyM$hierarchy)(this.data).count().sort((a, b)=>b.value - a.value);
// tile function required to place the "groupData" (see parse func.)
const root = (0, $5OpyM$treemap)().tile(this.tile.bind(this))(valueFn);
this.setScales();
// clean the elements before render
this.svg.selectAll("*").remove();
let group = this.svg.append("g").call(render, root);
}
async setData(data) {
this.rawData = data;
this.data = this.parse(data);
if (!this.scaleColor) this.setColorScale();
// wait for the locales resolution before draw anything
await this.getLocale();
this.build();
}
setScales() {
this.scaleX = (0, $5OpyM$scaleLinear)().rangeRound([
0,
this.width
]);
this.scaleY = (0, $5OpyM$scaleLinear)().rangeRound([
0,
this.height
]);
}
setColorScale() {
this.scaleColor = (0, $5OpyM$scaleOrdinal)().range(this.PALETTE);
}
onPointerMove(event, d) {
// the breadcrumb group is always the last item, so, if there's no next sibling, it's breadcrumb
const isBreadcrumb = !event.target.closest("g").nextSibling;
if (!this.cursorInsideTooltip && !isBreadcrumb && d.parent) {
const tooltip = this.tooltipContainer.html(this.tooltip(d));
const [x, y] = this.tooltipPosition(event, this.tooltipContainer.node(), 10);
tooltip.style("top", `${y}px`).style("left", `${x}px`).style("pointer-events", "auto").call((t)=>t.transition().duration(400).style("opacity", 1)).on("pointerover", ()=>this.cursorInsideTooltip = true).on("pointerleave", ()=>this.cursorInsideTooltip = false);
}
}
onPointerLeave() {
if (!this.cursorInsideTooltip) this.tooltipContainer.style("pointer-events", "none").transition().delay(1000).duration(400).style("opacity", 0);
}
tile(node, x0, y0, x1, y1) {
(0, $5OpyM$treemapBinary)(node, 0, 0, this.width, this.height);
for (const child of node.children){
child.x0 = x0 + child.x0 / this.width * (x1 - x0);
child.x1 = x0 + child.x1 / this.width * (x1 - x0);
child.y0 = y0 + child.y0 / this.height * (y1 - y0);
child.y1 = y0 + child.y1 / this.height * (y1 - y0);
}
}
parse(data) {
const reduce = this.valueProp ? (v)=>(0, $5OpyM$sum)(v, (d)=>d[this.valueProp]) : ()=>{};
const groupBys = Array.isArray(this.groupProp) ? this.groupProp.map((prop)=>(d)=>d[prop]) : [
(d)=>d[this.groupProp]
];
// since rollup "reduces" the data, it only works for creating the categories
const rollupData = (0, $5OpyM$rollup)(data, reduce, ...groupBys);
// still needing which items belongs to what category, so appends also the group function
const groupData = (0, $5OpyM$group)(data, ...groupBys);
// hierarchies always require an object
return {
[this.idProp]: this.rootTitle,
children: this.nest(rollupData, groupData)
};
}
nest(rollup, group) {
// https://observablehq.com/@bayre/unrolling-a-d3-rollup
return Array.from(rollup, ([key, value])=>value instanceof Map ? {
[this.idProp]: key,
children: this.nest(value, group.get(key))
} : {
[this.idProp]: key,
children: group.get(key)
});
}
nodePath(d) {
const nodes = d.ancestors().reverse().map((d)=>d.data[this.idProp]);
return this.data.children.length > 1 ? nodes : nodes.filter((node)=>node !== 'root');
}
defaultBreadcrumb(d) {
return d.map((pathName)=>`<span>${pathName}</span>`).join(" / ");
}
defaultItemTemplate(d) {
return [
`<div><strong>${d.data[this.idProp]}</strong></div>`,
`<div>${d.value.toLocaleString()}</div>`,
d.children && `<div>${d.children.length}</div>`
].join("");
}
defaultTooltip(d) {
return d.children && d.data.children.map((x)=>`
<div class="treemap-tooltip-block">
${[
`<div class="treemap-tooltip-id">${x[this.idProp]}</div>`,
x[this.valueProp] && `<div class="treemap-tooltip-values">${x[this.valueProp].toLocaleString()}</div>`
].join("")}
</div>
`).join("");
}
setGroup(value) {
this.groupProp = value;
this.setData(this.rawData);
}
setValue(value) {
this.valueProp = value;
this.setData(this.rawData);
}
setId(value) {
this.idProp = value;
this.setData(this.rawData);
}
setRootTitle(value) {
this.rootTitle = value;
this.setData(this.rawData);
}
setItemTemplate(value) {
this.itemTemplate = value;
this.build();
}
setBreadcrumb(value) {
this.breadcrumb = value;
this.build();
}
setTooltip(value) {
this.tooltip = value;
this.build();
}
setOnLeafClick(value) {
this.onLeafClick = value;
this.build();
}
setMargin(value) {
this.margin = {
...this.margin,
...value
};
this.container.replaceChildren();
this.getDimensions();
this.setupElements();
this.build();
}
}
class $9366a5821b508d57$export$2e2bcd8739ae039 extends (0, $f9a0fbfe241eb50c$export$2e2bcd8739ae039) {
constructor(container, data, options = {}){
super(container, data, options);
// this.tooltip = options.tooltip || this.defaultTooltip
this.margin = {
top: 30,
bottom: 0,
left: 0,
right: 0,
...options.margin
};
this.onClick = options.onClick || (()=>{});
this.tooltip = options.tooltip || this.defaultTooltip;
// main properties to display
this.fromProp = options.from || "from";
this.toProp = options.to || "to";
this.xAxisProp = options.x || "phase";
this.yAxisProp = options.y || "group";
this.idProp = options.id || "id";
// band item height
this.BAR_HEIGHT = options.barHeight || 10;
// chart size
this.getDimensions();
// static elements (do not redraw)
this.setupElements();
i