I am currently working on a project where i need to create a bowtie diagram from data coming from an application.
From a bit of research, the library that looks the best to acheive this is D3.js
I have played about with / looked at these examples:
Collapsible Tree: https://observablehq.com/#d3/collapsible-tree
Hierarchy Chart: https://bl.ocks.org/willzjc/a11626a31c65ba5d319fcf8b8870f281
Here is a basic bowtie diagram example that i would like to try and replicate:
As you can see in the image - i need multiple top level items and/or a double tree that can open each side/tree independently with data flowing from left to right (red side) and right to left (blue side) of the top level item.
Is this acheivable using D3.js?
Tutorials about d3.js only cover standard charts like bar charts and the tree examples online that i have found only have 1 top level item.
Any help, advice or pointers in the right direction would be greatly appreciated.
There's useful material to consider:
Tree drawing orientation - which links to a block showing that a left-to-right orientation can be achieved by altering use of x and y coordinates of the node
Similar question to yours - accepted answer says to draw two trees in two <g>s and shift one 'over'
Simple D3 v6 example - v6 is useful because it allows Array.from to get a list of the nodes from the d3.tree method without traversing the hierarchy.
The 2nd one isn't quite similar enough to your question so adapted the 3rd to use principles from the 1st:
You want two trees - one going left-to-right from the root and one going right-to-left from the root
The d3.tree method usefully computes the right xs and ys for the positioning of the nodes (except for 'horizontal' rendering you flip use of x and y in the code per the Mike Bostock block). We want to keep the understanding of the relative positions between nodes but do a translation on both RH tree (center in g) and LH tree (flip vertically left of center of g).
The exception being the root node e.g. where you have more nodes on the 'left' vs the 'right' the root will be in a slightly different position. You have to choose one root node for both trees.
You can recompute the coordinates such that:
in the right hand tree, the y coordinates (now meaning x) should have width / 2 added to shift them to the vertical center of the g and then halved to keep 'branch' lengths within the width.
in the left hand tree, the y coordinates (now meaning x) should be halved (same deal for 'branch lengths' in RH tree) and negated to flip the node positions to the left hand side of the root node (where you have opted to choose e.g. the LH tree's root position).
Here's the demo:
// useful links
// https://bl.ocks.org/mbostock/3184089
// https://bl.ocks.org/d3noob/72f43406bbe9e104e957f44713b8413c
var treeDataL = {
"name": "Root",
"children": [
{
"name": "L_A_1",
"children": [
{"name": "L_B_1"},
{"name": "L_B_2"}
]
},
{
"name": "L_A_2",
"children": [
{"name": "L_B_3"},
{"name": "L_B_4"}
]
}
]
}
var treeDataR = {
"name": "Root",
"children": [
{
"name": "R_A_1",
"children": [
{"name": "R_B_1"},
{"name": "R_B_2"},
{"name": "R_B_3"}
]
},
{
"name": "R_A_2",
"children": [
{"name": "R_B_3"},
{"name": "R_B_4"},
{"name": "R_B_5"},
{"name": "R_B_6"}
]
}
]
}
// set the dimensions and margins of the diagram
var margin = {top: 50, right: 50, bottom: 50, left: 50},
width = 400 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// declares a tree layout and assigns the size
var tree = d3.tree()
.size([width, height]);
// create 2x trees using d3 hierarchy
// this is where the computation of coordinates takes place
var nodesL = tree(d3.hierarchy(treeDataL));
var nodesR = tree(d3.hierarchy(treeDataR));
// get arrays of nodes - need v6
nodesLArray = Array.from(nodesL);
nodesRArray = Array.from(nodesR);
// switch out nodesR root for nodesL
// here the choice is to assign coords of root of LH tree to RH
nodesLRoot = nodesLArray.find(n => n.data.name == "Root");
nodesRRoot = nodesRArray.find(n => n.data.name == "Root");
nodesRRoot.x = nodesLRoot.x;
nodesRRoot.y = nodesLRoot.y;
// this is kinda like the 'important bit'
// REMEMBER for horizontal layout, flip x and y...
// LH: halve and negate all y's in nodesL add width / 2
nodesLArray.forEach(n => n.y = ((n.y * 0.5) * -1) + width / 2);
// RH: halve and add width / 2 to all y's nodesR
nodesRArray.forEach(n => n.y = (n.y * 0.5) + width / 2);
// now sticking a bit more closely to the tutorial in link 3
// append svg
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
// align g with margin
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// render both trees
[nodesL, nodesR].forEach(function(nodes, i) {
// adds the links between the nodes
// need to select links based on index to prevent bad rendering
var link = g.selectAll(`links${i}`)
.data( nodes.descendants().slice(1))
.enter()
.append("path")
.attr("class", `link links${i}`) // note two classes
.attr("d", function(d) {
// x and y flipped here to achieve horizontal placement
return `M${d.y},${d.x}C${d.y},${(d.x + d.parent.x) / 2} ${d.parent.y},${(d.x + d.parent.x) / 2} ${d.parent.y},${d.parent.x}`
});
// adds each node as a group
// need to select nodes based on index to prevent bad rendering
var node = g.selectAll(`.nodes${i}`)
.data(nodes.descendants())
.enter()
.append("g")
.attr("class", `node nodes${i}`) // note two classes
.attr("transform", function(d) {
// x and y flipped here to achieve horizontal placement
return `translate(${d.y},${d.x})`;
});
// adds the circle to the node
node.append("circle")
.attr("r", 10);
// adds the text to the node
node.append("text")
.attr("dy", ".35em")
.attr("y", function(d) { return d.children ? -20 : 20; })
.style("text-anchor", "middle")
.text(function(d) { return d.data.name; });
});
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 10px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<script src="https://d3js.org/d3.v6.min.js"></script>
The demo is basic and you might need to look into tree.nodeSize to achieve node placements accommodating boxes containing text etc. I think the principle of updating the y (being x) coordinates still applies to flip the LH tree around.
Related
I am currently working on a visualization in d3.js with the goal to visualize data similarity. I wish to compare my data within circles by creating two semi-circles for each node and putting the comparison data within these semicircles. My data consists of strings (each semicircle receives a single sentence).
My current approach is as follows:
First, I create my necessary node data using the pack-layout.
var bubble = d3.pack().size([SVG_WIDTH,SVG_HEIGHT]).padding(CIRCLE_PADDING),
root = d3.hierarchy({children: COMPARISON_DATA}).sum(function(d){ return d.children ? 0 : d[2]});
var nodeData = bubble(root).children;
d[2] is the maximum string length of the two sentences that are being put into the semicircles and thus decides the radius of the circles.
Next, I iterate over each node and create the corresponding semicircles. I have removed all the code-parts which are irrelevant to my question.
nodeData.forEach(function (data, index) {
//upperCircleGroup simply adds a small y-translate, so that the semicircles have a margin
var gUpper = upperCircleGroup.append("g");
var gLower = lowerCircleGroup.append("g");
var lowerCircle = gLower.append('path')
.attr('d', d3.arc()({
innerRadius: 0,
outerRadius: data.r,
startAngle: Math.PI / 2,
endAngle: 3 / 2 * Math.PI
}))
.attr('transform', `translate(${data.x},${data.y})`)
var upperCircle = gUpper.append('path')
.attr('d', d3.arc()({
innerRadius: 0,
outerRadius: data.r,
startAngle: 1 / 2 * Math.PI,
endAngle: - 1 / 2 * Math.PI
}))
.attr('transform', `translate(${data.x},${data.y})`)
var upperText = gUpper
.append("foreignObject")
.attr("width", () => {return data.r*Math.sqrt(2)})
.attr("height", () => {return data.r*(Math.sqrt(2)/2)})
.attr('transform', `translate(${data.x - (data.r / Math.sqrt(2))},${data.y - (data.r/Math.sqrt(2)) })`)
.text(() => {return data.data[0]})
var lowerText = gLower
.append("foreignObject")
.attr("width", () => {return data.r*Math.sqrt(2)})
.attr("height", () => {return data.r*(Math.sqrt(2)/2)})
.attr('transform', `translate(${data.x - (data.r / Math.sqrt(2))},${data.y })`)
.text(() => {return data.data[1]})
});
As you can see, I draw my semicircles using d3's arc. Now this is where my question arises. I've had trouble putting my textual content inside the arc, so after searching for a while I chose this solution to put a div inside my semicircles which then receives the text. The sqrt(2) operations are used to fit the square into the semicircle.
My problem with this solution is, that at times, the sentence simply won't fit into the div and some content is lost. Is there a way to calculate the font-size of a string necessary, so that it fits the div of a given size? If this were possible, I could simply calculate the appropriate font-size and add a zoom option to the visualization. Also, if there are better ways to achieve what I am trying to do I would also be happy to get some feedback from you guys as I am a complete beginner when it comes to using d3.
Making text responsive to an element is difficult but CSS-Tricks have made a great article about different ways to approach it...
https://css-tricks.com/fitting-text-to-a-container/
I need to display a D3 map with a topological / shaded relief background. All user functionalities need to be implemented (e.g. zoom and panning)
So far, I have layered the map over a PNG that has the topology. I then did some hacking around with the projection to align the PNG border with the map borders. I then allow the user to zoom the PNG (eg: http://bl.ocks.org/pbogden/7363519). The result is actually very good. When I pan and zoom the map moves with the PNG which is great (image below):
The problem is that the PNG is very heavy (20MB), and the whole resulting experience is seriously buggy to the point that is is unusable. Results are obviously use a lower resolution image, but then the topology looks crap when the user zooms in. I tried converting the PNG to JPG ... which was actually worse!
What would be the best solution to achieve my goal in D3? Initial thoughts are as follows:
(1) The d3.geo.tile plugin (http://bl.ocks.org/mbostock/4132797). The difficulty here is that I would need to create my own tiles from my PNG image. Is this a promising avenue? Would I be able to layer a D3 map on top of that? I cannot find an example with custom tiles.
(2) I've seen this successful implementation of OpenSeaDragon and D3 (http://bl.ocks.org/zloysmiertniy/0ab009ca832e7e0518e585bfa9a7ad59). The issue here is that I am not sure whether it'll be possible to implement the desired D3 functionalities (zoom, pan, transitions) such that the D3 map and the underlying image move simultaneously.
(3) Any other thoughts or ideas?
To turn an image into tiles you'll need to have a georeferenced image - or be able to georeference the image yourself. As I believe you are using a natural earth dataset to create this image, you could use the source tif file and work with this. I use tile mill generally for my tiles (with some python) and it is fairly straightforward. You would not be able to use your png as is for tiles.
However, creating at tile set is unnecessary if you are looking for a hillshade or some sort of elevation/terrain texture indication. Using a leaflet example here, you can find quite a few tile providers, the ESRI.WorldShadedRelieve looks likes it fits the bill. Here's a demo with it pulled into d3 with a topojson feature drawn ontop:
var pi = Math.PI,
tau = 2 * pi;
var width = 960;
height = 500;
// Initialize the projection to fit the world in a 1×1 square centered at the origin.
var projection = d3.geoMercator()
.scale(1 / tau)
.translate([0, 0]);
var path = d3.geoPath()
.projection(projection);
var tile = d3.tile()
.size([width, height]);
var zoom = d3.zoom()
.scaleExtent([1 << 11, 1 << 14])
.on("zoom", zoomed);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var vector = svg.append("g");
// Compute the projected initial center.
var center = projection([-98.5, 39.5]);
d3.json("https://unpkg.com/world-atlas#1/world/110m.json",function(error,data) {
vector.append("path")
.datum(topojson.feature(data,data.objects.land))
.attr("stroke","black")
.attr("stroke-width",2)
.attr("fill","none")
.attr("d",path)
// Apply a zoom transform equivalent to projection.{scale,translate,center}.
svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(1 << 12)
.translate(-center[0], -center[1]));
})
function zoomed() {
var transform = d3.event.transform;
var tiles = tile
.scale(transform.k)
.translate([transform.x, transform.y])
();
projection
.scale(transform.k / tau)
.translate([transform.x, transform.y]);
var image = raster
.attr("transform", stringify(tiles.scale, tiles.translate))
.selectAll("image")
.data(tiles, function(d) {
return d;
});
image.exit().remove();
// enter:
var entered = image.enter().append("image");
// update:
image = entered.merge(image)
.attr('xlink:href', function(d) {
return 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/' + d.z + '/' + d.y + '/' + d.x + '.png';
})
.attr('x', function(d) {
return d.x * 256;
})
.attr('y', function(d) {
return d.y * 256;
})
.attr("width", 256)
.attr("height", 256);
vector.selectAll("path")
.attr("transform", "translate(" + [transform.x, transform.y] + ")scale(" + transform.k + ")")
.style("stroke-width", 1 / transform.k);
}
function stringify(scale, translate) {
var k = scale / 256,
r = scale % 1 ? Number : Math.round;
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
body { margin: 0; }
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3-tile#0.0.4/build/d3-tile.js"></script>
<script src="https://unpkg.com/topojson-client#3"></script>
You could certainly use OpenSeadragon for this. You'd want to turn the image into tiles; you don't need a specialized server for it... there are a number of standalone scripts you can use:
http://openseadragon.github.io/examples/creating-zooming-images/
Once you have that, OpenSeadragon handles the zooming and panning for you.
To overlay SVG so that it matches the zooming and panning, use the SVG overlay plugin:
https://github.com/openseadragon/svg-overlay
It works great with SVG produced by D3.
One thing to be aware of is that OpenSeadragon does not have any geo-specific functionality, so you'll position the overlay in image pixels rather than latitude/longitude.
BTW, OpenSeadragon can also work with non-tiled images, so if you want to give it a test before tiling your image, that's no problem. You'll just want to tile your image before production so you're not sending 20mb to your users.
I have three shapes all in the same group. This group have been transformed. I want to draw a line from one of the elements within that group. I am trying to access this elements coordinates by:
s.select("#hitboxHeel").getBBox().cx and s.select("#hitboxHeel").getBBox().cy
However this gives some weird coordinates, that are far off from where they should be. How do i get the actual position of the points, thus being able to draw a line?
I had a similar problem and found the solution from this post : Rectangle coordinates after transform
Here, you basically want to apply the 'aggregated' transform matrix of your shape to coordinates that are not transformed (sorry for the awkward phrasing). This matrix also incorporates the transformations of parent elements (group nodes here), so you shouldn't have to worry about them.
So, given :
your native svg element node
your native svg container svg
your original point of interest coordinates (before transforms) x and y that you want transformed
the expected transformed coordinates of your original point transformedPoint
`
// get the component transform matrix
var ctm = node.getCTM();
var svgPoint = svg.createSVGPoint();
svgPoint.x = x;
svgPoint.y = y;
// apply the matrix to the point
var transformedPoint = svgPoint.matrixTransform(ctm);
// an example using d3.js ( svg > g > rect )
// get the center of the rectangle after tansformations occured
var svg = d3.select('body').append('svg')
.attr('width', 500)
.attr('height', 500)
.attr('id', 'myCanvas')
.style('margin', 100)
var g = svg.append('g')
.attr('transform', 'translate(-10,10)')
var r = g.append('rect')
.attr('x', 300).attr('y', 100).attr('width', 79).attr('height', 150)
.attr('transform', 'translate(-54,300)rotate(-30,30,20)')
.attr('stroke', 'black')
.attr('fill', 'red')
var pt = svg.node().createSVGPoint()
pt.x = parseInt(r.attr('x')) + parseInt(r.attr('width')) / 2
pt.y = parseInt(r.attr('y')) + parseInt(r.attr('height')) / 2
var ctm = r.node().getCTM()
var center = pt.matrixTransform(ctm)
console.log('the transformed rectangle center', center)
// draw the center to confirm the accuracy of the process
svg.append('circle')
.attr('cx', center.x).attr('cy', center.y).attr('r', 5)
.attr('stroke', 'black')
.attr('fill', 'blue')
`
I am building a chart using D3.js, which shows some info about employee's competencies.
screenshot:
As you can see, some text is larger than container element size, because of that, part of the text, is cut. I want to wrap these texts inside container.
I found this example , but I was not able to apply some solution to my chart.
Help would be appreciated...
Here is charts codepen url
and here is full screen view
p.s. I need text to be wrapped by words
In order to wrap the labels, you need to adjust Mike's solution to deal with textPath elements.
For this, we need several things:
1. Get the available width, reaching which the labels should wrap
You could compute the length of the arc itself, but I've done this by computing the segment created by the endpoints of your invisible paths that your labels follow. This will provide us with a little side margin as well, as the segment's length is shorter than the arc's length.
The distance between two points is computed as follows:
d = sqrt((x2 - x1)^2 + (y2 - y1)^2)
2. Wrap the labels when they rich available width and keep the aligned to center
For managing this one, I had to dig into the SVG documentation on the textPath element to see how it can be wrapped and shifted along the y axis.
Initially, I tried setting several textPath elements within one text label, but I couldn't manage to shift them along the y axis. It turns out, that for this you need to add tspan elements within textPath elements. But here another problem arose - I couldn't manage to keep them centrally aligned.
In the end, to achieve shift along y axis and central alignment, you need to use one textPath element (for horizontal alignment) with one tspan element inside (for vertical alignment).
3. Wrap the labels by letters, not by words
This is the point that I have assumed that you'll need namely letter wrapping (at the moment of writing, I didn't get the answer from OP), because on small sizes of your chart, there are words too long to fit into one line.
This was the easiest problem to solve. Just adjust the splitting and joining operations to switch from words to letters:
letters = text.text().split('').reverse(); // instead of .split(/\s+/)
...
tspan.text(line.join("")); // instead of .join(" ")
And here's the whole code that was changed, with relevant comments:
outerSvg.selectAll(".outerCircleText")
.data(pie(behaviorsDatasetOuterCircle))
.enter().append("text")
.attr("class", "outerCircleText")
//Move the labels below the arcs for those slices with an end angle greater than 90 degrees
.attr("dy", function (d, i) {
d.i = i;
return (d.startAngle >= 90 * Math.PI / 180 ? 18 : -11);
})
.text(function(d) { return d.data.name; })
.call(wrap); // Do not add `textPath` elements here. Instead, add them in the `wrap` function
function wrap(text) {
text.each(function() {
var text = d3.select(this),
letters = text.text().split('').reverse(),
letter,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
textPath = text.text(null).append("textPath") // Add a textPath element
.attr("startOffset", '50%')
.style("text-anchor", "middle")
.attr("xlink:href", function(d) { return "#outerArc" + d.i; }),
tspan = textPath.append('tspan'), // Inslide textPath, add a tspan element, for offset feature later.
path = d3.select(text.select('textPath').attr('xlink:href')); // Get the path to compute width of text later.
var startLoc = /M(.*?)A/;
var newStart = path.attr('d').match(startLoc)[1];
var newEnd = path.attr('d').indexOf(' 0 0 0 ') > -1
? path.attr('d').split(' 0 0 0 ')[1]
: path.attr('d').split(' 0 0 1 ')[1] ;
// Compute the start/end coordinate points of the arc that the text will follow.
var x1 = parseFloat(newStart.split(' ')[0]),
y1 = parseFloat(newStart.split(' ')[1]),
x2 = parseFloat(newEnd.split(' ')[0]),
y2 = parseFloat(newEnd.split(' ')[1]);
// Compute the length of the segment between the arc start/end points. This will be the
// width which the labels should wrap when reaching it.
var width = Math.sqrt(Math.pow((x2 - x1), 2) + Math.pow((y2 - y1), 2));
// And then we go on (with slight changes) with the example from Mike Bostock
// from here https://bl.ocks.org/mbostock/7555321
while (letter = letters.pop()) {
line.push(letter);
tspan.text(line.join(""));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(""));
line = [letter];
// Instead of adding only a tspan element, add a new textPath so that the wrapped
// letters will be aligned to center. Without it, the letters will start drawing
// from right with part of them invisible, like if the labels are not wrapped.
textPath = text.append("textPath")
.attr("startOffset", '50%')
.style("text-anchor", "middle")
.attr("xlink:href", function(d) { return "#outerArc" + d.i; }),
// Add a tspan element to offset the wrapped letters from the previous line
tspan = textPath.append("tspan")
.attr('dy', '1em')
.attr('text-anchor', 'middle')
.text(letter);
}
}
});
}
In the end, it was an interesting challenge. Here is a fork of your codepen with a working example (the changes are starting with line 749).
The codepen has only the outer labels wrapped. I have left the inner labels for you to implement the approach described here. Good luck with that!
I have written the following code designed to display a d3js tree layout but encountered some difficulty when trying to get the generated svg to resize according to its aspect ratio. I was able (in the attached demo) to get the svg to scale the way I had desired but the code I have written is constrained by const ASPECT_RATIO, as seen here:
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (window.innerWidth * ASPECT_RATIO));
and again, further down, here:
layout.size([(window.innerWidth * ASPECT_RATIO), window.innerWidth - 128]);
Is there a way to circumvent this? I would prefer not having to change this value by hand every time the aspect ratio of the svg changes (that is, any time new content is added).
Regards,
Brian
tl;dr: Help me eliminate const ASPECT_RATIO.
Code:
/// <reference path="d3.d.ts" />
"use strict";
/* (c) brianjenkins94 | brianjenkins94.me | MIT licensed */
// Get JSON, impose synchronicity
d3.json("js/data.json", function(error, treeData) {
if (!error) {
// Instantiate canvas
var canvas = d3.select("#canvas");
// Aspect ratio nonsense
const ASPECT_RATIO = 1.89260808926;
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (window.innerWidth * ASPECT_RATIO));
canvas.attr("preserveAspectRatio", "xMinYMin slice");
// Update
update();
function update() {
// Add an SVG group element
canvas.append("g");
// Instantiate group
var group = canvas.select("g");
// Translate group right
group.attr("transform", "translate(64, 0)");
// Instantiate layout tree
var layout = d3.layout.tree();
// Initialize layout dimensions
layout.size([(window.innerWidth * ASPECT_RATIO), window.innerWidth - 128]);
// Instantiate rotation diagonal
var diagonal = d3.svg.diagonal();
// Rotate projection 90 degrees about the diagonal
diagonal.projection(function(d) { return [d.y, d.x]; });
// Initialize node array
var nodes = layout.nodes(treeData);
// Initialize link array
var links = layout.links(nodes);
// Select all paths in group
group.selectAll("path")
// For each link, create a path
.data(links).enter().append("path")
// Provide the specific diagonal
.attr("d", diagonal);
// Select all groups in group
var node = group.selectAll("g")
// For each node, create a group
.data(nodes).enter().append("g")
// Translate accordingly
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
// Add a circle at every node
node.append("circle")
.attr("r", 3);
// Add label
node.append("text")
// To the left if the node has children, otherwise right
.attr("dx", function(d) { return d.children ? -8 : 8; })
.attr("dy", 0)
// Branch if the node has children
.attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
.text(function(d) { return d.name; });
}
} else {
console.log("There was a connection error of some sort.");
}
});
Demo:
https://rawgit.com/brianjenkins94/html5tree/master/index.html
Here's what I learned:
While an svg can be "dimensionless" it can't be "aspect-ratio-less." There must be some core aspect ratio to govern the resize.
The tree.size() and tree.nodeSize() functions create an "arbitrary coordinate system" that "is not limited screen coordinates."
What the aspect ratio is, is up to the developer and can be expressed as "arbitrary coordinates" or, in accordance with responsive design, can be expressed as relativistic measurements.
My solution is as follows:
// Viewbox & preserveAspectRatio
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (2 * window.innerWidth));
canvas.attr("preserveAspectRatio", "xMinYMin slice");
...
// Initialize layout dimensions
layout.size([(2 * window.innerWidth), (window.innerWidth - 128)]);
Thus eliminating the dependence on const ASPECT_RATIO in favor of relative measurements based off of browser dimensions.
This can potentially (and almost certainly will) cause rendering inconsistency across multiple viewports, but can be handled accordingly by querying the viewport prior to rendering and employing cosmetic adjustments.