I am working on this plnkr. I have three lines at angle 30, 45 and 60. I want to apply a brush on these lines so that when the chart is brushed the lines get redrawn at where it crossed the brushed rectangle with appropriate the values on the axis. Any help or hint to solve this problem is greatly appreciated.
EDIT: If you have different solutions to draw the rotated lines and brush on top of them it is welcomed too. Please help.
var ga = d3.select("svg")
.append("g")
.attr("class", "a axis")
.attr("transform", "translate(" + margin.left + "," + (height + margin.top) + ")")
.selectAll("g")
.data([30, 45, 60])
.enter()
.append("g")
.attr("class", "rotatedlines")
.attr("transform", function(d) { return "rotate(" + -d + ")"; })
.attr("stroke-width", 1)
.attr("stroke", "black")
.attr("stroke-dasharray", "5,5");
To explain my solution:
The fundamental steps to take are as follows:
update the domains of the x and y scales to the brush extent
redraw the axes
compute the scale factor and translation for the lines
scale and translate the line containers accordingly
reset the brush
Note that steps 3 and 4 are only necessary because you're not using the scales to draw everything -- a better approach would be to define two points for each line as the data that's bound to the elements and then use the scales to redraw. This would make the code simpler.
With your approach it's still possible though. In order to facilitate it, I've made a few modifications to your code -- in particular, I've cleaned up the various nested g elements with different translations and defined the lines through their x1, x2, y1, y2 attributes rather than through translation of the containers. Both of these changes make the functionality you want easier to implement as only a single transformation takes places that doesn't need to consider multiple other transformations. I've also nested the lines in multiple g elements so that they can be scaled and translated more easily.
The brush handler function now looks like this:
// update scales, redraw axes
var extent = brush.extent();
x.domain(brush.empty() ? x2.domain() : [ extent[0][0], extent[1][0] ]);
y.domain(brush.empty() ? y2.domain() : [ extent[0][1], extent[1][1] ]);
xAxisG.call(xAxis);
yAxisG.call(yAxis);
This code should be fairly self-explanatory -- the domains of the scales are updated according to the current extent of the brush and the axes are redrawn.
// compute and apply scaling and transformation of the g elements containing the lines
var sx = (x2.domain()[1] - x2.domain()[0])/(x.domain()[1] - x.domain()[0]),
sy = (y2.domain()[1] - y2.domain()[0])/(y.domain()[1] - y.domain()[0]),
dx = -x2(x.domain()[0]) - x2.range()[0],
dy = -y2(y.domain()[1]) - y2.range()[1];
d3.selectAll("g.container")
.attr("transform", "translate(" + [sx * dx, sy * dy] + ")scale(" + [sx, sy] + ")");
This is the tricky part -- based on the new domains of the scales, we need to compute the scale and translation for the lines. The scaling factors are simply the ratio of the old extent to the new extent (note that I have made copies of the scales that are not modified), i.e. a number greater than 1. The translation determines the shift of the (0,0) coordinate and is computed through the difference of the old (0,0) coordinate (I get this from the range of the original scales) and the position of the new domain origin according to the original scales.
When applying the translation and scale at the same time, we need to multiply the offsets with the scaling factors.
// reset brush
brush.clear();
d3.select(".brush").call(brush);
Finally, we clear the brush and reset it to get rid of the grey rectangle.
Complete demo here.
You can access the brush extent via d3.event.target.extent(). The flow for drawing the scale is this:
Set scale
Set axis
Draw axis
As soon as the brush is done, you have to modify the scale and then re-draw the axis according to the current x and y domain. Is that what you meant?
I cleaned up the code a bit and made a little demonstration: http://plnkr.co/edit/epKbXbcBR2MiwUOMlU5A?p=preview
Related
I've made effective D3 maps using rasters (d3.tile and map libraries) and vectors (TopoJSON in SVG shapes). But I hit a bug when I combine them.
I followed Mike Bostock's raster-and-vector examples, especially his "Raster & Vector III", which changes the transform and stroke width to update how the vectors are displayed.
My map almost works perfectly. However, upon loading, only the raster tiles are displayed; the vectors are invisible:
But as soon as I trigger the d3.zoom event (by panning or zooming), the vectors are displayed:
I don't understand this, because I explicitly tell the browser, independently of the zoom event, to draw the vectors. This is the relevant snippet:
// read in the topojson
d3.json("ausElectorates.json", function(error, mapData) {
if (error) throw error;
var electorates = topojson.feature(mapData, mapData.objects.tracts);
// apply a zoom transform equivalent to projection{scale,translate,center}
map.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(mapWidth / 2, mapHeight / 2)
.scale(1 << 12)
.translate(-centre[0], -centre[1]));
// draw the electorate vectors
vector.selectAll("path")
.data(electorates.features)
.enter().append("path")
.attr("class", "electorate")
.attr("d", path);
});
For some reason, that last line of the d3.json() function -- .attr("d", path") -- isn't visualising the vectors.
Click here to see the map. Click here to access the full code and the TopoJSON it uses.
I'd love advice on this one, which is baffling me!
(PS Apologies for omitting copyright attributions for the map tiles, D3.js library, etc - I'm just trying to minimise the code for this example.)
It is drawing the vectors - however, you can't rely on solely scaling and translating your vector with the d3 geoProjection as when you zoom you apply the translate and scale to the path itself - not the projection:
vector.selectAll("path")
.attr("transform", "translate(" + [change.x, change.y] + ")scale(" + change.k + ")")
.style("stroke-width", 1 / change.k);
Since you don't set scale and translate, when loading your vectors they just aren't drawn correctly. They are drawn very small - as your projection scale is 1/tau, with a translation of [0,0]. Inspecting the svg on page load shows that they are there, and they are tiny.
The solution is to draw your vectors prior to map.call("zoom") - this way you can apply the base transform (center, transform, and scale) on the path before manually zooming:
// read in the topojson
d3.json("ausElectorates.json", function(error, mapData) {
if (error) throw error;
var electorates = topojson.feature(mapData, mapData.objects.tracts);
// draw the electorate vectors
vector.selectAll("path")
.data(electorates.features)
.enter().append("path")
.attr("class", "electorate")
.attr("d", path);
// apply a zoom transform equivalent to projection{scale,translate,center}
map.call(zoom)
.call(zoom.transform, d3.zoomIdentity
.translate(mapWidth / 2, mapHeight / 2)
.scale(1 << 12)
.translate(-centre[0], -centre[1]));
});
I'm currently attempting to build a a multi-line graph with a d3.time.scale() for the x-axis.
I'm trying to add circles to each point on lines of the graph, but have been unsuccessful thus far.
When I do something like:
.attr('cx', function(d){ return x(d.price) })
I get a negative number.
I was thinking of setting up another scale (pointsScale) to handle this but have been largely unsuccessful.
What am I doing wrong?
Please refer to my JSBin for the code.
You're running into a few issues here:
Since you made the x-axis a time-scale, I'm guessing that you actually want price to be the y variable, while date is the x variable. That's why x(d.price) is negative - d3 is trying to interpret the prices as dates, which doesn't end up making much sense. So replace your line of code above with this: .attr('cy', function(d){ return y(d.price) })
In order to actually have circles be visible, they need to have three parameters set: cx, cy, and r. Since d3 already knows that your x axis is a time scale, you can set cx with .attr('cx', function(d){ return x(d.date) }). You can make r be whatever radius you want for the circles. Just choose one, or it will default to 0 and you won't be able to see the circles. .attr('r', 4), for instance, would set the radius to a perfectly visible value of 4.
You're drawing the circles before you draw the lines. As a result, the lines get drawn over the circles and it looks kind of weird. So move the circle code to after the line code if you want to avoid that.
Putting it all together, this is roughly what the code to create your circles should look like, and it should go after you declare var paths:
var circles = company.selectAll('circle')
.data(function(d){ return d.values; })
.enter().append('circle')
.attr('cy', function(d){
return y(d.price);}) //Price is the y variable, not the x
.attr('cx', function(d){
return x(d.date);}) //You also need an x variable
.attr('r',4); //And a radius - otherwise your circles have
//radius 0 and you can't see them!
Updated jsbin:
http://jsbin.com/gorukojoxu/edit?html,console,output
I haven't found any examples of this so far, but I was hoping the readers at Stack have run into this situation before.
We have a Y axis where the range from 0-200 isn't very interesting. We'd like to "squish" this area of the y axis and have the gap become larger as the Y axis values grow so that the top end of the spectrum shows a lot more movement in the line graph.
This would mean that the ticks on the Y axis would grow as they went up in range and allow the graph to show larger differences near the top end of the spectrum.
I can see that Y doesnt take a function, but has anyone done something like this and if so does anyone have any working examples?
If its not possible with D3 has anyone done something similar with highcharts?
NOTE
Please make sure that the ideas in the graph are still clear if you use such a scale. Be aware that the lines are only built based on start and end point and are not "kinked" when traversing the break in the axis.
When writing the answer I wasn't aware that you have a line graph and values below 200.
ANSWER
Assuming that you want to use a linear scale, you could e.g. try the following:
newScale = d3.scale.linear().domain([0,200,1000]).range([0,100,1000])
An axis could e.g. look like this:
var xAxis = d3.svg.axis()
.scale(newScale)
.orient('bottom');
d3.select("#someSVG").append("g")
.attr("class", "x axis")
.attr("transform", "translate(0, " + 123 + ')')
.call(xAxis);
Consequently, you'd get:
Looking at your codepen, you might want to try:
var y = d3.scale.linear().range([height,height * 9 / 10, 0]);
and then use as domain:
y.domain([0, 200, d3.max(data, function (d) {
return d.close;
})]);
so in this example, you allocate 1/10th of the height to the first 200.
I am trying to use the following code taken from this example to pan my time-series line-chart sideways, as new data is added to the graph:
d3.select("path")
.datum(globalData)
.attr("class", "line")
.attr("d", valueline(globalData))
.transition()
.ease("linear")
.attr("transform", "translate(" + x(-dx) + ")");
But this does't work - my line disappears. I am trying to understand what the units of the translate vector need to be for this to work. In my case dx would be the difference between the first and last x-values of the new data.
The units used in translate are screen pixels in the default coordinate system.
I was wondering if there is a way to create a force directed layout with d3.js and restrict it by an arbitrary shape in such a way that
all the nodes are equivalently distributed within the shape and
the distance between the border and the nodes is equally to the distance between the nodes
I hope there is already such a solution out there. Otherwise my idea is to start with the force directed layout and check the distances from the nodes to the borders in each iteration. Any suggestions from yourside?
Your idea is mine too. In the tick function you could add additional forces. This is my suggestion (not tested):
force.on('tick', function(e) {
node
.each(calcBorderDistance)
.attr('transform', function(d) {
d.x -= e.alpha*(1/Math.pow(d.borderDistance.x,2);
d.y -= e.alpha*(1/Math.pow(d.borderDistance.y,2);
return 'translate(' + d.x + ',' + d.y + ')'; // Move node
});
});
function calcBorderdistance(d) {
// Insert code here to calculate the distance to the nearest border of your shape
var x = ..., y = ...;
d.borderDistance = {'x':x,'y':y};
}
I have the inverse quadratic distance to the nearest border function loosely based on the formulas in excelent paper Drawing Graphs Nicely using Simulated Annealing. Following picture illustrates how methods from this paper affect drawing nodes bounded by a box:
And this picture illustrate case with different constraints, involving links between nodes: