Published
Edited
Jun 7, 2022
Importers
2 stars
Insert cell
Insert cell
tabel2_temporal = timesHeatMap(
tbl2t3_processed.objects(),
"Access Point",
"Political Label",
"Percent",
["#ff1100", "#ffe100", "#26ff00", "#0040ff", "#ae00ff", "black"],
["#2166AC", "#92C5DE", "#7FC97F", "gray", "#F4A582", "#B2182B"],
["YT Homepage", "Search", "YT Channel", "YT Video", "External URLs", "Other"],
["fL", "L", "AW", "C", "R", "fR"],
["Far Left", "Left", "Anti-Woke", "Center", "Right", "Far Right"],
null
)
Insert cell
Insert cell
timesHeatMap = (
data,
xTitle,
yTitle,
topTitle,
palette_x,
palette_y,
xOrdering,
yOrdering,
xLabel = null,
yLabel = null
) => {
const margin = { top: 10, right: 20, bottom: 40, left: 40 };
const innerMargin = { top: 10, right: 10 };
const w = width - margin.left - margin.right;
const h = 450 - margin.top - margin.bottom;

const chartBody = document.createElement("div");
chartBody.id = "chartBody";

const svg = d3.create("svg").attr("viewBox", [0, 0, w, h]);

const tooltip = d3
.create("div")
.style("visibility", "hidden")
.attr("class", "tooltip")
.style("background-color", "white")
.style("border", "solid")
.style("border-width", "2px")
.style("border-radius", "5px")
.style("position", "absolute")
.style("padding", "5px")
.text("im a tooltip");

const legend = basicLegend(xLabel, yOrdering, palette_y);

const xGroups = xOrdering
? [...new Set(data.map((d) => d.x))].sort((a, b) => {
return xOrdering.indexOf(a) - xOrdering.indexOf(b);
})
: [...new Set(data.map((d) => d.x))].sort();
const yGroups = yOrdering
? [...new Set(data.map((d) => d.y))].sort((a, b) => {
return yOrdering.indexOf(a) - yOrdering.indexOf(b);
})
: [...new Set(data.map((d) => d.y))];

let targetData;
let selectedRects = new Set();

const x = d3
.scaleBand()
.domain(xGroups)
.range([margin.left, w - margin.right])
.padding(0.05);

const xAxis = d3.axisBottom(x).tickSize(0);

svg
.append("text")
.attr("text-anchor", "end")
.attr("x", w)
.attr("y", h - 5)
.text(xTitle);

let topBarCategory;

svg
.append("g")
.attr("id", "x-axis")
.attr("transform", `translate(0, ${h - margin.bottom})`)
.call(xAxis)
.select(".domain")
.remove();

const yAxisTitle = svg
.append("text")
.attr("text-anchor", "end")
.attr("transform", "rotate(-90)")
.attr("y", margin.left - 28) //due to rotation, y is now x and vice versa
.attr("x", -margin.top)
.text(yTitle);

const y = d3
.scaleBand()
.domain(yGroups)
.range([margin.top, h - margin.bottom])
.padding(0.05);

const yAxis = d3.axisLeft(y).tickSize(0);

svg
.append("g")
.attr("id", "y-axis")
.attr("transform", `translate( ${margin.left}, 0)`)
.call(yAxis)
.select(".domain")
.remove();

// svg
// .select("#y-axis")
// .selectAll(".tick")
// .on("click", (e) => {
// showTemporal(e.target.__data__);
// e.target.style = "color: red; font-weight: bold;";
// })
// .on("mouseover", (e) => {
// e.target.style = "color: red; font-weight: bold;";
// })
// .on("mouseout", (e) => {
// e.target.style = "color: black; font-weight: normal;";
// });

const clip = svg
.append("defs")
.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", w + 5)
.attr("height", h + 10)
.attr("x", margin.left - 5)
.attr("y", 0);

const topX = d3.scaleTime().range([margin.left, w - margin.right]);

const topXAxis = d3.axisBottom(topX);

const topXSVGAxis = svg.append("g");

topXSVGAxis
.attr("id", "top-x-axis")
.attr("transform", `translate(0, ${(h - margin.bottom) / 2 - 20})`)
.call(topXAxis)
.style("opacity", 0)
.select(".domain")
.remove();

let brush = d3.brushX();

const topY = d3.scaleLinear().range([margin.top, margin.top]);

const topYAxis = d3.axisLeft(topY);

const topYSVGAxis = svg.append("g");

topYSVGAxis
.attr("id", "top-y-axis")
.attr("transform", `translate( ${margin.left}, 0)`)
.call(topYAxis)
.style("opacity", 0)
.select(".domain")
.remove();

const topYAxisTitle = svg
.append("text")
.attr("text-anchor", "end")
.attr("transform", "rotate(-90)")
.style("opacity", 0)
.attr("y", margin.left - 28) //due to rotation, y is now x and vice versa
.attr("x", -margin.top)
.text(topTitle);

const temporalDots = svg.append("g").attr("clip-path", "url(#clip)");

let linefunc = d3.line().curve(d3.curveBasis);

const lineGroup = svg.append("g").attr("clip-path", "url(#clip)");

const color = d3
.scaleSequentialLog()
.interpolator(d3.interpolate("white", "orange"))
.domain(d3.extent(data, (d) => d.avg_value));

// const color = xGroups.map((d) => {
// let xGroup = data.filter((w) => w.x == d);
// let xExtent = d3.extent(xGroup, (n) => n.avg_value);
// let xRange = xExtent[1] - xExtent[0];
// return {
// group: d,
// func: d3
// .scaleSequential()
// .interpolator(d3.interpolate("white", "orange"))
// .domain([xExtent[0] - xRange / 2, xExtent[1]])
// };
// });

const topColorY = d3.scaleOrdinal().domain(yOrdering).range(palette_y);

const topColorX = d3.scaleOrdinal().domain(xOrdering).range(palette_x);

const rectGroup = svg.append("g");
rectGroup
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", (d) => x(d.x) + 5)
.attr("y", (d) => y(d.y))
.attr("width", x.bandwidth() - 5)
.attr("height", y.bandwidth())
.style("stroke", "#ddd")
.style("opacity", 0.8)
.style("fill", (d) => color(d.avg_value ? d.avg_value : 0))
// .style("fill", (d) =>
// color.filter((n) => n.group == d.x)[0].func(d.avg_value ? d.avg_value : 0)
// )
.attr("class", (d) => `rect-${d.x}-${d.y}-${d.avg_value}`)
.on("mouseover", (d) => {
tooltip.style("visibility", "visible");
d.target.style.stroke = "black";
})
.on("mouseleave", (d) => {
tooltip.style("visibility", "hidden");
if (!selectedRects.has(d.target.getAttribute("class"))) {
d.target.style.stroke = "#ddd";
}
})
.on("mousemove", (d) => {
tooltip
.html(`Value: ${d.target.getAttribute("class").split("-")[3]}%`)
.style("left", d.offsetX + 10 + "px")
.style("top", d.offsetY + 10 + "px");
})
.on("click", (d) => {
let currClass = d.target.getAttribute("class");
if (selectedRects.has(currClass)) {
selectedRects.delete(currClass);
} else if (selectedRects.size == 6) {
tooltip
.html(`Maximum of 6 cells selected.`)
.style("left", d.offsetX + 10 + "px")
.style("top", d.offsetY + 10 + "px");
} else {
selectedRects.add(currClass);
}
console.log(selectedRects);
if (selectedRects.size == 0) {
resetchart();
} else {
showTemporal(d.target.__data__);
}
});

let idleTimeout;
function idled() {
idleTimeout = null;
}

const exitBtn = d3
.create("div")
.attr("id", "exitBtn")
.attr(
"style",
"position: absolute; top: 10px; right: 0px; width: 20px; height: 20px; border-radius: 2px; background-color: #eee; display: flex; justify-content: center; align-items: center; cursor: pointer;"
)
.text("x")
.style("visibility", "hidden")
.on("click", () => {
resetchart();
})
.on("mouseover", (d) => {
tooltip.style("position", "fixed");
tooltip.style("visibility", "visible");
})
.on("mouseleave", (d) => {
tooltip.style("position", "absolute");
tooltip.style("visibility", "hidden");
})
.on("mousemove", (d) => {
tooltip
.html(`Clear selection`)
.style("left", d.clientX + 10 + "px")
.style("top", d.clientY + 10 + "px");
});
chartBody.appendChild(exitBtn.node());

function resetchart() {
selectedRects.clear();
x.range([margin.left, w - margin.right]);
y.range([margin.top, h - margin.bottom]);

//svg.select("#x-rightBar-axis").remove();
//svg.select("#y-topBar-axis").remove();

svg.select("#y-axis").selectAll("text").style =
"color: black; font-weight: normal";

topY.range([margin.top, margin.top]);
topYSVGAxis.style("opacity", 0);
topXSVGAxis.style("opacity", 0);

topYAxisTitle.style("opacity", 0);

rectGroup.selectAll("rect").style("stroke", "#ddd");

temporalDots.selectAll("circle").remove();
lineGroup.selectAll("path").remove();

exitBtn.style("visibility", "hidden");

updateMaterials();

yAxisTitle.transition().duration(1000).attr("x", -margin.top);
}

function showTemporal(target) {
temporalDots.selectAll("circle").remove();
lineGroup.selectAll("path").remove();
temporalDots.selectAll(".brush").remove();

exitBtn.style("visibility", "visible");

targetData = [].concat(
...data
.filter((d) => selectedRects.has(`rect-${d.x}-${d.y}-${d.avg_value}`))
.map((d) => d.temporal)
);

rectGroup.selectAll("rect").stroke = "none";
rectGroup.selectAll("rect").attr("stroke", (d) => {
return d.y == target.y && d.x == target.x ? "black" : "none";
});

y.range([(h - margin.bottom) / 2, h - margin.bottom]);

topY
.range([(h - margin.bottom) / 2 - 20, margin.top])
.domain(d3.extent(targetData, (d) => d.y));
topYAxis.tickSizeInner(-(w - margin.left)).tickSizeOuter(3);

topX
.range([margin.left, w - margin.right])
.domain(d3.extent(targetData, (d) => d.x));

topXAxis.tickSizeOuter(3);

topXSVGAxis.style("opacity", 1);
topYSVGAxis.style("opacity", 1);

topYAxisTitle.style("opacity", 1);

temporalDots
.selectAll("circle")
.data(targetData)
.enter()
.append("circle")
.attr("class", (d) => "dot-" + target)
.attr("cx", (d) => topX(d.x))
.attr("cy", (d) => topY(d.y))
.attr("r", 4)
.attr("fill", (d) => topColorY(d.label))
//.attr("stroke", (d) => topColorX(d.category))
.attr("stroke-width", 3)
.style("opacity", 0.7);

linefunc = linefunc.x((d) => topX(d.x)).y((d) => topY(d.rolling_y));

console.log(targetData);
console.log(d3.group(targetData, (d) => `${d.category}-${d.label}`));

lineGroup
.selectAll("path")
.data(d3.group(targetData, (d) => `${d.category}-${d.label}`))
.enter()
.append("path")
.attr("class", (d) => "line-" + d[0])
.attr("fill", "none")
.attr("stroke", (d) => {
console.log(d[0].split("-")[1]);
console.log(topColorY(d[0].split("-")[1]));
return topColorY(d[0].split("-")[1]);
})
.attr("stroke-width", 3)
.attr("d", (d) => {
return linefunc(d[1]);
})
.on("mouseover", (d) => {
tooltip.style("visibility", "visible");
})
.on("mouseleave", (d) => {
tooltip.style("visibility", "hidden");
})
.on("mousemove", (event, d) => {
tooltip
.style("left", event.offsetX + 10 + "px")
.style("top", event.offsetY + 10 + "px")
.text(event.target.getAttribute("class").split("-")[1]);
});

// .style("background-color", "white")
// .style("border", "solid")
// .style("border-width", "2px")
// .style("border-radius", "5px")
// .style("position", "absolute")
// .style("padding", "5px")`

brush
.extent([
[margin.left, 0],
[w, (h - margin.bottom) / 2 - 20]
])
.on("end", updateChart);
temporalDots.append("g").attr("class", "brush").call(brush);

updateMaterials();
}

function updateChart() {
const extent = d3.brushSelection(this);
//deHighlightChart();

// Reset to initial bounds if timeout or selection was zero. Otherwise, zoom to
// specified x boundaries.

if (!extent) {
if (!idleTimeout) return (idleTimeout = setTimeout(idled, 350));
topX.domain(d3.extent(targetData, (d) => d.x));
} else {
topX.domain([topX.invert(extent[0]), topX.invert(extent[1])]);
temporalDots.select(".brush").call(brush.move, null);
}
updateMaterials();
}

function updateMaterials() {
svg.select("#x-axis").transition().duration(1000).call(xAxis);
svg.select("#y-axis").transition().duration(1000).call(yAxis);
svg.selectAll(".domain").remove();

yAxisTitle
//.select("text")
.transition()
.duration(1000)
.attr("x", -margin.top - (h - margin.bottom) / 2);

lineGroup.selectAll("path").style("opacity", 0);

rectGroup
.selectAll("rect")
.transition("rectMove")
.duration(1000)
.attr("x", (d) => x(d.x) + 5)
.attr("y", (d) => y(d.y))
.attr("width", x.bandwidth() - 5)
.attr("height", y.bandwidth());

topYSVGAxis
.transition()
.duration(1000)
.call(topYAxis)
.selectAll(".tick")
.selectAll("line")
.style("stroke", "lightgrey");

topXSVGAxis
.transition()
.duration(1000)
.call(topXAxis)
.selectAll(".tick")
.selectAll("line")
.style("stroke", "lightgrey");

temporalDots
.selectAll("circle")
.transition("temporalDotsMove")
.duration(1000)
.attr("cx", (d) => topX(d.x))
.attr("cy", (d) => topY(d.y));

lineGroup
.selectAll("path")
.transition("pathmovement")
.duration(1000)
.style("opacity", 1)
.attr("d", (d) => linefunc(d[1]))
.attr("stroke", (d) => topColorY(d[0].split("-")[1]));

//brush.clear();
}

chartBody.appendChild(svg.node());
chartBody.appendChild(legend);
chartBody.appendChild(tooltip.node());
return chartBody;
}
Insert cell
tbl2t_data.objects().filter((d) => d.label == "L")[0].temporal
Insert cell
[].concat(
...tbl2t3_processed
.objects()
.filter((d) => d.y == "fL")
.map((d) => d.temporal)
)
Insert cell
Insert cell
viewof tbl2_sorted = tbl2_data
.join(tbl2t_data, ["y", "label"])
.select(aq.not("label"))
.view()
Insert cell
viewof tbl2_data = tbl2_raw
.derive({
percentge: (d) => op.round(op.parse_float(d.percentge) * 100) / 100,
counts: (d) => op.parse_int(d.counts),
label_clean: (d) => (d.label != "IDW" ? d.label : "AW")
})
.groupby("label_clean")
.derive({ info: (d) => [d.percentge, d.counts] })
.pivot("cases", "info")
.rename({
"1": "YT Homepage",
"2": "Search",
"3": "YT Channel",
"4": "YT Video",
"5": "External URLs",
"6": "Other"
})
.fold([
"YT Homepage",
"Search",
"YT Channel",
"YT Video",
"External URLs",
"Other"
])
.impute({ value: () => [0, 0] })
.spread("value", { as: ["value", "subvalue"] })
.rename({ label_clean: "y", key: "x" })
.view()
Insert cell
viewof tbl2t_data = tbl2t_raw
.derive({
label: (d) => (d.label == "IDW" ? "AW" : d.label),
y: (d) => op.parse_float(d.percentge),
x: (d) =>
op.parse_date(d.year + "-" + (d.month < 10 ? "0" + d.month : d.month)),
subvalue: (d) => op.parse_int(d.counts),
category: (d) => op.parse_int(d.cases)
})
.select("label", "x", "y", "subvalue", "category")
.derive({
temporal: `d => { return {x: d.x, y: d.y, subvalue: d.subvalue, category: d.category}}`
})
.groupby("label")
.rollup({
temporal: op.array_agg("temporal")
})
.view()
Insert cell
viewof tbl2t_raw = aq.from(await FileAttachment("tbl2_times.csv").csv()).view()
Insert cell
viewof tbl2_raw = aq.from(await FileAttachment("tbl2_base.csv").csv()).view()
Insert cell
tbl2t3_processed.objects().filter((d) => d.y == "L")[0].temporal
Insert cell
cases_id_to_name = (id) => {
return id == 1
? "YT Homepage"
: id == 2
? "Search"
: id == 3
? "YT Channel"
: id == 4
? "YT Video"
: id == 5
? "External URLs"
: id == 6
? "Other"
: "ERROR: id not known";
}
Insert cell
viewof tbl2t3_processed = tbl2t3_raw
.derive({
label: (d) => (d.label == "IDW" ? "AW" : d.label),
y: (d) => op.parse_float(d.percentge),
x: (d) =>
op.parse_date(d.year + "-" + (d.month < 10 ? "0" + d.month : d.month)),
subvalue: (d) => op.parse_int(d.counts),
category: (d) => op.parse_int(d.cases)
})
.select("label", "x", "y", "subvalue", "category")
.groupby("label", "category")
.derive({
rolling_y: aq.rolling((d) => op.mean(d.y), [-2, 0]),
category: (d) =>
d.category == 1
? "YT Homepage"
: d.category == 2
? "Search"
: d.category == 3
? "YT Channel"
: d.category == 4
? "YT Video"
: d.category == 5
? "External URLs"
: d.category == 6
? "Other"
: "ERROR: id not known"
})
.orderby("x")
.derive({
temporal: `d => { return {x: d.x, y: d.y, rolling_y: d.rolling_y, subvalue: d.subvalue, label: d.label, category: d.category}}`
})
.groupby("label", "category")
.rollup({
avg_value: op.mean("y"),
avg_subvalue: op.mean("subvalue"),
temporal: op.array_agg("temporal")
})
.derive({
avg_value: (d) => op.round(d.avg_value * 100) / 100,
avg_subvalue: (d) => op.round(d.avg_value * 100) / 100
})
.rename({ label: "y", category: "x" })
.view()
Insert cell
tbl2t3_raw
.derive({
label: (d) => (d.label == "IDW" ? "AW" : d.label),
y: (d) => op.parse_float(d.percentge),
x: (d) =>
op.parse_date(d.year + "-" + (d.month < 10 ? "0" + d.month : d.month)),
subvalue: (d) => op.parse_int(d.counts),
category: (d) => op.parse_int(d.cases)
})
.select("label", "x", "y", "subvalue", "category")
.groupby("label", "category")
.derive({ rolling_y: aq.rolling((d) => op.average(d.y), [-2, 0]) })
.filter((d) => d.label == "fL" && d.category == 6)
.orderby("x", aq.desc("x"))
.view()
Insert cell
viewof tbl2t3_raw = aq
.from(await FileAttachment("Table2_temporal_v3.csv").csv())
.view()
Insert cell
Insert cell
Insert cell
d3 = require("d3@v6")
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