rotate and zoom svg with d3 javascript - javascript

I want to rotate and zoom graphic around its center with D3.js. When I zoom graphic I want to zoom it with current aspect ratio and vice versa when I rotate graphic I want to zoom it to the current point that my mouse points. For zooming I use wheel of the mouse and for rotation I use the button of the mouse.
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
transform = d3.zoomIdentity;
var points = d3.range(2000).map(phyllotaxis(10));
var g = svg.append("g");
g.append("line")
.attr("x1", "20")
.attr("y1", "20")
.attr("x2", "60")
.attr("y2", "60")
.attr("stroke", "black")
.attr("stroke-width", "10");
svg.call(d3.drag()
.on("drag",onDrag)
)
// ##########################
var boxCenter = [100, 100];
// #############################
function onDrag(){
var x = d3.event.sourceEvent.pageX,
y = d3.event.sourceEvent.pageY;
var angle = Math.atan2(x - boxCenter[0],
- (y - boxCenter[1]) )*(180/Math.PI);
g.attr("transform", "rotate("+angle+")");
}
svg.call(d3.zoom()
.scaleExtent([1 / 2, 8])
.on("zoom", zoomed));
function zoomed() {
g.attr("transform", d3.event.transform);
}
function phyllotaxis(radius) {
var theta = Math.PI * (3 - Math.sqrt(5));
return function(i) {
var r = radius * Math.sqrt(i), a = theta * i;
return {
x: width / 2 + r * Math.cos(a),
y: height / 2 + r * Math.sin(a)
};
};
}
Here is my example:
https://jsfiddle.net/6Lyjz35L/

For the rotation around center to be correct at the initial zoom you need to add a 'transform-origin' attribute to 'g'.
g.attr("transform-origin", "50% 50%");
The other problems you're having stem from assigning the 'transform' attribute in two separate places. An element ('g') can only have one 'transform' attribute applied at a time, so you're overwriting one or the other each time you rotate or zoom. To fix this you can create a helper method which will append both of the transforms you want in a single string.
var currentAngle = 0;
var currentZoom = '';
function getTransform(p_angle, p_zoom) {
return `${p_zoom} rotate(${p_angle})`;
// return p_zoom + " rotate(" + p_angle + ")";
}
// In the rotate:
currentAngle = angle;
g.attr("transform", getTransform(currentAngle, currentZoom));
// In the zoom:
currentZoom = d3.event.transform;
g.attr("transform", getTransform(currentAngle, currentZoom));
There is one more issue which is introduced by the zoom, and that is that you'll have to calculate a new transform-origin at different zoom levels.
The issue I said was introduced by the zoom was actually the result of applying the operations in the incorrect order. Originally I applied the rotation and THEN then translation. It actually needs to be reversed, translation and THEN rotation. This will keep the correct transform-origin.
Here's a fiddle with those changes: https://jsfiddle.net/scmxcszz/1/

Related

How to get map to show longitude and latitude accurately with d3

