A demo visualization of channel move dependencies in the upcoming broadcast television repack.
xxxxxxxxxx
<meta charset="utf-8">
<style>
.links line {
stroke: #828282;
stroke-width: 2px;
}
.nodes circle {
pointer-events: all;
stroke-width: 40px;
}
.d3-tip {
line-height: 1;
padding: 6px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 4px;
font-size: 12px;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
/* Style northward tooltips specifically */
.d3-tip.n:after {
margin: -2px 0 0 0;
top: 100%;
left: 0;
}
div.tooltip {
position: absolute;
text-align: center;
width: 125px;
height: 90px;
padding: 2px;
font: 13px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
</style>
<body>
<script src="https://d3js.org/d3.v4.js"></script>
<script src="d3-tip.js"></script>
<script>
var radius = 7,
phase_height = 350,
gap = radius * 10,
num_phases = 11,
width = 0.9*screen.width,
height = num_phases * phase_height + (num_phases - 1) * gap
tgl_idx = -1
big_tgl = false
clk_tgl = false
linkedByIdx = {};
var color_scale = d3.scaleOrdinal(d3.schemeCategory20);
d3.select("body").append("h1")
.html("Post Auction Transition Dependencies")
d3.select("body").append("h2")
.html("Broadcast facilities are colored by phase assignment. <br/>" +
"Arrows are transition dependencies induced by current channel assignments, final channel assignments, as well as interference constraints. <br/>" +
"Dependencies between stations in the same phase indicate the stations are a linked set. <br/>" +
"Click a node to highlight its direct neighbors. <br/>" +
"Double click a node to highlight its entire daisy-chain.")
var svg = d3.select("body").append("svg")
.attr("id", "svg")
.attr("width", width)
.attr("height", height);
svg.append("defs").append("marker")
.attr("id", "arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 26)
.attr("refY", 0)
.attr("markerWidth", 4)
.attr("markerHeight", 4)
.attr("orient", "auto")
.style("fill", "#828282")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var tool_tip = d3.tip()
.attr("class", "d3-tip")
.offset([-2 * radius, 0])
.html(function(d) { return tooltip_txt(d); });
svg.call(tool_tip);
var simulation = d3.forceSimulation().alphaDecay(0.05)
.force("link", d3.forceLink().id(function(d) { return d.fac_id; }).distance(60))
.force("charge", d3.forceManyBody().strength(-15))
.force("collide",d3.forceCollide( function(d){return 3 * d.radius; }).iterations(3) )
.force("center", d3.forceCenter(width / 2, height / 2))
.force("horizontal", d3.forceX(width/2).strength(-0.01))
.force("vertical", d3.forceY(function(d) {return get_phase_y_center(d.phase);}).strength(0.2))
d3.json("graph.json", function(error, graph) {
if (error) throw error;
graph.nodes.forEach(function(node){node.radius = radius;
node.neighbors = [];});
var link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.attr("marker-end", "url(#arrow)");
var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", function(d){return d.radius; })
.attr("fill", function(d){return color_scale(d.phase);})
.call(d3.drag()
.on("start", drag_started)
.on("drag", dragged)
.on("end", drag_ended))
.on("click",clicked)
.on("dblclick",dbl_clicked)
.on("mouseover", function(d) {
var n = d3.select(this),
t = d3.transition().duration(250);
if (tgl_idx != d.index) enlarge_node(n,t);
// Only show tooltip for 'visible nodes'
if (n.style("opacity") > 0.1){
tool_tip.show(d);
}
})
.on("mouseout", function(d) {
var t = d3.transition().duration(250);
if (tgl_idx != d.index) shrink_node(d3.select(this),t);
tool_tip.hide(d);
});
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links)
.strength(function (l) {
var dp = Math.abs(l.source.phase - l.target.phase),
ret = 2;
if (dp > 0) ret = 1/(ret+dp);
return ret;});
link.style("opacity",function(d){return link_opacity(d);});
for (i = 0; i < graph.nodes.length; i++) {
linkedByIdx[graph.nodes[i].index + "," + graph.nodes[i].index] = 1;
};
graph.links.forEach(function(l) {
linkedByIdx[l.source.index + "," + l.target.index] = 1;
var source = l.source, target = l.target;
(source.neighbors || (source.neighbors = [])).push(target);
(target.neighbors || (target.neighbors = [])).push(source);
});
graph.nodes.forEach(traverse);
function ticked() {
node
.attr("cx", function(d) {return d.x = bound_x_pos(d.x); })
.attr("cy", function(d) {return d.y = bound_y_pos(d.y, d.phase); });
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
function clicked(d) {
if (!clk_tgl || (clk_tgl && d.index != tgl_idx)) {
// Shrink tgl_idx, enlarge d.index
var t = d3.transition(250);
if (d.index != tgl_idx) {
node.each(function (e) {
if (e.index == tgl_idx) {
shrink_node(d3.select(this),t);
}
})
}
//Reduce the opacity of all but the neighbouring nodes
node.style("opacity", function (o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
});
link.style("opacity", function (o) {
return d.index==o.source.index | d.index==o.target.index ? 1 : 0.1;
});
clk_tgl = true;
tgl_idx = d.index;
} else {
resetOpacity();
clk_tgl = false;
tgl_idx = -1;
}
}
function dbl_clicked(d) {
if (!clk_tgl || (clk_tgl && d.index != tgl_idx)) {
// Shrink tgl_idx, enlarge d.index
var t = d3.transition(250);
if (d.index != tgl_idx) {
node.each(function (e) {
if (e.index == tgl_idx) {
shrink_node(d3.select(this),t);
}
})
}
//Reduce the opacity of all but the neighbouring nodes
node.style("opacity", function (o) {
return o.group == d.group ? 1 : 0.1;
});
link.style("opacity", function (o) {
return d.group == o.source.group || d.group == o.target.group ? 1 : 0.1;
});
clk_tgl = true;
tgl_idx = d.index;
} else {
resetOpacity();
clk_tgl = false;
tgl_idx = -1;
}
}
function resetOpacity() {
node.style("opacity", 1);
link.style("opacity", function(d) {return link_opacity(d);});
}
});
function drag_started(d) {
if (!d3.event.active) simulation.alphaTarget(0.1).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function drag_ended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
function bound_x_pos(x_pos) {
return Math.max(3*radius, Math.min(x_pos, width - 3*radius));
}
function bound_y_pos(y_pos, phase) {
var top = radius + ((phase-1)*phase_height) + ((phase-1)*gap);
var bottom = (phase*phase_height) + ((phase-1)*gap) - radius;
return Math.max(top, Math.min(y_pos, bottom));
}
function get_phase_y_center(phase) {
var top = radius + ((phase-1)*phase_height) + ((phase-1)*gap);
var bottom = (phase*phase_height) + ((phase-1)*gap) - radius;
return top + ((bottom - top) / 2);
}
function link_opacity(l) {
return 1 - Math.abs(l.source.phase - l.target.phase)/num_phases;
}
//This function looks up whether a pair are neighbours
function neighboring(a, b) {
return linkedByIdx[a.index + "," + b.index];
}
function enlarge_node(n, trans) {
var d = n.node().__data__;
d.radius *= 2;
n.style("stroke-width", "3px")
.style("stroke","black")
.transition(trans)
.attr("r", function(e) {return e.radius; });
}
function shrink_node(n, trans) {
var d = n.node().__data__;
d.radius = radius;
n.style("stroke", "none")
.transition(trans)
.attr("r", function(e) {return e.radius; });
}
function tooltip_pos(el) {
var rect = document.getElementById("svg").getBoundingClientRect(),
x = parseInt(el.attr("cx")) + rect.left + 3 * radius,
y = parseInt(el.attr("cy")) + rect.top + 3 * radius;
return {"x" : x, "y" : y};
}
function tooltip_txt(d) {
var t_txt = "Facility ID: " + d.fac_id + "<br/>";
t_txt += "Phase: " + (d.phase-1) + "<br/>";
t_txt += "Callsign: " + d.callsign + "<br/>";
t_txt += "Current Channel: " + d.cur_chan + "<br/>";
t_txt += "Final Channel: " + d.fin_chan + "<br/>";
t_txt += "Country: " + d.country + "<br/>";
t_txt += "Market: " + d.dma;
return t_txt;
}
function traverse(n, group) {
if ("group" in n) {
n.group = Math.min(n.group, group);
} else {
n.group = group;
n.neighbors.forEach(function(d) { traverse(d, group); });
}
}
</script>
https://d3js.org/d3.v4.js