I'm generating a musical waveform using D3 by pushing rectangles next to one another. Here's the fiddle: http://jsfiddle.net/s4dML/
var data = [ 0.0534973, /* ...lots and lots of data... */ 0.290771];
data = data.filter(function(datum, index){
return index % 3 == 0;
});
var width = 340,
height = 70,
svg = d3
.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
svg
.selectAll('rect')
.data(data.map(function(datum){
return (datum * height)/2;
}))
// .data(dataset)
.enter()
.append('rect')
.attr('x', function(d, i){
return i * (width / data.length);
})
.attr('y', function(d){
return (height /2) - d ;
})
.attr('width', function(d, i){
return width / data.length;
})
.attr('height', function(d){
return d*2;
})
.attr('fill', 'teal');
Does anyone know why the result ins't single, flat color as expected? There is a kind of shimmering effect throughout. This might be desirable, but regardless I'd like to know how it got there and how to get rid of it if I'm so inclined.
This is an artifact of SVG rendering (or really, any vector graphics rendering). Suppose that you have two rects that meet 40% of the way into a pixel. Then the first rect will paint into that pixel with 40% opacity, and the second with 60% opacity, meaning that the pixel is only (40 + 0.6 * 60 =) 76% colored, even though logically it is 100% covered by colored shapes.
A fix for this is to define the graph as a single <path> object tracing out the top and bottom edges with no "cracks" like this between rects.
I'm not familiar with D3, but in ordinary Javascript:
var path = "M 0," + (height / 2);
for(var i = 0; i < data.length; i++) {
var x = (i + 1) * (width / data.length);
var y = height / 2 - (data[i] * height)/2;
path += " V " + y + " H " + x;
}
for(var i = data.length - 1; i >= 0; i--) {
var x = i * (width / data.length);
var y = height / 2 + (data[i] * height)/2;
path += " V " + y + " H " + x;
}
path += " Z";
Russell's answer is a good one, though you'll end up with a monstrous path. This shouldn't be too much of a problem.
I encountered the same problem the other day when trying to make a bar chart of about 500 data points using very thin bars. The advantage of doing so is that it is much easier to make a mouseover that highlights an individual bar. In cases like this, I find that you have to use integer values for the widths and x positions.
For your example, setting the width and the interval to 1 completely fixes the problem while only making it about 10 percent shorter:
http://jsfiddle.net/s4dML/1/
.attr('x', function(d, i){
return i;// * (width / data.length);
})
.attr('y', function(d){
return (height /2) - d ;
})
.attr('width', function(d, i){
return 1;
})
Of course, this not an extensible solution--just depends on your plans for the widget. I added a mouseover in the above example for demo purposes.
Related
I have drawn a spiral using this code:
var width = 1000,
height = 1000,
start = 0,
end = 2.25,
numSpirals = 78,
margin = {top:50,bottom:50,left:50,right:50};
// Constructing the spiral:
// theta for the spiral
var theta = function(r) {
return numSpirals * Math.PI * r;
};
// the r works out the space within which the spiral can take shape - the width and height is set above
var r = d3.min([width, height]) / 2 - 40 ;
// The radius of the spiral
var radius = d3.scaleLinear()
.domain([start, end])
.range([40, r]);
// inserts svg into the DOM
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.left + margin.right)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// The path to draw the spiral needs data to inform it, points generates this, and is used in .datum(points) below
var points = d3.range(start, end + 0.02, (end - start) / 2000);
// this is the spiral, utilising the theta and radius generated above
var spiral = d3.radialLine()
.curve(d3.curveCardinal)
.angle(theta)
.radius(radius);
var path = svg.append("path")
.datum(points)
.attr("id", "spiral")
.attr("d", spiral)
.style("fill", "none")
.style("stroke", "grey")
.style("stroke", ("6, 5"))
.style("opacity",0.5);
But, now I want to draw some lines on top of this spiral, following the existing path of the spiral. These lines will need to connect two different dates across two different columns within the array. I have tried a few approaches, such as d3.line but I can't get the lines to follow the spiral. I imagine I somehow need to reference the initial spiral? I am unsure how to proceed with this though.
This might be a simple question, but I have a map in d3 and I'd like to represent event-counts as squares.
Here's an example png of what I'm going for:
They're not aligned perfectly in the picture, but let's say I have a JSON:
[
{city:'New York', count:3},
{city:'Washington, D.C.', count:1},
{city:'Austin', count:5},
{city:'Havana', count:8}
]
of counts that I'd like to represent as squares, preferably clustered in an orderly way.
I'm scratching my head on this — I think maybe a force-directed graph will do the trick? I've also seen this: http://bl.ocks.org/XavierGimenez/8070956 and this: http://bl.ocks.org/mbostock/4063269 that might get me close.
For context and set-up (I don't need help making the map, but just to share), here's the repo I'm using for the project: https://github.com/alex2awesome/custom-map, which shows the old way I was representing counts (by radius of a circle centered on each city).
does someone at least know what this might be called?
The technical name of this in dataviz is pictogram.
Here is a general code for plotting the rectangles, you'll have to change some parts according to your needs. The most important part is the calculation for the rectangles x and y position, using the modulo operator.
First, let's set the initial position and the size of each rectangle. You'll have to set this according to your coordinates.
var positionX = 5;
var positionY = 5;
var size = 5;
Then, let's set how many rectangles you want (this, in your code, will be d.count):
var count = 15;
var gridSize = Math.ceil(Math.sqrt(count));
var data = d3.range(count);
Based on the count, we set the gridSize (just a square root) and the data.
Now we plot the rectangles:
var rects = svg.selectAll(".rects")
.data(data)
.enter()
.append("rect");
rects.attr("width", size)
.attr("height", size)
.attr("x", function(d,i){ return positionX + (i%gridSize)*(size*1.1)})
.attr("y", function(d,i){ return positionY + (Math.floor((i/gridSize)%gridSize))*(size*1.1) })
.attr("fill", "red");
Here is a working snippet, using 15 as count (4, 9, 16, 25 etc will give you a perfect square). Change count to see how it adapts:
var svg = d3.select("body")
.append("svg")
.attr("width", 50)
.attr("height", 50);
var count = 15;
var size = 5;
var positionX = 5;
var positionY = 5;
var gridSize = Math.ceil(Math.sqrt(count));
var data = d3.range(count);
var rects = svg.selectAll(".rects")
.data(data)
.enter()
.append("rect");
rects.attr("width", size)
.attr("height", size)
.attr("x", function(d,i){ return positionX + (i%gridSize)*(size*1.2)})
.attr("y", function(d,i){ return positionY + (Math.floor((i/gridSize)%gridSize))*(size*1.2) })
.attr("fill", "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I have a D3js map built with topojson.js.
var projection = d3.geo.mercator();
Everything works fine, but I am looking for an effect that I cannot manage to achieve. When, zooming the map I would like the pins over it to scale down, But if I scale it down, I need to recalculate their coordinates and I can't find the formula to do so.
Zoom handler
scale = d3.event.scale;
if (scale >= 1) {
main.attr("transform", "translate(" + d3.event.translate + ")
scale(" + d3.event.scale + ")");
}
else {
main.attr("transform", "translate(" + d3.event.translate + ")scale(1)");
}
//43x49 is the size initial pine size when scale = 1
var pins = main.select("#pins").selectAll("g").select("image")
.attr("width", function () {
return 43 - (43 * (scale - 1));
})
.attr("height", function () {
return 49 - (49 * (scale - 1));
})
.attr("x", function () {
//Calculate new image coordinates;
})
.attr("y", function () {
//Calculate new image coordinates;
});
My question is : how do I calculate x and y based on new scale?
I hope I am clear enough.
Thanks for your help
EDIT :
Calculation of initial pins coordinates :
"translate(" + (projection([d.lon, d.lat])[0] - 20) + ","
+ (projection([d.lon, d.lat])[1] - 45) + ")"
-20 and -45 to have the tip of the pin right on the target.
You need to "counter-scale" the pins, i.e. as main scales up/down you need to scale the pins down/up, in the opposite direction. The counter-scale factor is 1/scale, and you should apply it to each of the <g>s containing the pin image. This lets you remove your current width and height calculation from the <image> nodes (since the parent <g>'s scale will take care of it).
However, for this to work properly, you'll also need to remove the x and y attributes from the <image> and apply position via the counter-scaled parent <g> as well. This is necessary because if there's a local offset (which is the case when x and y are set on the <image>) that local offset gets scaled as the parent <g> is scaled, which makes the pin move to an incorrect location.
So:
var pinContainers = main.select("#pins").selectAll("g")
.attr("transform", function(d) {
var x = ... // don't know how you calculate x (since you didn't show it)
var y = ... // don't know how you calculate y
var unscale = 1/scale;
return "translate(" + x + " " + y + ") scale(" + unscale + ")";
})
pinContainers.select("image")
.attr("width", 43)
.attr("height", 49)
// you can use non-zero x and y to get the image to scale
// relative to some point other than the top-left of the
// image (such as the tip of the pin)
.attr("x", 0)
.attr("y", 0)
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.
In effort to learn D3.js, I took this example and made a plunker out of it
http://plnkr.co/edit/ynWB0GznvrMMRmqkaqaA?p=preview
Problem?
The data doesn't fit correctly and you can see that data is incomplete
How shall I fix it?
First thing is that your labels are incorrect. So change this:
.text(function(d) {
return 'd.data.age'; });
to this:
.text(function(d) {
return d.data.age; });
and you'll get the correct labels.
Second, the labels are getting cut off. This appears to be to do with the sizes specified for the pie chart. You can either increase the width specified (looks fine at 360 say):
var width = 360,
height = 300,
radius = Math.min(width, height) / 2;
Or bring the labels in closer to the graph:
.attr("transform", function(d) {
return "translate(" + ( (radius -50) * Math.sin( ((d.endAngle - d.startAngle) / 2) + d.startAngle ) ) + "," + ( -1 * (radius - 50) * Math.cos( ((d.endAngle - d.startAngle) / 2) + d.startAngle ) ) + ")"; })
Where it says radius - x determines how close or far the labels will appear to the graph. The bigger x is, the closer they will be.