I'm using d3.js to try to map coordinates for a csv data file that contains an order id, latitude & longitude of the address the order was shipped to, and the amount that was spent on the order. I've tried mapping them out linearly and tried using a log scale, but the plot points still seem skewed. I was trying to get it to look like a map of the US which it slightly resembles, but the map seems warped/skewed. I made sure that the longitude was set to the x-axis and that latitude was set for the y-axis. The radius of the circles are related to the amount spent on orders. I'm wondering if it has something to do with the scale used, but this is my first time trying to mess with d3, so any help/advice would be appreciated!
var outerWidth = 500;
var outerHeight = 250;
var margin = { left: -50, top: 0, right: -50, bottom: 0 };
var xColumn = "longitude";
var yColumn = "latitude";
var rColumn = "total";
var dollarPerPixel = 10;
var innerWidth = outerWidth - margin.left - margin.right;
var innerHeight = outerHeight - margin.top - margin.bottom;
var svg = d3.select("body").append("svg")
.attr("width", outerWidth)
.attr("height", outerHeight);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xScale = d3.scale.log().range([0, innerWidth]);
var yScale = d3.scale.log().range([innerHeight, 0]);
var rScale = d3.scale.sqrt();
function render(data){
xScale.domain( d3.extent(data, function (d){ return d[xColumn]; }));
yScale.domain( d3.extent(data, function (d){ return d[yColumn]; }));
rScale.domain([0, d3.max(data, function (d){ return d[rColumn]; })]);
// Compute the size of the biggest circle as a function of dollarPerPixel.
var dollarMax = rScale.domain()[1];
var rMin = 0;
var rMax = Math.sqrt(dollarMax / (dollarPerPixel * Math.PI));
rScale.range([rMin, rMax]);
var circles = g.selectAll("circle").data(data);
circles.enter().append("circle");
circles
.attr("cx", function (d){ return xScale(d[xColumn]); })
.attr("cy", function (d){ return yScale(d[yColumn]); })
.attr("r", function (d){ return rScale(d[rColumn]); });
circles.exit().remove();
}
function type(d){
d.latitude = +d.latitude;
d.longitude = +d.longitude;
d.total = +d.total;
return d;
}
d3.csv("data.csv", type, render);
While scales may seem to be an appropriate method for plotting geographic points: don't use this approach.
You lose control over rotation of a projection and you cannot use a non cylindrical projection (only unrotated cylindrical projections can plot lat and long independently). But it also makes it very hard to align features positioned by scales with other map elements if they don't use the same approach.
Instead, D3 has a wide range of built in projections.
The projections take a [longitude,latitude] pair and return a [x,y] coordinate. Latitudes and longitudes must be in degrees, x and y are in pixels.
To create a projection you can use:
var projection = d3.geoMercator() // or geoAlbers, geoSupportedProjection, etc.
To use it, just pass it a coordinate:
projection([long,lat]) // [x,y]
In your case this might look like (for the cx, cy looks similar)
.attr("cx", function(d) { return projection([d.long,d.lat])[0] })
Now this projection is centered at 0,0 degrees by default and set up for a 960x500 pixel map. But you can modify scale, center and rotation, for example:
var projection = d3.geoMercator().center([-100,35]).scale(1000)
For a more complete run down of projection methods you should look at the documentation for d3-geo.
In your case there is a special composite projection that covers the US, d3.geoAlbersUsa, which has room for Hawaii and Alaska. But, because of its composite nature is less flexible, though you can still scale it. The default scale anticipates 960x600 pixel map (setting larger map scales spreads the map over a larger area).

d3.js visibility-zone calculations or how to draw geo rectangle

i want to figure out how to properly calculate vizibility zone and draw it using d3.geo projections. visibility zone in my case is optical camera frustum
for now, i have a two plots, both represent azimuth and elevation from view point, one in gnomonic (according to wiki) projection:
// this magic number is experimentally found
//pixels in one degree in gnomonic projection chart with scale 1500
var px = 26.8;
Width and height below is a optical camera view angles in degrees by azimuth and elevation axes
var w = px * viewport.width;
var h = px * viewport.height;
d3.geoGnomonic()
.translate([w / 2, h / 2])
.scale(1500)
on gnomonic plot i've placed points by its border, then reproject these points using d3.projection.invert method and used resulting angles on d3.geoEquirectangular projection plot to draw areas(like here), with following results:
viewport here is a size of frustum in angles
current method is wrong, but gives me approximate result
i want to figure out what is wrong in my scenario..
ps: i've extracted minimum example, it differs from original code but has same bug: here you can see that size by horizontal axis differs from input size (must be 10, 20, 30, 40 degrees)
Suggestions and comments are appriciated. Thanks for reading!
var d3 = window.d3;
var colorGenerator = d3.scaleOrdinal(d3.schemeCategory10);
var bounds = [650, 500];
var projection = d3.geoEquirectangular().translate([bounds[0]/2, bounds[1]/2]);
var geoPath = d3.geoPath().projection(projection);
var zoom = d3.zoom()
.scaleExtent([1, 1000])
.translateExtent([[0, 0], bounds])
.on("zoom", zoomed);
var svg = d3.select('body')
.append('svg')
.attr("width", bounds[0])
.attr("height", bounds[1])
.attr("viewbox", "0 0 " + bounds[0] + " " + bounds[1])
.call(zoom)
.append('g');
svg.append("g")
.append("path")
.datum(d3.geoGraticule())
.attr("stroke", "gray")
.attr("d", geoPath);
d3.range(0, 4).forEach(function (i) {
var size = (i + 1) * 10;
addVisibilityZone([-130 + size * 5, 50],
colorGenerator(i), [size, size]);
});
function zoomed() {
var t = d3.event.transform;
svg.attr("transform", t);
d3.selectAll("path").attr('stroke-width', 1/t.k);
}
function addVisibilityZone(angles, color, size) {
var xy = projection(angles);
var points = generateRect(100, 0, 0, size[0], size[1]);
var gnomonicProjection = d3.geoGnomonic().clipAngle(180)
.translate([size[0]/2, size[1]/2])
.scale(57); // this magic number is experimentally found
var g = svg.append("g");
var drag = d3.drag()
.on("start", dragged)
.on("drag", dragged);
var path = g.append("path")
.datum({
type: "Polygon",
coordinates: [[]],
})
.classed("zone", "true")
.attr("fill", color)
.attr("stroke", color)
.attr("fill-opacity", 0.3)
.call(drag);
update();
function dragged() {
g.raise();
xy = [d3.event.x, d3.event.y];
update()
}
function update() {
angles = projection.invert(xy);
gnomonicProjection.rotate([-angles[0], -angles[1]]);
path.datum().coordinates[0] = points.map(gnomonicProjection.invert);
path.attr('d', geoPath);
}
}
function generateRect(num, x, y, width, height) {
var count = Math.floor(num / 4) + 1;
var range = d3.range(count);
return range.map(function (i) { // top
return pt(i * width / count, 0);
}).concat(range.map(function (i) { // right
return pt(width, i * height / count);
})).concat(range.map(function (i) { // bottom
return pt(width - i * width / count, height);
})).concat(range.map(function (i) { // left
return pt(0, height - i * height / count);
}));
function pt(dx, dy) {
return [x + dx, y + dy];
}
}
* {
margin: 0;
overflow: hidden;
}
<script src="//d3js.org/d3.v5.min.js"></script>
Your approach looks correct for FOV on sphere visualization. It shouldn't be a rectangle in the result.
Here is an example:
As you can see the distorsion looks correct. It shouldn't be a rectangle.
Same for non equatorial target:

