Constance = function (data) {
class TreemapHandler {
setup() {
const TM_PADDING = 1;
var nested = d3.group(
data,
(d) => d.product,
(d) => d.id
);
const treemap = d3
.treemap()
.tile(d3.treemapSquarify)
.size([width - 2 * padding, height - 2 * padding])
.padding(TM_PADDING);
let hierarchy = d3.hierarchy(nested);
hierarchy
.sort((a, b) => b.virulence - a.virulence)
.sum((d) => d.virulence);
let t = treemap(hierarchy);
const leaves = hierarchy.leaves();
leaves.forEach((l) => {
l.data.x0 = l.x0;
l.data.x1 = l.x1;
l.data.y0 = l.y0;
l.data.y1 = l.y1;
});
}
getMap() {
return new Map()
.set("rx", 0)
.set("x", (d) => d.x0)
.set("y", (d) => d.y0)
.set("width", (d) => d.x1 - d.x0)
.set("height", (d) => d.y1 - d.y0)
.set("fill", colorNode)
.set("stroke", strokeNode)
.set("stroke-width", strokeWidthNode)
.set("opacity", opacityNode);
}
}
class BubbleHandler {
#sideNode;
#sideRadius;
#scaledHorizontal;
#scaledVertical;
#scaledRadius;
setup() {
const extentD = d3.extent(data, (d) => d.dstart);
const extentV = d3.extent(data, (d) => d.virulence);
const radiusFromValue = (value) => Math.sqrt(value) / Math.PI;
const maxRadius = 100;
this.scaledHorizontal = d3
.scaleTime()
.range([margin + maxRadius / 2, width - margin - maxRadius / 2])
.domain(extentD);
this.scaledVertical = d3
.scaleLinear()
.range([margin + maxRadius / 2, height - margin - maxRadius / 2])
.domain(extentV);
this.scaledRadius = d3
.scaleLinear()
.range([1, maxRadius])
.domain([0, radiusFromValue(extentV[1])]);
this.sideNode = (d) => this.scaledRadius(radiusFromValue(d.virulence));
this.sideRadius = (d) =>
this.scaledRadius(radiusFromValue(d.virulence)) / 2;
}
getMap() {
return new Map()
.set("width", this.sideNode)
.set("height", this.sideNode)
.set("rx", this.sideRadius)
.set("x", (d) => this.scaledHorizontal(d.dstart) - this.sideNode(d))
.set("y", (d) => this.scaledVertical(d.virulence) - this.sideNode(d))
.set("fill", colorNode)
.set("stroke", strokeNode)
.set("stroke-width", strokeWidthNode)
.set("opacity", opacityNode);
}
}
class TimelineHandler {
setup() {
const TL_PADDING = 3;
const TL_MAXBAND_HEIGHT = 20;
const dateExtent = [
d3.min(data, (d) => d.start),
d3.max(data, (d) => d.end)
];
console.log(dateExtent);
const timeline = layoutTimeline()
.size([width - 2 * padding, height - 2 * padding])
.extent(dateExtent)
.padding(TL_PADDING)
.maxBandHeight(TL_MAXBAND_HEIGHT);
let accY = 0;
if (shouldGroup == "Yes") {
// unique products
const groups = Array.from(new Set(data.map((d) => d.product)));
groups.forEach(function (group, i) {
let onlyThisGroup = data.filter(function (d) {
return d.product === group;
});
let onlyThisTimeline = timeline(onlyThisGroup);
let maxY = 0;
onlyThisTimeline.forEach(function (d, j) {
let src = onlyThisGroup[j];
src.y = d.y + accY;
src.dy = d.dy;
src.lane = d.lane;
src.start = d.start;
src.end = d.end;
src.originalEnd = d.originalEnd;
src.originalStart = d.originalStart;
maxY = Math.max(d.y, maxY);
});
accY += maxY + TL_MAXBAND_HEIGHT;
//selection.datum(onlyThisTimeline, function(d) { return d.id; });
});
} else {
let onlyThisTimeline = timeline(data);
let maxY = 0;
onlyThisTimeline.forEach(function (d, j) {
let src = data[j];
src.y = d.y + accY;
src.dy = d.dy;
src.lane = d.lane;
src.start = d.start;
src.end = d.end;
src.originalEnd = d.originalEnd;
src.originalStart = d.originalStart;
maxY = Math.max(d.y, maxY);
});
accY += maxY + TL_MAXBAND_HEIGHT;
}
}
getMap() {
return new Map()
.set("rx", (d) => (d.virulence ? 2 : 0))
.set("x", (d) => d.start)
.set("y", (d) => d.y)
.set("height", (d) => d.dy)
.set("width", (d) => d.end - d.start)
.set("fill", colorNode)
.set("stroke", strokeNode)
.set("stroke-width", strokeWidthNode)
.set("opacity", opacityNode);
}
}
class BarHandler {
#x;
#y;
setup() {
const sumByProduct = new Map();
const byProduct = d3.group(data, (d) => d.product);
byProduct.forEach((v, k) => {
sumByProduct.set(
k,
d3.sum(v, (d) => d.virulence)
);
});
const domain =
shouldGroup == "Yes"
? d3.groupSort(
data,
// TODO: find a better way to map the product to a value that guarantees the desired order
([d]) => -(sumByProduct.get(d.product) * 100 + d.virulence),
(d) => d.id
)
: d3.groupSort(
data,
([d]) => -d.virulence,
(d) => d.id
);
this.x = d3
.scaleBand()
.domain(domain) // descending virulence
.range([margin, width - margin])
.padding(0.1);
this.y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.virulence)])
.range([height - margin - padding, margin - padding]);
}
getMap() {
return new Map()
.set("width", this.x.bandwidth())
.set("height", (d) => this.y(0) - this.y(d.virulence))
.set("rx", 0)
.set("x", (d) => this.x(d.id))
.set("y", (d) => this.y(d.virulence))
.set("fill", colorNode)
.set("stroke", strokeNode)
.set("stroke-width", strokeWidthNode)
.set("opacity", opacityNode);
}
}
////
const vizTypes = new Map()
.set("Bubble", new BubbleHandler())
.set("Bar", new BarHandler())
.set("Timeline", new TimelineHandler())
.set("Treemap", new TreemapHandler());
const container = html`
<div style="position:relative;">
<div id="constance_toolbar" style="display: flex; flex-direction: row; padding: 5px 5px 5px 5px;">
${Array.from(
Array.from(vizTypes.keys()),
(d) => html`<button value="${d}">${d}</button>`
)}
</div>
<div id="constance_chart">
${tooltipTemplate}
</div>
</div>`;
const buttons = d3
.select(container)
.select("#constance_toolbar")
.selectAll("button")
.on("click", (ev) => morphToViz(ev.target.value));
const tooltipDiv = d3.select(container).select(".tooltip");
const svg = d3
.select(container)
.select("#constance_chart")
.append("svg")
.attr("width", width - 2 * padding)
.attr("height", height - 2 * padding)
.style("background", "#FFFFFF")
.append("g");
const rects = svg
.selectAll("rect")
.data(data, (d) => d.id)
.enter()
.append("rect")
.call(tooltip, tooltipDiv);
// Eevent handlers
function onMouseOver(event, d) {
d3.select(event.target).style("cursor", "pointer");
}
// function onMouseMove(event, d) {
// }
function onMouseOut(event, d) {
d3.select(event.target).style("cursor", "default");
}
// toggling state as opposed to clearing out and selecting allows for multi-select,
// which is great for selection
// ..or you could do shift-click
function onClick(event, d) {
if (event.shiftKey) {
selectToggle(d);
} else {
selectOnly(d);
}
morphToViz(currVizType);
}
rects
.on("click", onClick)
.on("mouseover", onMouseOver)
.on("mouseout", onMouseOut);
// .on("mousemove", onMouseMove);
function selectOnly(d) {
data.forEach((d) => (d.selected = false));
d.selected = true;
}
function selectToggle(d) {
d.selected = !d.selected;
}
let currSelectMethod = "fade";
function selectMethod(value) {
currSelectMethod = value;
morphToViz(currVizType);
}
// "Morhing" engine
let currVizType = undefined;
function morphToViz(value) {
const handler = vizTypes.get(value);
if (handler) {
currVizType = value;
morph(handler);
}
}
function morph(handler) {
handler.setup();
rects
.transition()
.delay(function (d, i) {
return i * transitionDelay;
}) // stagger
.duration(transitionTime)
.ease(d3.easeLinear)
.call(applyHandler, handler.getMap());
}
function applyHandler(selection, handlerMap) {
handlerMap.forEach((v, a) => {
selection.attr(a, v);
});
}
function init() {
const largestEntry = data.reduce((prev, curr) => {
return prev.virulence > curr.virulence ? prev : curr;
});
selectOnly(largestEntry);
morphToViz(vizTypes.keys().next().value);
}
init();
return container;
}