Public
Edited
Nov 7, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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;
}
Insert cell
Insert cell
vizChannels = ["id", "product", "virulence", "first", "last"]
Insert cell
function tooltipContents(datum, tooltipDiv) {
// customize this function to set the tooltip's contents however you see fit
tooltipDiv
.selectAll("p")
.data(Object.entries(datum))
.join("p")
.filter(([key, value]) => vizChannels.includes(key))
.html(
([key, value]) =>
`<strong>${key}</strong>: ${
typeof value === "object" ? value.toLocaleString("en-US") : value
}`
);
}
Insert cell
{
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)
);
});
return sumByProduct;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
colorScale = d3.scaleOrdinal(d3.schemeAccent);
Insert cell
Insert cell
strokeNode = (d) => {
return d.selected ? "grey" : "lightgrey";
}
Insert cell
Insert cell
Insert cell
Insert cell
external_padding = 10
Insert cell
padding = 5;
Insert cell
margin = 5
Insert cell
Insert cell
Insert cell
data = {
let data = [
{
id: "S1117269",
status: "O",
product: "COMMONSHTML",
component: "HTML.OBJECTINSPECTOR",
first: "2014-09-15",
last: "2015-10-13",
virulence: 1
},
{
id: "S1123311",
status: "O",
product: "COMMONSHTML",
component: "HTML.OBJECTINSPECTOR",
first: "2014-10-08",
last: "2015-10-13",
virulence: 1
},
{
id: "S1124568",
status: "D",
product: "COMMONSHTML",
component: "HTML.THEMES",
first: "2014-10-14",
last: "2014-11-10",
virulence: 1
},
{
id: "S1147317",
status: "O",
product: "COMMONSHTML",
component: "HTML.THEMES",
first: "2015-01-20",
last: "2015-10-13",
virulence: 2
},
{
id: "S1147340",
status: "N",
product: "COMMONSHTML",
component: "HTML.THEMES",
first: "2015-01-20",
last: "2015-06-10",
virulence: 1
},
{
id: "S1147457",
status: "D",
product: "RDVISUALDESIGN",
component: "RDVISUALDESIGN",
first: "2015-01-21",
last: "2015-02-03",
virulence: 10
},
{
id: "S1150372",
status: "D",
product: "FORECASTWEB",
component: "FORECASTWEB.TRACKINGNODE.RESULTS",
first: "2015-02-02",
last: "2015-02-24",
virulence: 5
},
{
id: "S1164366",
status: "F",
product: "BIPLATFORM",
component: "BIRD.VAV.HTML5",
first: "2015-03-18",
last: "2015-03-28",
virulence: 20
},
{
id: "S1168148",
status: "F",
product: "COMMONSHTML",
component: "HTML.RESPONSIVEPOPOVER",
first: "2015-03-31",
last: "2015-04-23",
virulence: 7
},
{
id: "S1169125",
status: "D",
product: "FORECASTWEB",
component: "FORECASTWEB.INPUTNODE.RESULTS",
first: "2015-04-03",
last: "2015-04-07",
virulence: 9
},
{
id: "S1173252",
status: "D",
product: "FORECASTWEB",
component: "FORECASTWEB.DSW.HIERARCHY",
first: "2015-04-20",
last: "2015-04-22",
virulence: 11
},
{
id: "S1182669",
status: "O",
product: "COMMONSHTML",
component: "HTML.OBJECTINSPECTOR",
first: "2015-05-29",
last: "2015-10-13",
virulence: 1
},
{
id: "S1185409",
status: "O",
product: "COMMONSHTML",
component: "HTML.OBJECTINSPECTOR",
first: "2015-06-11",
last: "2015-10-13",
virulence: 12
},
{
id: "S1203552",
status: "D",
product: "COMMONSHTML",
component: "HTML.THEMES",
first: "2015-09-15",
last: "2015-10-09",
virulence: 1
}
];

function toDate(s) {
var dt = s.split(/[: T-]/).map(parseFloat);
dt = new Date(
dt[0],
dt[1] - 1,
dt[2],
dt[3] || 0,
dt[4] || 0,
dt[5] || 0,
0
);
console.log(`${s} => ${dt}`);
if (dt.getFullYear() == 1969) console.log(`Danger, Will Robinson!!!!`);
return dt;
}
// Cleaning up data => consider changing how it is generated. Use start and end instead of first and last
data.forEach(function (d) {
d.start = toDate(d.first);
d.dstart = d.start; //d.start is changed by timeline
d.end = toDate(d.last);
});

return data.filter((d) => d.virulence > 0);
}
Insert cell
{
let first = "2015-09-15";
let dt = first.split(/[: T-]/).map(parseFloat);
let start = new Date(
dt[0],
dt[1] - 1,
dt[2],
dt[3] || 0,
dt[4] || 0,
dt[5] || 0,
0
);
console.log(first, start, new Date(start));
}
Insert cell
medieval_empires_cleaned@5.json
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
Insert cell
Insert cell
dummy = function (_) {}
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more