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;
I am working on a D3 React requirement where in if user clicks on a rectangle on svgm an input box is provided for text. I want to use that input for updating another component in react app. How do I do that using foreign object in svg. Is there any other way I can link a text box with a rectangle(drawn just above each rectangle) in d3 in React.
My existing code snippet is here
d3.select('svg#target')
.append("g")
.selectAll("whatever")
.data([1,2,3,4])
.enter()
.append("rect")
.attr("x", function(d, i) {
var x = 200 * Math.sin(-60 * i * (Math.PI / 180));
return x - 50;
})
.attr("y", function(d, i) {
var y = 200 * Math.cos(-60 * i * (Math.PI / 180));
return y - 20;
})
.attr("width", 100)
.attr("height", 40)
.attr("fill", "gray")
.on("click", function(d, i) {
const count = i;
d3.select(this)
.append("foreignObject")
.attr("x", function() {
return 300 * Math.sin(-60 * i * (Math.PI / 180)) + 50;
})
.attr("y", function() {
return 280 * Math.cos(-60 * i * (Math.PI / 180));
})
.attr("width", 140)
.attr("height", 20)
.html(function(d, i) {
return (
'<input type="text" id="attribute' +
count +
'value="Text goes here" />'
);
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg viewbox="-250 -250 500 500" width="400" id="target"></svg>
You were pretty close, you only need to change it so that the foreign object is not appended inside the <rect />, but rather inserted as a sibling, like:
<g>
<rect x="-50" y="180" width="100" height="40" fill="gray">
</rect>
<rect x="-223.20508075688772" y="80.00000000000003" width="100" height="40" fill="gray">
</rect>
<rect x="-223.20508075688775" y="-119.99999999999996" width="100" height="40" fill="gray">
</rect>
<rect x="-50.0" y="-220" width="100" height="40" fill="gray">
</rect>
<foreignObject x="-50.0" y="-220" width="100" height="40">
<input type="text" id="attribute3" value="text goes here">
</foreignObject>
</g>
We can do this with this on line 24: d3.select(this.parentNode) which selects the parent, so now when we append, it becomes a sibling of the <rect />, rather than a child.
d3.select('svg#target')
.append("g")
.selectAll("whatever")
.data([1,2,3,4])
.enter()
.append("rect")
.attr("x", function(d, i) {
var x = 200 * Math.sin(-60 * i * (Math.PI / 180));
return x - 50;
})
.attr("y", function(d, i) {
var y = 200 * Math.cos(-60 * i * (Math.PI / 180));
return y - 20;
})
.attr("width", 100)
.attr("height", 40)
.attr("fill", "gray")
.on("click", function(d, i) {
const count = i;
if (document.getElementById('attribute' + count)){
return 0;
}
d3.select(this.parentNode)
.append("foreignObject")
.attr("x", function() {
var x = 200 * Math.sin(-60 * i * (Math.PI / 180));
return x - 50;
})
.attr("y", function() {
var y = 200 * Math.cos(-60 * i * (Math.PI / 180));
return y - 20;
})
.attr("width", 100)
.attr("height", 40)
.html(function(d, i) {
return (
`<input type="text" id="attribute${count}"
style="height: 40px;" value="Text goes here" />`
);
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg viewbox="-250 -250 500 500" width="400" id="target"></svg>
Demo:
Given a pie chart with an arc, I'd like to append lines at each arcs center points to follow the path. I'm able to append the lines on the path properly currently but I'm not able to angle them appropriately. I have a set of circles going around my pie chart and need to place these lines slightly below the circles at the mid point between 1 circle and the next. Here is a pretty gross image of what I'm trying to do, the lines should be at the center of the arc but just below the circles and following the path of the arc.
And here is what I am currently getting with my code.I'm trying to get the dark black lines to follow a similar rotation to the light gray ones that you see at the center.
const startAngle = (-45 * Math.PI) / 180;
const endAngle = (-45 * Math.PI) / 180 + 2 * Math.PI;
const width = 500;
const height = Math.min(width, 500);
const viewBox = [-width / 2, -height / 2, width, height];
const svg = d3
.select("#canvas")
.append("svg")
.attr("viewBox", viewBox);
function drawGridDonut(svg, innerFactor, outerFactor) {
const radius = Math.min(width, height) / 2;
const graph = new Array(6); // 6 equal sections
const gridPie = d3
.pie()
.startAngle(startAngle)
.endAngle(endAngle)
.sort(null)
.value(1);
const arc = d3
.arc()
.innerRadius(radius * innerFactor)
.outerRadius(radius * outerFactor);
// base path + arc
svg
.append("g")
.attr("class", "grid")
.selectAll("path")
.data(gridPie(graph))
.join("path")
.attr("stroke-width", 1)
.attr("fill", "none")
.attr("d", arc);
// border lines
svg
.selectAll(".grid")
// border lines, 1 = dark border, 0 = light border
.data(gridPie([1, 0, 1, 0, 1, 0]))
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr(
"y2",
d =>
Math.sin(d.startAngle - Math.PI / 2) *
(radius - (d.data === 1 ? 0 : 75.5))
)
.attr(
"x2",
d =>
Math.cos(d.startAngle - Math.PI / 2) *
(radius - (d.data === 1 ? 0 : 75.5))
)
.attr("stroke", d => (d.data === 0 ? "#C8C8C8" : "#000"))
.attr("stroke-width", "0.1");
// dots for overuse + benefits
svg
.selectAll(".dot")
.data(gridPie([...Array(12)]))
.enter()
.append("circle")
.attr('class', '.dot')
.attr("cx", d => arc.centroid(d)[0])
.attr("cy", d => arc.centroid(d)[1])
.attr("r", 1)
.style("fill", "#C8C8C8");
// this is where the lines are being made
svg
.selectAll('.hash')
.data(gridPie([...Array(12)]))
.enter()
.append('line')
.attr('class', '.hash')
.attr('d', arc)
.attr("x1", (d) => arc.centroid(d)[0] - d.startAngle)
.attr("y1", (d) => arc.centroid(d)[1] - d.startAngle)
.attr('x2', d => arc.centroid(d)[0] + d.endAngle )
.attr('y2', d => arc.centroid(d)[1] + d.endAngle)
.attr('width', 10)
.attr('height', 4)
.attr("stroke", "black")
.attr('transform', (d) => 'rotate(180)')
}
for (let i = 0; i < 6; i++) {
drawGridDonut(svg, 0.9 - 0.04, 0.9 + 0.002)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="canvas"></div>
I fixed this by drawing a line from the center of my pie to the top then adding dasharray (with fine tuning of the inner and outer arcs to mark the safe spaces.
function drawHyphen(svg, innerFactor, outerFactor, dashPercent) {
const radius = Math.min(width, height) / 2;
const gridPie = d3
.pie()
.startAngle(startAngle)
.endAngle(endAngle)
.sort(null)
.value(1);
svg
.selectAll('.line')
.data(gridPie([...Array(12)]))
.enter()
.append("line")
.attr('class', (d, i) => `line${i}`)
.attr(
"y1",
d =>
Math.sin((d.startAngle) - Math.PI / 4) * radius * innerFactor
)
.attr(
"x1",
d =>
Math.cos((d.startAngle) - Math.PI / 4) * radius * innerFactor
)
.attr(
"y2",
d =>
Math.sin((d.startAngle) - Math.PI / 4) * radius / outerFactor
)
.attr(
"x2",
d =>
Math.cos((d.startAngle) - Math.PI / 4) * radius / outerFactor
)
.attr("stroke", "#EEEEEE")
.attr("stroke-dasharray", `0.5, ${dashPercent}`)
.attr("stroke-width", 4)
}
then I call the function as many times as I need to and set the outerFactor to be the threshold where the safe space will be.
let outerArc = 1 - 0.051
drawHyphen(svg, outerArc - 0.835, outerArc - 2.3, "1.9%")
I am trying to modify the the tooltip in zalando's tech radar.
The relevant code is:
function showBubble(d) {
if (d.active || config.print_layout) {
var tooltip = d3.select("#bubble text")
.text(d.label);
var bbox = tooltip.node().getBBox();
d3.select("#bubble")
.attr("transform", translate(d.x - bbox.width / 2, d.y - 16))
.style("opacity", 0.8);
d3.select("#bubble rect")
.attr("x", -5)
.attr("y", -bbox.height)
.attr("width", bbox.width + 10)
.attr("height", bbox.height + 4);
d3.select("#bubble path")
.attr("transform", translate(bbox.width / 2 - 5, 3));
}
}
In order to extend the tooltip I tried doing the following based on the solution described here.
My modified code:
function showBubble(d) {
if (d.active || config.print_layout) {
var tooltip = d3.select("#bubble text");
tooltip.html("foo"); // this works!
//tooltip.html(function(d) { d.label}) // d is undefinded here ...
tooltip.append("div").attr("id", "foo");
d3.select("#foo").html("This is not shown").attr("style", "block");
var bbox = tooltip.node().getBBox();
d3.select("#bubble")
.attr("transform", translate(d.x - bbox.width / 2, d.y - 16))
.style("opacity", 0.8);
d3.select("#bubble rect")
.attr("x", -5)
.attr("y", -bbox.height)
.attr("width", bbox.width + 10)
.attr("height", bbox.height + 4);
d3.select("#bubble path")
.attr("transform", translate(bbox.width / 2 - 5, 3));
}
}
Can someone give me a hint how to show this extra text?
update
the complete code https://github.com/zalando/tech-radar
Multiline text in svg works a little different than HTML. You can't just append <div> & <br> tags because they don't mean anything in SVG.
So your options are to:
use a foreignObject to render HTML within SVG
var tooltip = d3.select("#bubble")
var fo = tooltip.append('foreignObject').attr('width', '100%').attr('height', '100%')
var foDiv = fo.append("xhtml:body").append("xhtml:div").attr('class', 'fe-div').style('background', '#ccc').html("foo <br>2nd line <br>3rd line")
html,
body,
svg {
height: 100%
}
.fe-div {
color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">
<g id="bubble">
</g>
</svg>
or use positioned tspan elements to break up text like so:
var tooltip = d3.select("#bubble text");
tooltip.html("foo"); // this works!
// Create a tspan for the 2nd line
var tspan1 = tooltip.append("tspan");
tspan1.html("2nd line");
tspan1.attr('x', 0).attr('dy', '1em')
// Create a tspan for the 3rd line
var tspan2 = tooltip.append("tspan");
tspan2.html("3rd line");
tspan2.attr('x', 0).attr('dy', '1em')
html,
body,
svg {
height: 100%
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg viewBox="0 0 240 80" xmlns="http://www.w3.org/2000/svg">
<g id="bubble">
<text y="35"></text>
</g>
</svg>
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.