Given the following speed dial, which is constructed using arcs in D3:
segmentArc = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth).startAngle(arcStartRad + startPadRad).endAngle(arcEndRad - endPadRad);
How do I move the labels in each segment so that it appears right justified (at the end of each segment opposed to center)?
the labels are currently added likes this:
chart.append('text')
.attr('transform', () => {
var x = Math.round(segmentArc.centroid()[0]);
var y = Math.round(segmentArc.centroid()[1]);
return 'translate(' + x + ',' + y + ')';
})
.style("text-anchor", "middle")
.text(sectionLabel);
I solved this problem by replicating each segment arc, giving it a transparent fill and making it exactly twice as long. From there it is as simple as working out the centroid for the transparent arc.
Related
I need to display a D3 map with a topological / shaded relief background. All user functionalities need to be implemented (e.g. zoom and panning)
So far, I have layered the map over a PNG that has the topology. I then did some hacking around with the projection to align the PNG border with the map borders. I then allow the user to zoom the PNG (eg: http://bl.ocks.org/pbogden/7363519). The result is actually very good. When I pan and zoom the map moves with the PNG which is great (image below):
The problem is that the PNG is very heavy (20MB), and the whole resulting experience is seriously buggy to the point that is is unusable. Results are obviously use a lower resolution image, but then the topology looks crap when the user zooms in. I tried converting the PNG to JPG ... which was actually worse!
What would be the best solution to achieve my goal in D3? Initial thoughts are as follows:
(1) The d3.geo.tile plugin (http://bl.ocks.org/mbostock/4132797). The difficulty here is that I would need to create my own tiles from my PNG image. Is this a promising avenue? Would I be able to layer a D3 map on top of that? I cannot find an example with custom tiles.
(2) I've seen this successful implementation of OpenSeaDragon and D3 (http://bl.ocks.org/zloysmiertniy/0ab009ca832e7e0518e585bfa9a7ad59). The issue here is that I am not sure whether it'll be possible to implement the desired D3 functionalities (zoom, pan, transitions) such that the D3 map and the underlying image move simultaneously.
(3) Any other thoughts or ideas?
To turn an image into tiles you'll need to have a georeferenced image - or be able to georeference the image yourself. As I believe you are using a natural earth dataset to create this image, you could use the source tif file and work with this. I use tile mill generally for my tiles (with some python) and it is fairly straightforward. You would not be able to use your png as is for tiles.
However, creating at tile set is unnecessary if you are looking for a hillshade or some sort of elevation/terrain texture indication. Using a leaflet example here, you can find quite a few tile providers, the ESRI.WorldShadedRelieve looks likes it fits the bill. Here's a demo with it pulled into d3 with a topojson feature drawn ontop:
var pi = Math.PI,
tau = 2 * pi;
var width = 960;
height = 500;
// Initialize the projection to fit the world in a 1×1 square centered at the origin.
var projection = d3.geoMercator()
.scale(1 / tau)
.translate([0, 0]);
var path = d3.geoPath()
.projection(projection);
var tile = d3.tile()
.size([width, height]);
var zoom = d3.zoom()
.scaleExtent([1 << 11, 1 << 14])
.on("zoom", zoomed);
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height);
var raster = svg.append("g");
var vector = svg.append("g");
// Compute the projected initial center.
var center = projection([-98.5, 39.5]);
d3.json("https://unpkg.com/world-atlas#1/world/110m.json",function(error,data) {
vector.append("path")
.datum(topojson.feature(data,data.objects.land))
.attr("stroke","black")
.attr("stroke-width",2)
.attr("fill","none")
.attr("d",path)
// Apply a zoom transform equivalent to projection.{scale,translate,center}.
svg
.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(width / 2, height / 2)
.scale(1 << 12)
.translate(-center[0], -center[1]));
})
function zoomed() {
var transform = d3.event.transform;
var tiles = tile
.scale(transform.k)
.translate([transform.x, transform.y])
();
projection
.scale(transform.k / tau)
.translate([transform.x, transform.y]);
var image = raster
.attr("transform", stringify(tiles.scale, tiles.translate))
.selectAll("image")
.data(tiles, function(d) {
return d;
});
image.exit().remove();
// enter:
var entered = image.enter().append("image");
// update:
image = entered.merge(image)
.attr('xlink:href', function(d) {
return 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/' + d.z + '/' + d.y + '/' + d.x + '.png';
})
.attr('x', function(d) {
return d.x * 256;
})
.attr('y', function(d) {
return d.y * 256;
})
.attr("width", 256)
.attr("height", 256);
vector.selectAll("path")
.attr("transform", "translate(" + [transform.x, transform.y] + ")scale(" + transform.k + ")")
.style("stroke-width", 1 / transform.k);
}
function stringify(scale, translate) {
var k = scale / 256,
r = scale % 1 ? Number : Math.round;
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
body { margin: 0; }
<svg></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3-tile#0.0.4/build/d3-tile.js"></script>
<script src="https://unpkg.com/topojson-client#3"></script>
You could certainly use OpenSeadragon for this. You'd want to turn the image into tiles; you don't need a specialized server for it... there are a number of standalone scripts you can use:
http://openseadragon.github.io/examples/creating-zooming-images/
Once you have that, OpenSeadragon handles the zooming and panning for you.
To overlay SVG so that it matches the zooming and panning, use the SVG overlay plugin:
https://github.com/openseadragon/svg-overlay
It works great with SVG produced by D3.
One thing to be aware of is that OpenSeadragon does not have any geo-specific functionality, so you'll position the overlay in image pixels rather than latitude/longitude.
BTW, OpenSeadragon can also work with non-tiled images, so if you want to give it a test before tiling your image, that's no problem. You'll just want to tile your image before production so you're not sending 20mb to your users.
Drag and Drop Example
I am trying to rewrite part of this example above to use in my code, specifically this piece:
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
However I am getting stuck since the v4 package has changed quite a bit. I wrote my zoomListener function to be
var zoomListener = d3.zoom()
.scaleExtent([0.3,2])
.on("zoom", zoomed);
function zoomed() {
transform = d3.event.transform;
console.log(d3.event);
svg.attr("transform", transform);
}
function centerNode(source){
t = transform;
console.log(t);
x = t.x*t.k; //I only want things to be centered vertically
y = (t.y + -source.x0)*t.k + (viewerHeight)/2 ;
svg.transition()
.duration(duration)
.attr("transform","translate(" + x + "," + y +")scale(" + t.k + ")");
transform.scale(t.k); //DOES NOT WORK
transform.translate([x, y]); //DOES NOT WORK
}
and I know that according to the doc things have changed and info are no longer are stored on what would be my zoomListener
D3 V4 release note on zoom I guess I am just confused on how I am suppose to do it with the new version. The last few lines of my centerNode function don't work which has for effect that when I center the node the zooming and panning reset...
Any suggestion?
So after much digging and trial and error I cam up with an answer that works pretty well for my purposes. Note that this code below is only the relevant part of my code not the whole code, certain variable were self explanatory so did not include them. ALSO THIS IS IN VERSION 4 of d3.js.
var zoom = d3.zoom()
.scaleExtent([0.3,2])
.on("zoom", zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight);
var zoomer = svg.append("rect")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom);
var g = svg.append("g");
zoomer.call(zoom.transform, d3.zoomIdentity.translate(150,0)); //This is to pad my svg by a 150px on the left hand side
function zoomed() {
g.attr("transform", d3.event.transform);//The zoom and panning is affecting my G element which is a child of SVG
}
function centerNode(source){
t = d3.zoomTransform(zoomer.node());
console.log(t);
x = t.x;
y = source.x0;
y = -y *t.k + viewerHeight / 2;
g.transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + t.k + ")")
.on("end", function(){ zoomer.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(t.k))});
}
As per the examples for v4 on the d3.js page, I used a rectangle to apply the zoom to
The zoom behavior is applied to an invisible rect overlaying the SVG
element; this ensures that it receives input, and that the pointer
coordinates are not affected by the zoom behavior’s transform. Pan & Zoom Example
In the Center node function I am using d3.zoomTransform(zoomer.node()); to get the current transform applied to the page.
The purpose of this function is only to center the collapsible tree vertically not horizontally, so I am keeping the current transform.x (here t.x) the same.
The coordinate in my svg are flip hence why y= source.x0, source is a what node was clicked in my collapsible tree. ("Look to the example referenced to the top of this thread to understand what I am trying to convert to version 4)
I am apply the transformation to my G element and then I want to commit those changes to the zoom transform, to do so I use the .on("end", function(){}) otherwise it was doing weird behavior with the transition, by doing that all it does is setting the current state of the transform.
zoomer.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(t.k))
This line above is applying a translation of x and y and a scale -- that is equal to what the current state -- to the identiy matrix has to get a new transform for G, i then apply it to zoomer which is the element I called zoom on earlier.
This worked like a charm for me!
Calling transform.scale and transform.translate returns a new transform, and modifies nothing. Therefore:
transform = transform.translate([x, y]).scale(k)
svg.call(zoomListener.transform, newTransform)
(At this point zoomListener is a pretty inaccurate name for this, but regardless...)
k, x, and y can be derived from source, maybe as you show, but I'm not sure, because I don't know what source is. But to me, t.x*t.k looks suspicious, because it's multiplying the existing transforms x by its scale. Seems like it would cause a feedback loop.
For more into about the zoom in v4, check out this related StackOverflow post, or this example by mbostock demonstrating programmatic control over the zoom transform of an element (canvas in this case) and includes transitions.
Right now in my program I have both a drag function and a zoom function.
The problem can best be illustrated in this minimal test case. When the zoom is initiated at first, you may notice that the points in the graph will transition just fine. But, if you drag a point before initiating a zoom, that point will usually travel outside the scope of the graph.
This problem seems to be happening because my coordinates are defined in scaled space centered around the upper left corner. However, after an object is dragged, its coordinates are switched to pixel space, centered around its original coordinates. When a zoom occurs, it will then treat the pixel coordinates as scaled space coordinates relative to the upper left corner, which causes problems.
I would appreciate any tips or pointers and thanks in advance
Drag function:
function dragmove(d) {
var barz = document.querySelector("#visual");
var point = d3.mouse(barz),
tempP = {
x: point[0],
y: point[1]
};
if (this.nodeName === "circle") {
d3.event.sourceEvent.stopPropagation();
var useZoom = $('#zoom').is(":checked");
if (useZoom == false) {
d.usePixels = 1;
d3.select(this).attr("transform", "translate(" + (d.x = tempP.x - xRange(d.initx)) + "," + (d.y = tempP.y - yRange(d.inity)) + ")");
//events to update line to fit dots
updateXs();
redoLine();
updateBars(canvas);
}
} }
Zoom function:
function zoomOut() {
//update axis
yRange.domain([d3.min(lineData, function (d) {
return d.y - 10;
}), d3.max(lineData, function (d) {
return d.y + 10;
})])
yAxisGroup.transition().call(yAxis);
xAxisGroup.transition().attr("transform", "translate(0," + yRange(0) + ")");
//update line
d3.select(".myLine").transition()
.attr("d", lineFunc(lineData));
var c = vis.selectAll("circle")
c.transition()
.attr(circleAttrs)
}
I'm using Leaflet 0.7.1 and want to draw a radial menu (like openstreetmap's iD editor) on top using d3.
Some examples I found use Leaflet's overlayPane to append the svg element to:
var svgContainer= d3.select(map.getPanes().overlayPane).append("svg");
On mouseclick, I add the menu and reposition it to the screen xy coords:
map.on('contextmenu', function(e) {
console.log('contextmenu()');
var tooltip;
var center = [e.layerPoint.x, e.layerPoint.y];
var menu = svgContainer.append('g')
.attr("class", 'leaflet-zoom-hide')
.attr('class', 'radial-menu')
.attr('transform', 'translate(' + center + ')')
.attr('opacity', 0);
menu.transition()
.attr('opacity', 1);
menu.append('path')
.attr('class', 'radial-menu-background')
.attr('d', 'M' + r * Math.sin(a0) + ',' +
r * Math.cos(a0) +
' A' + r + ',' + r + ' 0 ' + 0 + ',0 ' +
(r * Math.sin(a1) + 1e-3) + ',' +
(r * Math.cos(a1) + 1e-3)) // Force positive-length path (#1305)
.attr('stroke-width', 50)
.attr('stroke-linecap', 'round');
});
Somehow, the SVG paths get drawn (this is visible in the Chrome Inspector) but are behind the map object.
When I 'drag' the SVG element just below the body tag in Inspector, I can see the circle paths.
Any ideas on how I can draw SVG 'menu' elements on top of Leaflet?
Thanks!!
See this fiddle for a demo. Right click or hold to add an invisible element.
ps. I asked this question on gis.stackexchange.com, too.
Solved. A colleague pointed out that the SVG container had no dimensions. Giving it the appropriate width and height proved to be the answer:
svgContainer.attr("width", width).attr("height", height)
I have been struggling with this issue for the past couple days: I have a force directed graph that labels its edges just like this example does it. The problem I am facing is that when the graph updates (ie: a node on the graph is added upon a user's click) it updates the graph but it leaves the old edge labels that I wrote previously behind:
BEFORE & AFTER A NEW GRAPH IS APPENDED:
As you can see, my edge labels are hanging around after an update. I have a function that is called everytime new data comes in, and in this function I have the following code that draws the labels:
path_text = svg.selectAll(".path")
.data(force.links(), function(d){ return d.name;})
.enter().append("svg:g");
path_text.append("svg:text")
.attr("class","path-text")
.text(function(d) { return d.data.label; });
The svg variable is declared once at a top level closure like so:
var svg = d3.select("body").append("svg:svg")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMidYMid meet");
My graph has a tick() function that calculates the location of each label like so:
function tick()
{
// Line label
path_text.attr("transform", function(d)
{
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y);
var dr = Math.sqrt(dx * dx + dy * dy);
var sinus = dy/dr;
var cosinus = dx/dr;
var l = d.data.label.length * 6;
var offset = (1 - (l / dr )) / 2;
var x=(d.source.x + dx*offset);
var y=(d.source.y + dy*offset);
return "translate(" + x + "," + y + ") matrix("+cosinus+", "+sinus+",
"+-sinus+", "+cosinus+", 0 , 0)";
});
.
.
.
I have tried moving this svg declaration down into the update function, so that this is instantiated each time there is a graph change. This actually works - but it makes an entire duplicate of the entire graph. The first, original copy still keeps the old labels - but the second copy acts exactly how I want it to. Is there a way, perhaps, instead of appending svg, there is a way of replacing? I have also tried calling exit().remove() without any luck as well.
Thank you so much for your time. This has been killing me as to how I'm supposed to do this.
I placed the svg declaration inside my graph update function, attached it to a div, and clear the div before appending it again:
jQuery('#v').empty();
var svg = d3.select("#v").append("svg:svg")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMidYMid meet");
Not the cleanest solution in my opinion, but will go with this unless you all have a better solution!