Stop zooming of bubbles in D3 World Map

I am currently working on a D3 world map in which I have brought in a zoom functionality up-to the boundary level of any country or county based on its click.
I have Added Bubbles pointing the counties in Kenya,which gets enlarged on the zoom functionality that I have added.But I want to stop the zooming of bubbles,on zooming of the Map.
Here is a plunker for my current work.
https://plnkr.co/edit/nZIlJxvU74k8Nmtpduzc?p=preview
And below is the code for zooming and zoom out
function clicked(d) {
var conditionalChange = d;
if(d.properties.hasOwnProperty("Country")){
var country = d.properties.Country;
var obj = data.objects.countries.geometries;
$.each(obj, function(key, value ) {
if(countries[key].properties.name == "Kenya")
{
conditionalChange = countries[key].geometry;
}
});
}
d = conditionalChange;
if (active.node() === this) return reset();
active.classed("active", false);
active = d3.select(this).classed("active", true);
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = 1.2/ Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
g.transition()
.duration(750)
.style("stroke-width", 1/ scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}
function reset() {
active.classed("active", false);
active = d3.select(null);
g.transition()
.duration(750)
.style("stroke-width", "1px")
.attr("transform", "");
}
You are scaling the entire g element, this effectively zooms the map. Everything will increase in size; however, for the map lines you have adjusted the stroke to reflect the change in g scale factor:
g.transition()
.duration(750)
.style("stroke-width", 1/ scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
To keep the circles the same size, you have to do the same adjustment for your circles by modifying the r attribute for each circle according to the g scale factor:
g.selectAll(".city-circle")
.transition()
.attr("r", 5 / scale )
.duration(750);
Though, since you don't actually apply the class city-circle on your circles you'll need to do that too when you append them:
.attr("class","city-circle")
And, just as you reset the stroke width on reset, you need to reset the circles' r :
g.selectAll(".city-circle")
.transition()
.attr("r", 5)
.duration(750);
Together that gives us this.

d3.js Animation not working for all the iterations of loop

I am building a d3js liquid fill visualization as mentioned in http://jsfiddle.net/zm5p9LLe/. The example shows one liquid fill gauge. I am looping in and creating multiple gauges. But, the animation works only for last iteration of the loop. Is this due to the common div id for a transition? do we have any alternates to make animation work in all the gauges?
HTML
<svg id="fillgauge1" width="97%" height="250"></svg>
CSS
.liquidFillGaugeText {
font-family: Helvetica;
font-weight: bold;
}
JAVASCRIPT
loadLiquidFillGauge("fillgauge1", 45.34);
function liquidFillGaugeDefaultSettings() {
return {
minValue: 0, // The gauge minimum value.
maxValue: 100, // The gauge maximum value.
circleThickness: 0.05, // The outer circle thickness as a percentage of it's radius.
circleFillGap: 0.05, // The size of the gap between the outer circle and wave circle as a percentage of the outer circles radius.
circleColor: "#178BCA", // The color of the outer circle.
waveHeight: 0.05, // The wave height as a percentage of the radius of the wave circle.
waveCount: 3, // The number of full waves per width of the wave circle.
waveRiseTime: 1000, // The amount of time in milliseconds for the wave to rise from 0 to it's final height.
waveAnimateTime: 1000, // The amount of time in milliseconds for a full wave to enter the wave circle.
waveRise: true, // Control if the wave should rise from 0 to it's full height, or start at it's full height.
waveHeightScaling: true, // Controls wave size scaling at low and high fill percentages. When true, wave height reaches it's maximum at 50% fill, and minimum at 0% and 100% fill. This helps to prevent the wave from making the wave circle from appear totally full or empty when near it's minimum or maximum fill.
waveAnimate: true, // Controls if the wave scrolls or is static.
waveColor: "#178BCA", // The color of the fill wave.
waveOffset: 0.25, // The amount to initially offset the wave. 0 = no offset. 1 = offset of one full wave.
textVertPosition: .8, // The height at which to display the percentage text withing the wave circle. 0 = bottom, 1 = top.
textSize: 0.6, // The relative height of the text to display in the wave circle. 1 = 50%
valueCountUp: true, // If true, the displayed value counts up from 0 to it's final value upon loading. If false, the final value is displayed.
displayPercent: true, // If true, a % symbol is displayed after the value.
textColor: "#045681", // The color of the value text when the wave does not overlap it.
waveTextColor: "#A4DBf8" // The color of the value text when the wave overlaps it.
};
}
function loadLiquidFillGauge(elementId, value, config) {
if (config == null) config = liquidFillGaugeDefaultSettings();
var gauge = d3.select("#" + elementId);
var radius = Math.min(parseInt(gauge.style("width")), parseInt(gauge.style("height"))) / 2;
var locationX = parseInt(gauge.style("width")) / 2 - radius;
var locationY = parseInt(gauge.style("height")) / 2 - radius;
var fillPercent = Math.max(config.minValue, Math.min(config.maxValue, value)) / config.maxValue;
var waveHeightScale;
if (config.waveHeightScaling) {
waveHeightScale = d3.scale.linear()
.range([0, config.waveHeight, 0])
.domain([0, 50, 100]);
} else {
waveHeightScale = d3.scale.linear()
.range([config.waveHeight, config.waveHeight])
.domain([0, 100]);
}
var textPixels = (config.textSize * radius / 2);
var textFinalValue = parseFloat(value).toFixed(2);
var textStartValue = config.valueCountUp ? config.minValue : textFinalValue;
var percentText = config.displayPercent ? "%" : "";
var circleThickness = config.circleThickness * radius;
var circleFillGap = config.circleFillGap * radius;
var fillCircleMargin = circleThickness + circleFillGap;
var fillCircleRadius = radius - fillCircleMargin;
var waveHeight = fillCircleRadius * waveHeightScale(fillPercent * 100);
var waveLength = fillCircleRadius * 2 / config.waveCount;
var waveClipCount = 1 + config.waveCount;
var waveClipWidth = waveLength * waveClipCount;
// Rounding functions so that the correct number of decimal places is always displayed as the value counts up.
var textRounder = function (value) {
return Math.round(value);
};
if (parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))) {
textRounder = function (value) {
return parseFloat(value).toFixed(1);
};
}
if (parseFloat(textFinalValue) != parseFloat(textRounder(textFinalValue))) {
textRounder = function (value) {
return parseFloat(value).toFixed(2);
};
}
// Data for building the clip wave area.
var data = [];
for (var i = 0; i <= 40 * waveClipCount; i++) {
data.push({
x: i / (40 * waveClipCount),
y: (i / (40))
});
}
// Scales for drawing the outer circle.
var gaugeCircleX = d3.scale.linear().range([0, 2 * Math.PI]).domain([0, 1]);
var gaugeCircleY = d3.scale.linear().range([0, radius]).domain([0, radius]);
// Scales for controlling the size of the clipping path.
var waveScaleX = d3.scale.linear().range([0, waveClipWidth]).domain([0, 1]);
var waveScaleY = d3.scale.linear().range([0, waveHeight]).domain([0, 1]);
// Scales for controlling the position of the clipping path.
var waveRiseScale = d3.scale.linear()
// The clipping area size is the height of the fill circle + the wave height, so we position the clip wave
// such that the it will won't overlap the fill circle at all when at 0%, and will totally cover the fill
// circle at 100%.
.range([(fillCircleMargin + fillCircleRadius * 2 + waveHeight), (fillCircleMargin - waveHeight)])
.domain([0, 1]);
var waveAnimateScale = d3.scale.linear()
.range([0, waveClipWidth - fillCircleRadius * 2]) // Push the clip area one full wave then snap back.
.domain([0, 1]);
// Scale for controlling the position of the text within the gauge.
var textRiseScaleY = d3.scale.linear()
.range([fillCircleMargin + fillCircleRadius * 2, (fillCircleMargin + textPixels * 0.7)])
.domain([0, 1]);
// Center the gauge within the parent SVG.
var gaugeGroup = gauge.append("g")
.attr('transform', 'translate(' + locationX + ',' + locationY + ')');
// Draw the outer circle.
var gaugeCircleArc = d3.svg.arc()
.startAngle(gaugeCircleX(0))
.endAngle(gaugeCircleX(1))
.outerRadius(gaugeCircleY(radius))
.innerRadius(gaugeCircleY(radius - circleThickness));
gaugeGroup.append("path")
.attr("d", gaugeCircleArc)
.style("fill", config.circleColor)
.attr('transform', 'translate(' + radius + ',' + radius + ')');
// Text where the wave does not overlap.
var text1 = gaugeGroup.append("text")
.text(textRounder(textStartValue) + percentText)
.attr("class", "liquidFillGaugeText")
.attr("text-anchor", "middle")
.attr("font-size", textPixels + "px")
.style("fill", config.textColor)
.attr('transform', 'translate(' + radius + ',' + textRiseScaleY(config.textVertPosition) + ')');
// The clipping wave area.
var clipArea = d3.svg.area()
.x(function (d) {
return waveScaleX(d.x);
})
.y0(function (d) {
return waveScaleY(Math.sin(Math.PI * 2 * config.waveOffset * -1 + Math.PI * 2 * (1 - config.waveCount) + d.y * 2 * Math.PI));
})
.y1(function (d) {
return (fillCircleRadius * 2 + waveHeight);
});
var waveGroup = gaugeGroup.append("defs")
.append("clipPath")
.attr("id", "clipWave" + elementId);
var wave = waveGroup.append("path")
.datum(data)
.attr("d", clipArea);
// The inner circle with the clipping wave attached.
var fillCircleGroup = gaugeGroup.append("g")
.attr("clip-path", "url(#clipWave" + elementId + ")");
fillCircleGroup.append("circle")
.attr("cx", radius)
.attr("cy", radius)
.attr("r", fillCircleRadius)
.style("fill", config.waveColor);
// Text where the wave does overlap.
var text2 = fillCircleGroup.append("text")
.text(textRounder(textStartValue) + percentText)
.attr("class", "liquidFillGaugeText")
.attr("text-anchor", "middle")
.attr("font-size", textPixels + "px")
.style("fill", config.waveTextColor)
.attr('transform', 'translate(' + radius + ',' + textRiseScaleY(config.textVertPosition) + ')');
// Make the value count up.
if (config.valueCountUp) {
var textTween = function () {
var i = d3.interpolate(this.textContent, textFinalValue);
return function (t) {
this.textContent = textRounder(i(t)) + percentText;
}
};
text1.transition()
.duration(config.waveRiseTime)
.tween("text", textTween);
text2.transition()
.duration(config.waveRiseTime)
.tween("text", textTween);
}
// Make the wave rise. wave and waveGroup are separate so that horizontal and vertical movement can be controlled independently.
var waveGroupXPosition = fillCircleMargin + fillCircleRadius * 2 - waveClipWidth;
if (config.waveRise) {
waveGroup.attr('transform', 'translate(' + waveGroupXPosition + ',' + waveRiseScale(0) + ')')
.transition()
.duration(config.waveRiseTime)
.attr('transform', 'translate(' + waveGroupXPosition + ',' + waveRiseScale(fillPercent) + ')')
.each("start", function () {
wave.attr('transform', 'translate(1,0)');
}); // This transform is necessary to get the clip wave positioned correctly when waveRise=true and waveAnimate=false. The wave will not position correctly without this, but it's not clear why this is actually necessary.
} else {
waveGroup.attr('transform', 'translate(' + waveGroupXPosition + ',' + waveRiseScale(fillPercent) + ')');
}
if (config.waveAnimate) animateWave();
function animateWave() {
wave.transition()
.duration(config.waveAnimateTime)
.ease("linear")
.attr('transform', 'translate(' + waveAnimateScale(1) + ',0)')
.each("end", function () {
wave.attr('transform', 'translate(' + waveAnimateScale(0) + ',0)');
animateWave(config.waveAnimateTime);
});
}
}

