D3: Overlay Svg and Canvas: geo Projection - javascript

I am plotting a map of NYC (interactive svg) and need to overlay it with the canvas with thousands of points. Using this tutorial I was able to achieve almost what I want: there is an overlay and points are visible.
However, despite using the same projection and having canvas within svg and with the same width and height, points are offset from the svg borders by some specific amount:
This offset remains even through resize, and I can't get where is an error.
Here is the snippet of my code:
var projection = d3.geoMercator()
.center([-73.98, 40.72])
.scale(width * 74)
.translate([(width) / 2, (height) / 2]);
...
var svg = d3.select("#svg-container")
.append("svg")
.attr("class", "map");
// CANVAS PART
var foreignObject = svg.append("foreignObject")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("class", "overlay");
// add embedded body to foreign object
var foBody = foreignObject.append("xhtml:body")
.style("margin", "0px")
.style("padding", "0px")
.style("background-color", "none")
.style("width", width + "px")
.style("height", height + "px");
// add embedded canvas to embedded body
var canvas = foBody.append("canvas")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
// Points
context.clearRect(0, 0, width, height);
context.globalAlpha = .8;
userpoints.forEach(function(d, i) {
var crd = projection([d.lat, d.lon]);
// console.log(crd, );
context.beginPath();
context.rect(crd[0], crd[1], 1, 1);
context.fillStyle=comm_colors[d.Community];
context.closePath();
context.fill();
Here is the corresponding part of the resulting DOM:
<svg class="map">
<foreignObject x="0" y="0" width="1036.962" height="1010" class="overlay" style="visibility: visible;">
<body style="margin: 0px; padding: 0px; width: 1036.96px; height: 1010px;">
<canvas x="0" y="0" width="1036.962" height="1010"></canvas>
</body>
</foreignObject>
<g id="boros"></g>
<g id="cts">
<path> ...
</path>
</g>
<defs>
<pattern id="lzssi" patternUnits="userSpaceOnUse" width="3" height="3"><rect width="3" height="3" fill="grey"></rect><path d="M 0,3 l 3,-3 M -0.75,0.75 l 1.5,-1.5
M 2.25,3.75 l 1.5,-1.5" stroke-width="0.5" shape-rendering="auto" stroke="white" stroke-linecap="square"></path>
</pattern>
</defs>
</svg>

Related

Clip path is moving with group of elements when using d3.drag

I'm trying to drag a group of shapes on a clipped path. For the first time, It works fine, but as soon as I started dragging, clipping does not work at all.
Here is my working code;
var svg = d3.select("svg");
// draw a circle
svg.append("clipPath") // define a clip path
.attr("id", "clip") // give the clipPath an ID
.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "red")
var g = svg.append("g")
.datum({x:0, y:0})
.attr("transform", function(d) { return 'translate(' + d.x + ' '+ d.y + ')'; })
.attr("clip-path","url(#clip)")
.call(d3.drag()
.on("start", function(d){
d3.select(this).raise().classed("active", true);
})
.on("drag", function(d){
d3.select(this).attr("transform","translate(" + (d3.event.x) + "," + (d3.event.y) + ")" );
})
.on("end", function(d){
d3.select(this).classed("active", false);
}));
g.append("rect")
.attr("x",100)
.attr("y",80)
.attr("height",100)
.attr("width",200)
g.append("line")
.attr("x1", 100)
.attr("y1", 80)
.attr("x2", 200)
.attr("y2", 80)
.style("stroke", "purple")
.style("stroke-width", 12)
.svgClass{
border:2px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="500" height="300" class="svgClass"></svg>
You can see on dragging, first time clipped shape is moving all the way. No further clipping is there.
To make it easy, I redraw the outer circle again. Check this code;
var svg = d3.select("svg");
// draw a circle
svg.append("clipPath") // define a clip path
.attr("id", "clip") // give the clipPath an ID
.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "red")
// redraw circle to make it easy
svg.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "red")
var g = svg.append("g")
.datum({x:0, y:0})
.attr("transform", function(d) { return 'translate(' + d.x + ' '+ d.y + ')'; })
.attr("clip-path","url(#clip)")
.call(d3.drag()
.on("start", function(d){
d3.select(this).raise().classed("active", true);
})
.on("drag", function(d){
d3.select(this).attr("transform","translate(" + (d3.event.x) + "," + (d3.event.y) + ")" );
})
.on("end", function(d){
d3.select(this).classed("active", false);
}));
g.append("rect")
.attr("x",100)
.attr("y",80)
.attr("height",100)
.attr("width",200)
g.append("line")
.attr("x1", 100)
.attr("y1", 80)
.attr("x2", 200)
.attr("y2", 80)
.style("stroke", "purple")
.style("stroke-width", 12)
.svgClass{
border:2px solid red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.5.0/d3.min.js"></script>
<svg width="500" height="300" class="svgClass"></svg>
Here You can see clipping is not working at all. I want to bound this dragging within circle and if moves out of clipping boundaries, it should clip it accordingly.
Can anyone help me out with this requirement? Or let me know where I'm
doing it wrong.
The drag callback is transforming the same g element that the clip path has been applied to. This means that the g element's clip path is also being transformed, which is why the clipped shape is moving around as you drag your shape.
The snippet below uses a grey rectangle to show the clip path definition, and a pink rectangle to show the area of the transformed g element. The circle is retaining the original clip shape because the g element's clip path is being translated along with the rest of the element.
<svg width="300" height="300">
<clipPath id="cut">
<rect width="100" height="100" x="100" y="50"></rect>
</clipPath>
<rect x="100" y="50" width="100" height="100" fill="#eee"></rect>
<g clip-path="url(#cut)" transform="translate(50, 50)">
<rect x="100" y="50" width="100" height="100" fill="pink"></rect>
<circle
class="consumption"
cx="100"
cy="100"
r="50">
</circle>
</g>
</svg>
In the snippet below, a clip path is applied to an outer g element (which is not translated and has the same co-ordinates as the original clip path definition), while the transformation is applied to an inner g element.
<svg width="300" height="300">
<clipPath id="cut">
<rect width="100" height="100" x="100" y="50"></rect>
</clipPath>
<rect x="100" y="50" width="100" height="100" fill="#eee"></rect>
<g clip-path="url(#cut)">
<rect x="100" y="50" width="100" height="100" fill="pink"></rect>
<g transform="translate(100, 50)">
<circle
class="consumption"
cx="100"
cy="100"
r="50">
</circle>
</g>
</g>
</svg>
So, as shown in the example you should apply the clip path to an outer g element, while transforming an inner g element.

Add small rectangles inside BIG rectangles with unique Ids and links in D3? after an specific ID

In my previous question I asked how could I generate unique IDs for generated rectangles here
Now, I've been trying to draw small rectangles inside those rectangles. However, id="rectup0" can only have 4 small rect with unique xlink:href attribute
the rest of them can have 2 rectangles inside.
<body>
<div id="chart">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.3.0/d3.js"></script>
<script type="text/javascript">
var width = 1000,
height = 250,
margin = 7,
nRect = 11,
rectWidth = (width - (nRect - 1) * margin) / nRect,
svgContainer = d3.select('#chart').append('svg')
.attr('width', width)
.attr('height', height),
svgWalkway = d3.select('#chart').append('svg')
.attr('width', '1000')
.attr('height', '150'),
svgContainer2 = d3.select('#chart').append('svg')
.attr('width', width)
.attr('height', height);
// Rectangles scaling
var firstRow = d3.range(nRect),
posxScale = d3.scaleLinear()
.domain(d3.extent(firstRow))
.range([0, width - rectWidth]);
var middleRow = d3.range(1),
posxScale3 = d3.scaleLinear()
.domain(d3.extent(middleRow))
.range([0, width - rectWidth]);
var secondRow = d3.range(nRect),
posxScale2 = d3.scaleLinear()
.domain(d3.extent(secondRow))
.range([0, width - rectWidth]);
//Drawing the actual objects HTML rects
svgContainer.selectAll('rect.up')
.data(firstRow)
.enter()
.append('rect')
.attr("id", function(d,i){ return "rectup" + i })
.attr('x', posxScale)
.attr('width', rectWidth)
.attr('height', height)
.attr('class','up');
svgWalkway.selectAll('rect.middle')
.data(middleRow)
.enter()
.append('rect')
.attr('x', posxScale3)
.attr('width','1000')
.attr('height','150')
.attr('class', 'middle');
svgContainer2.selectAll('rect.down')
.data(secondRow)
.enter()
.append('rect')
.attr('id', function (d,i) { return "rectdown" + i })
.attr('x', posxScale2)
.attr('width', rectWidth)
.attr('height', height)
.attr('class', 'down')
.attr('y', 0);
</script>
</div>
</body>
I've tried drawing similar rectangles like the previous ones (using .append()) but so far that just put them next to svgContainer. I've been reading some posts regarding using grouping, tried it but didn't work. Thanks in advance
UPDATE
so basically, the whole question can be resumed as adding a rect tag right after rect id="rectup0" so I can have
<rect id="rectup0"></rect>
<rect id="redrect" x="0" width="20" height="40"></rect>
I know JQuery lets you add class attributes to other classes, but not sure about D3 , this will be something like adding a new rect tag programmatically. is that even possible?
UPDATE 2
I guess it is possible with d3.insert, with the following:
d3.select('#rectup0').insert('rect')
.attr('id', 'redrect')
.attr('x', 0)
.attr('width', '20')
.attr('height', '40');
however it gets it in the middle of the beginning and closing rect tag like this
<rect id="rectup0">
<rect id="redrect" x="0" width="20" height="40"></rect>
</rect>
Needless to say this doesn't work because of the closing rect tag
You can do the trick with each wrapper for selected data. It accepts 3 arguments: current data d, current index i, and whole array of data arr. So if you need some kind of checks, should you append the small rect, or not, you can create a helper function:
var testIndex = function(d, i, arr) {
return
// test index
i == 5 ||
// test current data
d == 10 ||
// test other array members, which would be an EnterNode here
(i > 0 && arr[i - 1]['__data__'] == 7);
}
// do checks in one line:
var testIndex = function (d, i, arr) { return i == 4 || i == 10 || i == 7; }
var data = svgContainer.selectAll('rect.up')
.data(firstRow);
data
.enter()
.each(function (d, i, arr) {
// select DOM element to add the rects
t = d3.select(this);
t
.append('rect')
// note that here we use empty the parameters list
.attr('id', 'rectup' + i)
.attr('x', posxScale)
.attr('width', rectWidth)
.attr('height', height)
.attr('class','up');
if (!testIndex(d, i, arr)) {
return;
}
t
.append('rect')
.attr('id', 'redrect' + i)
// note that we need to define x-coord similar to first rect
.attr('x', posxScale)
.attr('class', 'redrect');
});
// remove your data before redraw
data
.exit()
.remove();
Output:
<svg width="1000" height="250">
<rect id="rectup0" x="0" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup1" x="91.54545454545456" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup2" x="183.09090909090912" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup3" x="274.6363636363636" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup4" x="366.18181818181824" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup5" x="457.72727272727275" width="84.54545454545455" height="250" class="up"></rect>
<rect id="redrect5" x="457.72727272727275" class="redrect"></rect>
<rect id="rectup6" x="549.2727272727273" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup7" x="640.8181818181818" width="84.54545454545455" height="250" class="up"></rect>
<rect id="redrect7" x="732.3636363636365" class="redrect"></rect>
<rect id="rectup8" x="732.3636363636365" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup9" x="823.909090909091" width="84.54545454545455" height="250" class="up"></rect>
<rect id="rectup10" x="915.4545454545455" width="84.54545454545455" height="250" class="up"></rect>
<rect id="redrect10" x="915.4545454545455" class="redrect"></rect>
</svg>
The downside of such method is that you can't provide a attr parameter as JSON:
// wouldn't work
.attr({
id: 'redrect',
x: posxScale,
width: '20',
height: '40'
});

Zooming on specific element with d3js

I recently just found d3js and this block of example code when looking for a good way to zoom on elements: the example. I'm trying to use the same method of zooming, but instead of using json to populate the svg and body, I want to just zoom on specific html elements.I'm new to using d3 so I'm not sure what exactly I'm doing wrong, but heres my code so far:
JS:
window.onload=function(){
var width = 400,
height = 400,
centered;
var projection = d3.geo.albersUsa()
.scale(1070)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.on("click", clicked);
var circle = svg.append("circle")
.attr("class", "logo")
.attr("cx", 225)
.attr("cy", 225)
.attr("r", 20)
.style("fill", "transparent")
.style("stroke", "black")
.style("stroke-width", 0.25)
.attr("d", path)
.on("click", clicked)
.on("mouseout", function(){
d3.select(this)
.style("fill", "transparent");
});
function clicked(d) {
console.log("clicked");
var x, y, k;
if (d && centered !== d) {
var centroid = path.centroid(d);
x = centroid[0];
y = centroid[1];
k = 4;
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
circle.selectAll("path")
.classed("active", centered && function(d) { return d === centered; });
circle.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
.style("stroke-width", 1.5 / k + "px");
}
}
HTML:
<script src="d3.min.js"></script>
<script src="index.js"></script>
</head>
<body>
<svg id="mySvg" width="80" height="80">
<defs id="mdef">
<pattern id="image" x="0" y="0" height="40" width="40">
<image x="0" y="0" width="40" height="40" xlink:href="image1.png"></image>
</pattern>
</defs>
</body>
Here is a fiddle of what I have so far. As you can see it calls the click function and styles the stroke width, but it doesn't zoom on the object.

Customize tick transform attribute

Horizontal axis, datetimes
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom')
.ticks(d3.time.hours, 3)
.tickSize(-200)
Range on scale(x) is [0, 100]
Output SVG XML
<g class="xAxis" transform="translate(0, 182)">
<g style="opacity: 1;" transform="translate(0,0)">
<line class="tick" y2="-200" x2="0"></line>
<text y="3" x="0" dy=".71em" style="text-anchor: middle;">Tue 15</text>
</g>
<g style="opacity: 1;" transform="translate(12.5,0)">
<line class="tick" y2="-200" x2="0"></line>
<text y="3" x="0" dy=".71em" style="text-anchor: middle;">03 AM</text>
</g>
<g style="opacity: 1;" transform="translate(25,0)">
<line class="tick" y2="-200" x2="0"></line>
<text y="3" x="0" dy=".71em" style="text-anchor: middle;">06 AM</text>
</g>
...
Just don't know how to get inside that transform attribute and use percentages for the translations instead => (0%, 12.5%, 25%, so on...)
Complete code
var width = 960;
var height = 200;
var container = d3.select(".timeTable")
var svg = container.append("svg")
.attr("width", "100%")
.attr("height", height)
var roomID = container.attr("data-room")
function draw(times) {
// domain
var floor = d3.time.day.floor(d3.min(times, function (d) { return new Date(d.from); }));
var ceil = d3.time.day.ceil(d3.max(times, function (d) { return new Date(d.until); }));
// define linear time scale
var x = d3.time.scale()
.domain([floor, ceil])
.range([0, 100])
// define x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom')
.ticks(d3.time.hours, 3)
.tickSize(-200)
// draw time bars
svg.selectAll("rect")
.data(times)
.enter().append("rect")
.attr("class", "timeRange")
.attr("width", function (d, i) { return x(new Date(d.until)) - x(new Date(d.from)) + "%" })
.attr("height", "10px")
.attr("x", function (d, i) { return x(new Date(d.from)) + "%" })
.attr("y", function (d, i) { return i * 11 })
// draw x axis
svg.append("g")
.attr("class", "xAxis")
.attr("transform", "translate(0, " + (height - 18) + ")")
.call(xAxis)
}
d3.json("/time/get/" + roomID, draw);

Add a smiley face to a SVG circle

I want to use d3 to add smiley (or frowny) faces to an existing SVG, containing many circle elements.
So far, I have been able to achieve this by appending elements directly to the SVG root. It works, but only because their coordinates happen to be set in the correct way.
I would like to extend it to be able to add a smiley face to any number of circles, wherever they are.
I have tried selecting circles, and appending to them, but it does not work.
Here is what I have achieved so far:
let svg = d3.select("#mySvg");
let appendedTo = svg;
//let appendedTo = svg.select(".mainCircle");
appendedTo.append("circle")
.attr("cx",13)
.attr("cy",15)
.attr("r",5);
appendedTo.append("circle")
.attr("cx",37)
.attr("cy",15)
.attr("r",5);
var arc = d3.svg.arc()
.innerRadius(10)
.outerRadius(11)
.startAngle(3*(Math.PI/2)) //converting from degs to radians
.endAngle(5 * (Math.PI/2)) //just radians
appendedTo.append("path")
.attr("d", arc)
.attr("transform", "translate(25,40)");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width = 50 height = 50 id="mySvg">
<circle class="mainCircle" cx=25 cy=25 r=25 fill="red"></circle>
</svg>
The issue is that is the position of the circle changes on the HTML page, the smiley would not be positionned correctly.
Could you give me some pointers, to 'anchor' the smiley to the circle element?
Edit:
An example of SVG:
<svg width = 500 height = 500 id="mySvg">
<circle class="mainCircle" cx=25 cy=25 r=25 fill="red"></circle>
<circle class="mainCircle" cx=125 cy=65 r=50 fill="red"></circle>
<circle class="mainCircle" cx=200 cy=12 r=10 fill="red"></circle>
<circle class="mainCircle" cx=210 cy=300 r=90 fill="red"></circle>
<circle class="mainCircle" cx=320 cy=25 r=5 fill="red"></circle>
<circle class="mainCircle" cx=400 cy=120 r=50 fill="red"></circle>
<circle class="mainCircle" cx=410 cy=230 r=25 fill="red"></circle>
</svg>
I reckon that a good solution here would be creating a function to which you can pass the circles' attributes (cx, cy and r), which would create the smileys based only on those values.
Creating the circles yourself
So, for instance, suppose that our circle's data has x, y, and r as those attributes. We can create a function, here named makeSmileys, that draws the circles and the path in the container group:
function makeSmileys(group, xPos, yPos, radius) {
//left eye
group.append("circle")
.attr("cx", xPos - radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
//right eye
group.append("circle")
.attr("cx", xPos + radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
arc.innerRadius(radius / 2)
.outerRadius(radius / 2.2);
//mouth
group.append("path")
.attr("d", arc)
.attr("transform", "translate(" + xPos + "," + yPos + ")");
}
As you can see, the position of the two eyes (circles) and the mouth (path) is based on the arguments only. You can tweak those positions the way you want.
For this function to work, we have to create the container groups and then call it on those respective selections:
circlesGroup.each(function(d) {
d3.select(this).call(makeSmileys, d.x, d.y, d.r)
})
Because I'm using selection.call, the first argument (which is group) is the selection itself. As an alternative, if you don't want to use selection.call, just call the function as a normal JavaScript function, passing the container to it.
Here is a demo, with 10 randomly generated circles:
const svg = d3.select("svg");
const data = d3.range(10).map(function(d) {
return {
x: 50 + Math.random() * 500,
y: 50 + Math.random() * 300,
r: Math.random() * 50
}
});
const arc = d3.arc()
.startAngle(1 * (Math.PI / 2))
.endAngle(3 * (Math.PI / 2));
const circlesGroup = svg.selectAll(null)
.data(data)
.enter()
.append("g");
circlesGroup.each(function(d) {
d3.select(this).append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.r)
.style("fill", "yellow")
.style("stroke", "black")
})
circlesGroup.each(function(d) {
d3.select(this).call(makeSmileys, d.x, d.y, d.r)
})
function makeSmileys(group, xPos, yPos, radius) {
group.append("circle")
.attr("cx", xPos - radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
group.append("circle")
.attr("cx", xPos + radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
arc.innerRadius(radius / 2)
.outerRadius(radius / 2.2);
group.append("path")
.attr("d", arc)
.attr("transform", "translate(" + xPos + "," + yPos + ")");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="600" height="400"></svg>
Using pre-existing circles
If you have an existing SVG (as you made clear in the edited question), you can select all circles with a selector...
const circles = svg.selectAll("circle");
...then get their attributes and finally call the function:
circles.each(function() {
const x = +d3.select(this).attr("cx");
const y = +d3.select(this).attr("cy");
const r = +d3.select(this).attr("r");
makeSmileys(x, y, r)
});
Mind the unary plus here, because the getters return strings for those attributes.
Here is the demo:
const svg = d3.select("svg");
const arc = d3.arc()
.startAngle(1 * (Math.PI / 2))
.endAngle(3 * (Math.PI / 2));
const circles = svg.selectAll("circle");
circles.each(function() {
const x = +d3.select(this).attr("cx");
const y = +d3.select(this).attr("cy");
const r = +d3.select(this).attr("r");
makeSmileys(x, y, r)
})
function makeSmileys(xPos, yPos, radius) {
svg.append("circle")
.attr("cx", xPos - radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
svg.append("circle")
.attr("cx", xPos + radius / 3)
.attr("cy", yPos - radius / 3)
.attr("r", radius / 8)
.style("fill", "black");
arc.innerRadius(radius / 2)
.outerRadius(radius / 2.2);
svg.append("path")
.attr("d", arc)
.attr("transform", "translate(" + xPos + "," + yPos + ")");
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="500" id="mySvg">
<circle class="mainCircle" cx=25 cy=25 r=25 fill="yellow"></circle>
<circle class="mainCircle" cx=125 cy=65 r=50 fill="yellow"></circle>
<circle class="mainCircle" cx=200 cy=12 r=10 fill="yellow"></circle>
<circle class="mainCircle" cx=210 cy=300 r=90 fill="yellow"></circle>
<circle class="mainCircle" cx=320 cy=25 r=5 fill="yellow"></circle>
<circle class="mainCircle" cx=400 cy=120 r=50 fill="yellow"></circle>
<circle class="mainCircle" cx=410 cy=230 r=25 fill="yellow"></circle>
</svg>
Instead of using a unique identifier on your SVG elements, use a class instead like this:
<svg width="50" height="50" class="face">
Then in your D3 you can reference all the instances of this class like so:
let svg = d3.selectAll(".face");

Categories

Resources