Aligning x position of tspan in IE11 using d3.js - javascript
Really struggling with this. I've tried a lot of hacks to get the labels to render correctly on my force directed d3 graph. Here is a stackBlitz
You will notice in all other browsers except IE11 renders ok.
In IE:
As you can see in IE11 the first <tspan> isn't anchored to the middle. I've tried a combination of things with and without the x attribute, dx manipulation. I have lined them up via the dx attribute but as each set of labels have different lengths the math isn't the same for each.
The svg structure is as follows:
<text class="edgelabel" id="edgelabel0" font-size="10" style="text-anchor: middle;">
<textPath xlink:href="#edgepath0" dominant-baseline="middle" startOffset="50%" style="cursor: pointer;">
<tspan class="edgelabels" dx="0">label one</tspan>
<tspan class="edgelabels" x="0" dy="22" dx="0">label two</tspan>
<tspan class="edgelabels" x="0" dy="22" dx="0">label three</tspan>
</textPath>
</text>
You can see above in this particular implementation I intentionally left out the x attribute for the first tspan.
This is the code that renders the above:
const edgelabels = zoomContainer
.selectAll('.edgelabel')
.data(links)
.enter()
.append('text')
.attr('class', 'edgelabel')
.style('text-anchor', 'middle')
.attr('id', function (d, i) {
return 'edgelabel' + i;
})
.attr('font-size', 10);
edgelabels
.append('textPath')
.attr('xlink:href', function (d, i) {
return '#edgepath' + i;
})
.style('cursor', 'pointer')
.attr('dominant-baseline', 'middle')
.attr('startOffset', '50%')
.selectAll('div.textPath')
.data(function (d, i) {
return d.label;
})
.enter()
.append('tspan')
.attr('class', 'edgelabels')
.text(function (d, i) {
return console.log(d), d;
})
.attr('x', function (d, i) {
if (i > 0) {
return 0;
}
})
.attr('dy', function (d, i) {
if (i > 0) {
return 22;
}
})
.attr('dx', 0);
Has anybody else had this issue, can you see anything wrong? Is there anything else I could try to get a consistent dx attribute for each set of labels? Could I restructure the code to get a better result?
Complete file code:
import { Injectable } from '#angular/core';
import * as d3 from 'd3';
#Injectable({
providedIn: 'root',
})
export class DirectedGraphExperimentService {
constructor() {}
/** A method to bind a zoom behaviour to the svg g element */
public applyZoomableBehaviour(svgElement, containerElement) {
let svg, container, zoomed, zoom;
svg = d3.select(svgElement);
container = d3.select(containerElement);
zoomed = () => {
const transform = d3.event.transform;
container.attr(
'transform',
'translate(' +
transform.x +
',' +
transform.y +
') scale(' +
transform.k +
')'
);
};
zoom = d3.zoom().scaleExtent([0.5, 1]).on('zoom', zoomed);
svg.call(zoom).style('cursor', 'move');
}
private clearView(svg) {
return svg.selectAll('*').remove();
}
private ticked(link, node, edgepaths, edgelabels) {
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;
});
node.attr('transform', function (d) {
return 'translate(' + d.x + ', ' + d.y + ')';
});
edgepaths.attr('d', function (d) {
return (
'M ' +
d.source.x +
' ' +
d.source.y +
' L ' +
d.target.x +
' ' +
d.target.y
);
});
edgelabels.attr('transform', function (d) {
if (d.target.x < d.source.x) {
let bbox = this.getBBox();
let rx = bbox.x + bbox.width / 2;
let ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
} else {
return 'rotate(0)';
}
});
}
private dragended(d3, d, simulation) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
private initDefinitions(svg) {
svg
.append('defs')
.append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10')
.attr('refX', 27)
.attr('refY', 0)
.attr('orient', 'auto')
.attr('markerWidth', 8)
.attr('markerHeight', 8)
.attr('xoverflow', 'visible')
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', '#999')
.style('stroke', 'none');
return svg;
}
private forceSimulation(d3, { width, height }) {
return d3
.forceSimulation()
.force(
'link',
d3
.forceLink()
.id(function (d) {
return d.id;
})
.distance(500)
.strength(2)
)
.force('charge', d3.forceManyBody())
.force('center', d3.forceCenter(width / 2, height / 2));
}
private _update(d3, svg, data) {
let { links, nodes } = data;
// this.clearView(svg); // removes everything!
this.initDefinitions(svg);
const simulation = this.forceSimulation(d3, {
width: +svg.attr('width'),
height: +svg.attr('height'),
});
const zoomContainer = d3.select('svg g');
const link = zoomContainer
.selectAll('.link')
.data(links)
.enter()
.append('line')
.style('stroke', '#999')
.style('stroke-opacity', '.6')
.style('stroke-width', '2px')
.attr('class', 'link')
.attr('marker-end', 'url(#arrowhead)');
link.append('title').text(function (d) {
return d.label;
});
const edgepaths = zoomContainer
.selectAll('.edgepath')
.data(links)
.enter()
.append('path')
.attr('class', 'edgepath')
.attr('fill-opacity', 0)
.attr('stroke-opacity', 0)
.attr('id', function (d, i) {
return 'edgepath' + i;
});
const edgelabels = zoomContainer
.selectAll('.edgelabel')
.data(links)
.enter()
.append('text')
.attr('class', 'edgelabel')
.style('text-anchor', 'middle')
.attr('id', function (d, i) {
return 'edgelabel' + i;
})
.attr('font-size', 10);
edgelabels
.append('textPath')
.attr('xlink:href', function (d, i) {
return '#edgepath' + i;
})
.style('cursor', 'pointer')
.attr('dominant-baseline', 'middle')
.attr('startOffset', '50%')
.selectAll('div.textPath')
.data(function (d, i) {
return d.label;
})
.enter()
.append('tspan')
.attr('class', 'edgelabels')
.text(function (d, i) {
return console.log(d), d;
})
.attr('x', function (d, i) {
if (i > 0) {
return 0;
}
})
.attr('dy', function (d, i) {
if (i > 0) {
return 22;
}
})
.attr('dx', 0);
svg.selectAll('.edgelabel').on('click', function () {
// arrow function will produce this = undefined
d3.selectAll('.edgelabel').style('fill', '#999');
d3.select(this).style('fill', 'blue');
});
const node = zoomContainer
.selectAll('.node')
.data(nodes)
.enter()
.append('g')
.attr('class', 'node')
.call(
d3
.drag()
.on('start', (d) => this.dragended(d3, d, simulation))
.on('drag', function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
})
.on('end', (d) => this.dragended(d3, d, simulation))
);
node
.append('circle')
.style('fill', '#fff')
.style('cursor', 'pointer')
.style('fill-opacity', '1')
.style('stroke-opacity', '0.5')
.attr('id', (d, i) => d.id)
.attr('r', 28);
svg.selectAll('circle').on('click', function () {
// arrow function will produce this = undefined
d3.selectAll('.nodeText').style('fill', 'black');
d3.select(this.parentNode).selectAll('.nodeText').style('fill', 'blue');
});
node
.append('image')
.attr('xlink:href', 'https://github.com/favicon.ico')
.attr('x', -10)
.attr('y', -40)
.attr('width', 16)
.attr('class', 'image')
.attr('height', 16);
node
.append('svg:foreignObject')
.attr('class', 'delete-icon')
.html('<div style="color:green;padding-left: 50px;">remove</div>')
.text('delete');
const nodeText = node
.data(nodes)
.append('text')
.style('text-anchor', 'middle')
.style('cursor', 'pointer')
.attr('dy', -3)
.attr('y', -25)
.attr('class', 'nodeText')
.attr('id', 'nodeText');
nodeText
.selectAll('tspan')
.data((d, i) => d.label)
.enter()
.append('tspan')
.attr('class', 'nodeTextTspan')
.text((d) => d)
.style('font-size', '12px')
.attr('x', -10)
.attr('dx', 10)
.attr('dy', 22);
svg.selectAll('.nodeText').on('click', function () {
d3.selectAll('.nodeText').style('fill', 'black');
d3.select(this).style('fill', 'blue');
});
node.append('title').text(function (d) {
return d.id;
});
simulation.nodes(nodes).on('tick', () => {
this.ticked(link, node, edgepaths, edgelabels);
});
simulation.force('link').links(links);
}
public update(data, element) {
const svg = d3.select(element);
return this._update(d3, svg, data);
}
}
UPDATE:
IE11 does not like <tspans> inside of <textPaths> therefore:
I've implemented some of #herrstrietzel solution into my new demo. The <tspans> are now directly inside of the <text> elements and each <text> has a dynamic x/y coordinates to lay it on the <path> as we no longer have <textPaths> to do this for us. I've had to do a lot of the rework inside of the ticked() method to gain access to the latest x/y coords as you do not get these outside. One issue remains:
// gets the coordinates of the edgepaths to use for the <text> positioning
edgepaths.each(function (d) {
source_x = d.source.x;
source_y = d.source.y;
target_x = d.target.x;
target_y = d.target.y;
});
let p1 = { x: source_x, y: source_y };
let p2 = { x: target_x, y: target_y };
// centers the <text> on the path
let textAnchor = this.interpolatedPoint(p1, p2, 0.5);
// Adds the x/y attribute to the <text> elements
edgelabels.attr('x', textAnchor.x).attr('y', textAnchor.y)
The text elements share the same coordinates and therefore share the same path... not sure why it hasn't adjusted inside the loop. The index of the first label that's already there should get the first index coordinate, but for some reason they are all just getting one set of path coordinates. p1 and p2 always give me the last coordinates in the array of edges.
Ok I figured it out. Here is the rendered SVG that works - IE11 doesn't like tspan's inside textPaths, so you have to separate each label into its own text + textPath element with an appropriate dy attribute on the text element.
<svg _ngcontent-c1="" height="600" width="900" style="cursor: move;"><g _ngcontent-c1="" ng-reflect-zoomable-of="[object SVGSVGElement]">
<line class="link" marker-end="url(#arrowhead)" style="stroke: rgb(153, 153, 153); stroke-opacity: 0.6; stroke-width: 2px;" x1="609.1621195478509" y1="362.0472615717732" x2="120.80347004842467" y2="254.41514023225977">
<title>label one,label two,label three</title></line>
<line class="link" marker-end="url(#arrowhead)" style="stroke: rgb(153, 153, 153); stroke-opacity: 0.6; stroke-width: 2px;" x1="620.0342472996117" y1="283.53757565928413" x2="120.80347004842467" y2="254.41514023225977">
<title>really Long Link text,tom</title>
</line>
<path class="edgepath" fill-opacity="0" stroke-opacity="0" id="edgepath0" d="M 609.1621195478509 362.0472615717732 L 120.80347004842467 254.41514023225977"></path><path class="edgepath" fill-opacity="0" stroke-opacity="0" id="edgepath1" d="M 620.0342472996117 283.53757565928413 L 120.80347004842467 254.41514023225977"></path>
<text class="edgelabel" id="edgelabel0" font-size="10" style="text-anchor: middle;" transform="rotate(180 370.5402526855469 287.623779296875)" dy="0">
<textPath xlink:href="#edgepath0" dominant-baseline="middle" startOffset="50%" style="cursor: pointer;">
label one
</textPath>
</text>
<text class="edgelabel" id="edgelabel0" font-size="10" style="text-anchor: middle;" transform="rotate(180 370.5402526855469 287.623779296875)" dy="22">
<textPath xlink:href="#edgepath0" dominant-baseline="middle" startOffset="50%" style="cursor: pointer;">
label two
</textPath>
</text>
<text class="edgelabel" id="edgelabel0" font-size="10" style="text-anchor: middle;" transform="rotate(180 370.5402526855469 287.623779296875)" dy="44">
<textPath xlink:href="#edgepath0" dominant-baseline="middle" startOffset="50%" style="cursor: pointer;">
label three
</textPath>
</text>
<text class="edgelabel" id="edgelabel1" font-size="10" style="text-anchor: middle;" transform="rotate(180 370.2359619140625 260.2969512939453)"><textPath xlink:href="#edgepath1" dominant-baseline="middle" startOffset="50%" style="cursor: pointer;"><tspan class="edgelabels" dx="0">really Long Link text</tspan><tspan class="edgelabels" x="0" dy="22" dx="0">tom</tspan></textPath></text><g class="node" transform="translate(609.1621195478509, 362.0472615717732)"><circle id="5678" r="28" style="fill: rgb(255, 255, 255); cursor: pointer; fill-opacity: 1; stroke-opacity: 0.5;"></circle><image xlink:href="https://github.com/favicon.ico" x="-10" y="-40" width="16" class="image" height="16"></image><foreignObject class="delete-icon">delete</foreignObject><text dy="-3" y="-25" class="nodeText" id="nodeText" style="text-anchor: middle; cursor: pointer;"><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Red</tspan><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">BMW</tspan><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Fast</tspan></text><title>5678</title></g><g class="node" transform="translate(120.80347004842467, 254.41514023225977)"><circle id="2494EA62" r="28" style="fill: rgb(255, 255, 255); cursor: pointer; fill-opacity: 1; stroke-opacity: 0.5;"></circle><image xlink:href="https://github.com/favicon.ico" x="-10" y="-40" width="16" class="image" height="16"></image><foreignObject class="delete-icon">delete</foreignObject><text dy="-3" y="-25" class="nodeText" id="nodeText" style="text-anchor: middle; cursor: pointer;"><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Cool</tspan><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Brown</tspan><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Ford Cortina</tspan></text><title>2494EA62</title></g><g class="node" transform="translate(620.0342472996117, 283.53757565928413)"><circle id="123" r="28" style="fill: rgb(255, 255, 255); cursor: pointer; fill-opacity: 1; stroke-opacity: 0.5;"></circle><image xlink:href="https://github.com/favicon.ico" x="-10" y="-40" width="16" class="image" height="16"></image><foreignObject class="delete-icon">delete</foreignObject><text dy="-3" y="-25" class="nodeText" id="nodeText" style="text-anchor: middle; cursor: pointer;"><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Aston</tspan><tspan class="nodeTextTspan" x="-10" dx="10" dy="22" style="font-size: 12px;">Martin</tspan></text><title>123</title></g></g><defs><marker id="arrowhead" viewBox="-0 -5 10 10" refX="27" refY="0" orient="auto" markerWidth="8" markerHeight="8" xoverflow="visible"><path d="M 0,-5 L 10 ,0 L 0,5" fill="#999" style="stroke: none;"></path></marker></defs></svg>
Not a proper d3.js solution, but should be able to adapt/include it in your script.
Calculate centered text x/y coordinates and rotation angles according to line/graph
We're using <text> elements placed at the middle of the line/graph replacing <textpath> usage.
Get centered text anchor coordinates replacing startOffset="50%"
Since you know the line's starting and end points, we can easily interpolate a mid point like so:
let p1 = {x:x1, y:y1};
let p2 = {x:x2, y:y2};
let textAnchor = {x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2};
The textAnchor point coordinates will become the <text> elements new x and y atribute values.
Get rotation angle
let angle = (Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI;
This variable is used so set the transform rotate property according to the line's drawing angle.
// example coordinates - should come from your data object
let x1 = line1.x1.baseVal.value;
let x2 = line1.x2.baseVal.value;
let y1 = line1.y1.baseVal.value;
let y2 = line1.y2.baseVal.value;
let p1 = {x:x1, y:y1};
let p2 = {x:x2, y:y2};
/**
* calculte mid point on line
* use LERP helper if you need to place the mid point at other positions:
* e.g t=0.75 = 75%
*/
let textAnchor = interpolatedPoint(p1, p2, 0.5);
// use this for simple centered alignment
//let textAnchor = {x:(p1.x+p2.x)/2, y:(p1.y+p2.y)/2};
// move parent <text> element to mid point
textLabel.setAttribute('x', textAnchor.x)
textLabel.setAttribute('y', textAnchor.y);
// add x and dy attributes to tspan for pdeudo-multiline appearance
let lineHeight = parseFloat(window.getComputedStyle(textLabel).fontSize)*1.2;
setTspanMultiline(textLabel, lineHeight)
// rotate according to line center point
let angle = getAngle(p1, p2);
// svg attribute transform
textLabel.setAttribute('transform', `rotate(${angle} ${textAnchor.x} ${textAnchor.y})`)
// css style equivalent
//textLabel.style.transform = `translate(${textAnchor.x}px, ${textAnchor.y}px) rotate(${angle}deg) translate(-${textAnchor.x}px, -${textAnchor.y}px)`;
function setTspanMultiline(textEl, dy){
let tspans = textEl.querySelectorAll('tspan');
let totalDy = (tspans.length-1)*dy;
let x= +textEl.getAttribute('x');
tspans.forEach((tspan, i)=>{
tspan.setAttribute('x', x);
if(i>0){
tspan.setAttribute('dy', dy);
}
})
textEl.setAttribute('dy', totalDy/-2 )
}
/**
* Linear interpolation (LERP) helper
*/
function interpolatedPoint(p1, p2, t = 0.5) {
//t: 0.5 - point in the middle
if (Array.isArray(p1)) {
p1.x = p1[0];
p1.y = p1[1];
}
if (Array.isArray(p2)) {
p2.x = p2[0];
p2.y = p2[1];
}
let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
return {
x: x,
y: y
};
}
// get angle helper
function getAngle(p1, p2) {
let angle = (Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180) / Math.PI;
return angle;
}
svg{
width:50em;
border:1px solid #ccc;
overflow:visible;
}
text{
font-size:10px;
}
<svg id="svg" viewBox="0 0 500 100">
<line id="line1" x1="0" x2="100%" y1="25%" y2="75%" stroke="#ccc" stroke-width="1" />
<text x="0" id="textLabel" text-anchor="middle" dominant-baseline="middle">
<tspan class="edgelabels">line one</tspan>
<tspan class="edgelabels">line two long</tspan>
<tspan class="edgelabels">line three</tspan>
<tspan class="edgelabels">line four</tspan>
</text>
</svg>
In the above example I'm using the setTspanMultiline(textEl, dy) helper function to generate the correct x, y and dy values.
<text x="250" id="textLabel" text-anchor="middle" dominant-baseline="middle" y="50" dy="-18" transform="rotate(5.710593137499643 250 50)">
<tspan class="edgelabels" x="250">line one</tspan>
<tspan class="edgelabels" x="250" dy="12">line two long</tspan>
<tspan class="edgelabels" x="250" dy="12">line three</tspan>
<tspan class="edgelabels" x="250" dy="12">line four</tspan>
</text>
Basically all <tspan> inherit the x value from the parent <text> elements.
The relative dy will emulate the line height.
The parent <text> dy value will vertically center the text around the line and is calculated like so:
(Total number of tspans - 1) * dy
let tspans = textEl.querySelectorAll('tspan');
let totalDy = (tspans.length-1)*dy;
Related
Group D3 legend items
How can I group the circle and text for each legend item? I am having an issue with enter/exit. I can create the groups (g), but I cannot append the text/circle elements. li.selectAll("g") .data(items, ([key]) => key) .call((d) => d.enter().append("text") .attr("y", (d, i) => (i * 1.25) + "em") .attr("x", "1em") .text(([key]) => key)) .call((d) => d.exit().remove()) .call((d) => d.enter().append("circle") .attr("cy", (d, i) => (i * 1.25 - 0.25) + "em") .attr("cx", 0) .attr("r", "0.4em") .style("fill", ([key, value]) => value.color)) Here is the current rendered SVG markup: <g class="legend-items"> <text y="0em" x="1em">Amanda</text> <text y="1.25em" x="1em">Linda</text> <text y="2.5em" x="1em">Dorothy</text> <text y="3.75em" x="1em">Betty</text> <text y="5em" x="1em">Helen</text> <text y="6.25em" x="1em">Patricia</text> <text y="7.5em" x="1em">Jessica</text> <text y="8.75em" x="1em">Ashley</text> <text y="10em" x="1em">Deborah</text> <circle cy="-0.25em" cx="0" r="0.4em" style="fill: rgb(228, 26, 28);"></circle> <circle cy="1em" cx="0" r="0.4em" style="fill: rgb(247, 129, 191);"></circle> <circle cy="2.25em" cx="0" r="0.4em" style="fill: rgb(255, 127, 0);"></circle> <circle cy="3.5em" cx="0" r="0.4em" style="fill: rgb(77, 175, 74);"></circle> <circle cy="4.75em" cx="0" r="0.4em" style="fill: rgb(255, 255, 51);"></circle> <circle cy="6em" cx="0" r="0.4em" style="fill: rgb(153, 153, 153);"></circle> <circle cy="7.25em" cx="0" r="0.4em" style="fill: rgb(166, 86, 40);"></circle> <circle cy="8.5em" cx="0" r="0.4em" style="fill: rgb(55, 126, 184);"></circle> <circle cy="9.75em" cx="0" r="0.4em" style="fill: rgb(152, 78, 163);"></circle> </g> This is the desired markup: <g class="legend-items"> <g class="legend-item" data-key="Amanda"> <text y="0em" x="1em">Amanda</text> <circle cy="-0.25em" cx="0" r="0.4em" style="fill: rgb(228, 26, 28);"></circle> </g> <g class="legend-item" data-key="Linda"> <text y="1.25em" x="1em">Linda</text> <circle cy="1em" cx="0" r="0.4em" style="fill: rgb(247, 129, 191);"></circle> </g> <g class="legend-item" data-key="Dorothy"> <text y="2.5em" x="1em">Dorothy</text> <circle cy="2.25em" cx="0" r="0.4em" style="fill: rgb(255, 127, 0);"></circle> </g> <g class="legend-item" data-key="Betty"> <text y="3.75em" x="1em">Betty</text> <circle cy="3.5em" cx="0" r="0.4em" style="fill: rgb(77, 175, 74);"></circle> </g> <g class="legend-item" data-key="Helen"> <text y="5em" x="1em">Helen</text> <circle cy="4.75em" cx="0" r="0.4em" style="fill: rgb(255, 255, 51);"></circle> </g> <g class="legend-item" data-key="Patricia"> <text y="6.25em" x="1em">Patricia</text> <circle cy="6em" cx="0" r="0.4em" style="fill: rgb(153, 153, 153);"></circle> </g> <g class="legend-item" data-key="Jessica"> <text y="7.5em" x="1em">Jessica</text> <circle cy="7.25em" cx="0" r="0.4em" style="fill: rgb(166, 86, 40);"></circle> </g> <g class="legend-item" data-key="Ashley"> <text y="8.75em" x="1em">Ashley</text> <circle cy="8.5em" cx="0" r="0.4em" style="fill: rgb(55, 126, 184);"></circle> </g> <g class="legend-item" data-key="Deborah"> <text y="10em" x="1em">Deborah</text> <circle cy="9.75em" cx="0" r="0.4em" style="fill: rgb(152, 78, 163);"></circle> </g> </g> This way I can add an event listener to .legend-item and use the data attribute key to cross-reference with the line-series. Note: The legend code below is an updated version (6.2.0) of d3-legend. I tweaked the code slightly. Example Here is a working example of what I have so far. (function() { d3.legend = function(g) { g.each(function() { var g = d3.select(this), items = {}, svg = d3.select(g.property("nearestViewportElement")), legendPadding = g.attr("data-style-padding") || 5, lb = g.selectAll(".legend-box").data([true]), li = g.selectAll(".legend-items").data([true]); lb = lb.enter().append("rect").classed("legend-box", true).merge(lb); li = li.enter().append("g").classed("legend-items", true).merge(li); svg.selectAll("[data-legend]").each(function() { var self = d3.select(this); items[self.attr("data-legend")] = { pos: self.attr("data-legend-pos") || this.getBBox().y, color: self.attr("data-legend-color") || self.style("stroke") || self.style("fill") }; }); items = Object.entries(items) .sort(([keyA, valA], [keyB, valB]) => valA.pos - valB.pos); li.selectAll("text") .data(items, ([key]) => key) .call((d) => d.enter().append("text") .attr("y", (d, i) => (i * 1.25) + "em") .attr("x", "1em") .text(([key]) => key)) .call((d) => d.exit().remove()) li.selectAll("circle") .data(items, ([key]) => key) .call((d) => d.enter().append("circle") .attr("cy", (d, i) => (i * 1.25 - 0.25) + "em") .attr("cx", 0) .attr("r", "0.4em") .style("fill", ([key, value]) => value.color)) // Reposition and resize the box var lbbox = li.node().getBBox(); lb.attr("x", lbbox.x - legendPadding) .attr("y", lbbox.y - legendPadding) .attr("height", lbbox.height + 2 * legendPadding) .attr("width", lbbox.width + 2 * legendPadding); }); return g; }; })(); const apiUrl = "https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/5_OneCatSevNumOrdered.csv"; const palette = [ "#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33", "#a65628", "#f781bf", "#999999" ]; const chartConfig = { groupingKey: "name", xField: "year", yField: "n" }; const mapperFn = ({ year, n, prop, ...rest }) => ({ ...rest, year: parseInt(year, 10), n: parseFloat(n), prop: parseFloat(prop) }); const { groupingKey, xField, yField } = chartConfig; const margin = { top: 10, right: 120, bottom: 20, left: 60 }, width = 400 - margin.left - margin.right, height = 200 - margin.top - margin.bottom; d3.csv(apiUrl).then((jsonData) => { const data = jsonData.map(mapperFn); const ref = document.querySelector('.d3-chart > svg'); const svgElement = d3.select(ref); // Empty the children svgElement.selectAll("*").remove(); // Inner-SVG const svgInner = svgElement .append("g") .attr("transform", `translate(${margin.left}, ${margin.top})`); // Group the data: I want to draw one line per group const groupedData = [ ...d3.group(data, (d) => d[groupingKey]) ] .map(([key, values]) => ({ key, values })); const halfway = Math.floor(groupedData.length / 2); const dataLeft = groupedData.slice(0, halfway); const dataRight = groupedData.slice(halfway); // Add X axis --> it is a date format var xScale = d3 .scaleLinear() .domain(d3.extent(data, (d) => d[xField])) .range([0, width]); svgInner .append("g") .attr("class", "d3-axis d3-axis-bottom") .attr("transform", `translate(0, ${height})`) .call( d3 .axisBottom(xScale) .ticks(2) .tickFormat((year) => year.toString()) ); // Add Y axis var yScaleLeft = d3 .scaleLinear() .domain([ 0, d3.max( dataLeft.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); var yScaleRight = d3 .scaleLinear() .domain([ 0, d3.max( dataRight.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .call(d3.axisLeft(yScaleLeft)); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .attr("transform", `translate(${width}, 0)`) .call(d3.axisRight(yScaleRight)); // Color palette var res = groupedData.map((d) => d.key).sort(); // list of group names var color = d3.scaleOrdinal().domain(res).range(palette); svgInner .append("g") .attr("transform", `translate(${margin.left}, ${margin.top})`); // Draw the line svgInner .selectAll(".line") .data(dataLeft) .enter() .append("path") .attr("fill", "none") .attr("data-legend", (d) => d.key) .attr("stroke", (d) => color(d.key)) .attr("stroke-width", 1.5) .attr("d", function(d) { return d3 .line() .x((d) => xScale(d[xField])) .y((d) => yScaleLeft(+d[yField]))(d.values); }); svgInner .selectAll(".line") .data(dataRight) .enter() .append("path") .attr("fill", "none") .attr("data-legend", (d) => d.key) .attr("stroke", (d) => color(d.key)) .attr("stroke-width", 1.5) .attr("d", function(d) { return d3 .line() .x((d) => xScale(d[xField])) .y((d) => yScaleRight(+d[yField]))(d.values); }); let legend = svgInner .append("g") .attr("class", "legend") .attr("transform", "translate(280,20)") .style("font-size", "12px") .call(d3.legend); }); :root { --chart-background: #272727; --chart-foreground: #d7d7d7; } .d3-chart { background: var(--chart-background); } .d3-chart .d3-axis line, .d3-chart .d3-axis path { stroke: var(--chart-foreground); } .d3-chart .d3-axis text { fill: var(--chart-foreground); } .d3-chart .legend-box { fill: none; } .d3-chart .legend .legend-items text { fill: var(--chart-foreground); } <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script> <div class="d3-chart"> <svg width="400px" height="200px" /> </div>
The result of .enter() is a selection that has corresponding datum values, but does not have any nodes related to it. That is why we normally only merge at the very last moment, after having called .append("g"). g = li.selectAll("g") .data(items); g .enter() .append("g") .merge(g); Because you can't append g twice, you would need to append a g element, then a text element, and then return the g. This would look something like this: li.selectAll("g") .data(items, ([key]) => key) .call((d) => { const g = d.enter() .append("g"); g.append("text") .attr("y", (d, i) => (i * 1.25) + "em") .attr("x", "1em") .text(([key]) => key); g.append("circle") .attr("cy", (d, i) => (i * 1.25 - 0.25) + "em") .attr("cx", 0) .attr("r", "0.4em") .style("fill", ([key, value]) => value.color) return g; }) .call((d) => d.exit().remove()) However, since you're using d3 v6, you can also use the new .join() functionality, where you can apply functions to your .enter(), .exit() and (implicit) update parts of the selection, and which returns the merged result: li.selectAll("g") .data(items, ([key]) => key) .join( enter => { const g = enter.append("g"); g.append("text") .attr("y", (d, i) => (i * 1.25) + "em") .attr("x", "1em") .text(([key]) => key); g.append("circle") .attr("cy", (d, i) => (i * 1.25 - 0.25) + "em") .attr("cx", 0) .attr("r", "0.4em") .style("fill", ([key, value]) => value.color); return g; }, update => update, exit => exit.remove() ); That leads to the following snippet: (function() { d3.legend = function(g) { g.each(function() { var g = d3.select(this), items = {}, svg = d3.select(g.property("nearestViewportElement")), legendPadding = g.attr("data-style-padding") || 5, lb = g.selectAll(".legend-box").data([true]), li = g.selectAll(".legend-items").data([true]); lb = lb.enter().append("rect").classed("legend-box", true).merge(lb); li = li.enter().append("g").classed("legend-items", true).merge(li); svg.selectAll("[data-legend]").each(function() { var self = d3.select(this); items[self.attr("data-legend")] = { pos: self.attr("data-legend-pos") || this.getBBox().y, color: self.attr("data-legend-color") || self.style("stroke") || self.style("fill") }; }); items = Object.entries(items) .sort(([keyA, valA], [keyB, valB]) => valA.pos - valB.pos); li.selectAll("g") .data(items, ([key]) => key) .join( enter => { const g = enter.append("g"); g.append("text") .attr("y", (d, i) => (i * 1.25) + "em") .attr("x", "1em") .text(([key]) => key); g.append("circle") .attr("cy", (d, i) => (i * 1.25 - 0.25) + "em") .attr("cx", 0) .attr("r", "0.4em") .style("fill", ([key, value]) => value.color); return g; }, update => update, exit => exit.remove() ); // Reposition and resize the box var lbbox = li.node().getBBox(); lb.attr("x", lbbox.x - legendPadding) .attr("y", lbbox.y - legendPadding) .attr("height", lbbox.height + 2 * legendPadding) .attr("width", lbbox.width + 2 * legendPadding); }); return g; }; })(); const apiUrl = "https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/5_OneCatSevNumOrdered.csv"; const palette = [ "#e41a1c", "#377eb8", "#4daf4a", "#984ea3", "#ff7f00", "#ffff33", "#a65628", "#f781bf", "#999999" ]; const chartConfig = { groupingKey: "name", xField: "year", yField: "n" }; const mapperFn = ({ year, n, prop, ...rest }) => ({ ...rest, year: parseInt(year, 10), n: parseFloat(n), prop: parseFloat(prop) }); const { groupingKey, xField, yField } = chartConfig; const margin = { top: 10, right: 120, bottom: 20, left: 60 }, width = 400 - margin.left - margin.right, height = 200 - margin.top - margin.bottom; d3.csv(apiUrl).then((jsonData) => { const data = jsonData.map(mapperFn); const ref = document.querySelector('.d3-chart > svg'); const svgElement = d3.select(ref); // Empty the children svgElement.selectAll("*").remove(); // Inner-SVG const svgInner = svgElement .append("g") .attr("transform", `translate(${margin.left}, ${margin.top})`); // Group the data: I want to draw one line per group const groupedData = [ ...d3.group(data, (d) => d[groupingKey]) ] .map(([key, values]) => ({ key, values })); const halfway = Math.floor(groupedData.length / 2); const dataLeft = groupedData.slice(0, halfway); const dataRight = groupedData.slice(halfway); // Add X axis --> it is a date format var xScale = d3 .scaleLinear() .domain(d3.extent(data, (d) => d[xField])) .range([0, width]); svgInner .append("g") .attr("class", "d3-axis d3-axis-bottom") .attr("transform", `translate(0, ${height})`) .call( d3 .axisBottom(xScale) .ticks(2) .tickFormat((year) => year.toString()) ); // Add Y axis var yScaleLeft = d3 .scaleLinear() .domain([ 0, d3.max( dataLeft.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); var yScaleRight = d3 .scaleLinear() .domain([ 0, d3.max( dataRight.flatMap((d) => d.values), (d) => d[yField] ) ]) .range([height, 0]); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .call(d3.axisLeft(yScaleLeft)); svgInner .append("g") .attr("class", "d3-axis d3-axis-right") .attr("transform", `translate(${width}, 0)`) .call(d3.axisRight(yScaleRight)); // Color palette var res = groupedData.map((d) => d.key).sort(); // list of group names var color = d3.scaleOrdinal().domain(res).range(palette); svgInner .append("g") .attr("transform", `translate(${margin.left}, ${margin.top})`); // Draw the line svgInner .selectAll(".line") .data(dataLeft) .enter() .append("path") .attr("fill", "none") .attr("data-legend", (d) => d.key) .attr("stroke", (d) => color(d.key)) .attr("stroke-width", 1.5) .attr("d", function(d) { return d3 .line() .x((d) => xScale(d[xField])) .y((d) => yScaleLeft(+d[yField]))(d.values); }); svgInner .selectAll(".line") .data(dataRight) .enter() .append("path") .attr("fill", "none") .attr("data-legend", (d) => d.key) .attr("stroke", (d) => color(d.key)) .attr("stroke-width", 1.5) .attr("d", function(d) { return d3 .line() .x((d) => xScale(d[xField])) .y((d) => yScaleRight(+d[yField]))(d.values); }); let legend = svgInner .append("g") .attr("class", "legend") .attr("transform", "translate(280,20)") .style("font-size", "12px") .call(d3.legend); }); :root { --chart-background: #272727; --chart-foreground: #d7d7d7; } .d3-chart { background: var(--chart-background); } .d3-chart .d3-axis line, .d3-chart .d3-axis path { stroke: var(--chart-foreground); } .d3-chart .d3-axis text { fill: var(--chart-foreground); } .d3-chart .legend-box { fill: none; } .d3-chart .legend .legend-items text { fill: var(--chart-foreground); } <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script> <div class="d3-chart"> <svg width="400px" height="200px" /> </div>
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.
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");