How to center (horizontal and vertical) text along an textPath inside an arc using d3.js?

After playing a while with d3.js and looking at a lot of examples, I was able to draw multiple arcs. Each one starting and ending at an specific degree and and given radius.
var dataset = {
"2":[{"degree1":0, "degree2":1.5707963267949,
"label":"Sample Text Test"},
{"degree1":1.5707963267949, "degree2":3.1415926535898,
"label":"Lorem ipsum sample text"},
{"degree1":3.1415926535898, "degree2":4.7123889803847,
"label":"Sample Text Text"},
{"degree1":4.7123889803847, "degree2":6.2831853071796,
"label":"Lorem ipsum"}],
"1":[{"degree1":0, "degree2":3.1415926535898,
"label":"Sample"},
{"degree1":3.1415926535898, "degree2":6.2831853071796,
"label":"Text"}],
"0":[{"degree1":0, "degree2":6.2831853071796,
"label":""}]
},
width = 450,
height = 450,
radius = 75;
// Helper methods
var innerRadius = function(d, i, j) {
return 1 + radius * j;
};
var outerRadius = function(d, i, j) {
return radius * (j + 1);
};
var startAngle = function(d, i, j) {
return d.data.degree1;
};
var endAngle = function(d, i, j) {
return d.data.degree2;
};
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(startAngle)
.endAngle(endAngle);
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width >> 1) + ',' + (height >> 1) + ')');
var level = svg.selectAll('g')
.data(function(d) {
return d3.values(dataset);
})
.enter()
.append('g');
var entry = level.selectAll('g')
.data(function(d, i) {
return pie(d);
})
.enter()
.append('g');
entry.append('path')
.attr('fill', '#aaa')
.attr('d', arc)
.attr('id', function(d, i, j) {
return 'arc' + i + '-' + j;
});
var label = entry.append('text')
.style('font-size', '20px')
.attr('dx', function(d, i, j) {
return Math.round((d.data.degree2 - d.data.degree1) * 180 / Math.PI);
})
.attr('dy', function(d, i, j) {
return ((radius * (j + 1)) - (1 + radius * j)) >> 1;
});
label.append('textPath')
.attr('xlink:href', function(d, i, j) {
return '#arc' + i + '-' + j;
})
.style('fill', '#000')
.text(function(d) {
return d.data.label;
});
See http://jsfiddle.net/3FP6P/2/ :
But some problem still exists:
How to center (horizonal und vertical) an text along an textpath of an any length inside an arc described by innerRadius, outerRadius, startAngle and endAngle?
The text occurs sometimes bold, sometimes not. Why?
The character spacing does not appear to be the same as if it is written inside a . Some letters stick more together as other ones. Why?
The letters are not located directly on the path. Some seem to have a little slip up or down. Why?
Vertical alignment
You can use another arc with radius (innerRadius + outerRadius) / 2 and use it as the textPath for the labels.
Note that even if you set the innerRadius == outerRadius, D3 will draw an path which moves clockwise and then anti-clockwise to doubles over itself. This becomes important while trying to figure out the horizontal centre of the path: it is at the 25% and 75% points while 0% and 50% points lie on the two tips of the arc.
Horizontal alignment
Use text-anchor: middle on the text element and set startOffset to 25% (or 75%) on the textPath.
Demo.
This is a more robust way than calculating the dx and dy by hand.
You should try out Lars's suggestions to further improve the quality and centring of the text, e.g. you might want to set text-rendering to optimizeLegibility and play with the baseline a bit.
Issues 2-4 are because of the font rendering. In my browser, the spacing and character size etc is consistent. You can try playing around with the text-rendering attribute to improve things.
To get the text centred, you will need to set the alignment-baseline and/or dominant-baseline attributes.
If this still doesn't give you the results you're looking for, try decreasing the font size. This may help because a slight rotation of a character will be less visible.

Categories