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>
Related
I am trying to make a d3 javascript that creates a rectangle whose color depends on a data set. All of the rectangles are adjacent to each other like:
[][][][][][]
[][][][][][]
I got my script to work to create rectangles for all of my data, but it overflows like:
[][][][][][][][][][][][][][][][][][][][][][][][][][][]
How can I create width and height properties for my d3 script so it looks more like
[][][][]
[][][][]
[][][][]
Here is my script:
<script>
//for whatever data set
var data = [];
//Make an SVG Container
var svgContainer = d3.select("body").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("width", 38)
.attr("height", 25);
//Draw the rectangle
var rectangle = svgContainer.append("rect")
.attr("x", 0)
.attr("y", 5)
.attr("width", 38)
.attr("height", 25);
</script>
You have to change the x and y properties.
.attr("x", function(d, i) {
return 5 + (i%itemPerLine) * widthRect;
})
.attr("y", function(d, i) {
return 5 + Math.floor(i/itemPerLine) * heightRect;
})
(itemPerLine is the number of rect per line)
See this fiddle as example
I have a very basic D3 SVG which essentially consists of a couple arcs.
No matter what I use (attr, attrTween, and call) I cannot seem to get the datum via the first argument of the callback--it is always coming back null (I presume it's some kind of parse error, even though the path renders correctly?)
I might be overlooking something basic as I am relatively new to the library...
var el = $('#graph'),
width = 280,
height = 280,
twoPi = Math.PI * 2,
total = 0;
var arc = d3.svg.arc()
.startAngle(0)
.innerRadius(110)
.outerRadius(130),
svg = d3.select('#graph').append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"),
meter = svg.append('g').attr('class', 'progress');
/* Add Meter Background */
meter.append('path')
.attr('class', 'background')
.attr('d', arc.endAngle(twoPi))
.attr('transform', 'rotate(180)');
/* Add in Icon */
meter.append('text')
.attr('text-anchor', 'middle')
.attr('class', 'fa fa-user')
.attr('y',30)
.text('')
/* Create Meter Progress */
var percentage = 0.4,
foreground = meter.append('path').attr('class', 'foreground')
.attr('transform', 'rotate(180)')
.attr('d', arc.endAngle(twoPi*percentage)),
setAngle = function(transition, newAngle) {
transition.attrTween('d', function(d,v,i) {
console.log(d,v,i)
});
/*transition.attrTween('d', function(d) { console.log(this)
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) { d.endAngle = interpolate(t); return arc(d); };
});*/
};
setTimeout(function() {
percentage = 0.8;
foreground.transition().call(setAngle, percentage*twoPi);
},2000);
It's this block of code that seems to be problematic:
transition.attrTween('d', function(d,v,i) {
console.log(d,v,i)
});
Returning:
undefined 0 "M7.959941299845452e-15,-130A130,130 0 0,1 76.4120827980215,105.17220926874317L64.65637775217205,88.99186938124421A110,110 0 0,0 6.735334946023075e-15,-110Z"
I tried using the interpolator to parse the i value as a string since I cannot seem to acquire "d," however that had a parsing error returning a d attribute with multiple NaN.
This all seems very strange seeing as it's a simple path calculated from an arc???
The first argument of basically all callbacks in D3 (d here) is the data element that is bound to the DOM element you're operating on. In your case, no data is bound to anything and therefore d is undefined.
I've updated your jsfiddle here to animate the transition and be more like the pie chart examples. The percentage to show is bound to the path as the datum. Then all you need to do is bind new data and create the tween in the same way as for any of the pie chart examples:
meter.select("path.foreground").datum(percentage)
.transition().delay(2000).duration(750)
.attrTween('d', function(d) {
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc.endAngle(twoPi*interpolate(t))();
};
});
I have a bar chart where I want to make the gap more pronounced between the 6th and the bar in my chart and the 12th and 13th bar in my chart. Right now I'm using .rangeRoundBands which results in even padding and there doesn't seem to be a way to override that for specific rectangles (I tried appending padding and margins to that particular rectangle with no success).
Here's a jsfiddle of the graph
And my code for generating the bands and the bars themselves:
var yScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([padding, h- padding], 0.05);
svg.selectAll("rect.bars")
.data(dataset)
.enter()
.append("rect")
.attr("class", "bars")
.attr("x", 0 + padding)
.attr("y", function(d, i){
return yScale(i);
})
.attr("width", function(d) {
return xScale(d.values[0]);
})
.attr("height", yScale.rangeBand())
You can provide a function to calculate the height based on data and index. That is, you could use something like
.attr("height", function(d,i) {
if(i == 5) {
return 5;
}
return yScale.rangeBand();
})
to make the 6th bar 5 pixels high. You can of course base this value on yScale.rangeBand(), i.e. subtract a certain number to make the gap wider.
Here's a function for D3 v6 that takes a band scale and returns a scale with gaps.
// Create a new scale from a band scale, with gaps between groups of items
//
// Parameters:
// scale: a band scale
// where: how many items should be before each gap?
// gapSize: gap size as a fraction of scale.size()
function scaleWithGaps(scale, where, gapSize) {
scale = scale.copy();
var offsets = {};
var i = 0;
var offset = -(scale.step() * gapSize * where.length) / 2;
scale.domain().forEach((d, j) => {
if (j == where[i]) {
offset += scale.step() * gapSize;
++i;
}
offsets[d] = offset;
});
var newScale = value => scale(value) + offsets[value];
// Give the new scale the methods of the original scale
for (var key in scale) {
newScale[key] = scale[key];
}
newScale.copy = function() {
return scaleWithGaps(scale, where, gapSize);
};
return newScale;
}
To use this, first create a band scale...
let y_ = d3
.scaleBand()
.domain(data.map(d => d.name))
.range([margin.left, width - margin.right])
.paddingInner(0.1)
.paddingOuter(0.5)
... then call scaleWithGaps() on it:
y = scaleWithGaps(y_, [1, 5], .5)
You can create a bar chart in the normal way with this scale.
Here is an example on Observable.
I need to show value at corresponding place while mouseover a line/bar chart using d3.js
var toolTip = svg.selectAll("path")
.append("svg:title")
.text(getmouseoverdata(data)
);
function getmouseoverdata(d) {
return d;
}
Here i get all the data in the array while mouseover at any place in the graph.
But I want to show the value at corresponding place. How can I achieve it?
You can use d3js mouse event handler like this
var coordinates = [0, 0];
coordinates = d3.mouse(this);
var x = coordinates[0];
var y = coordinates[1];
It will provide you the current mouse coordinates.
If you're only looking to display the data elements when you mouseover the path/rect elements, you could try to add the titles directly onto those elements while they are being created?
For example:
var data = [0, 1, 2, 3, 4, 5],
size = 300;
var canvas = d3.select("body")
.append("svg:svg")
.attr("width", size)
.attr("height", size);
var pixels = size / data.length;
canvas.selectAll("rect").data(data).enter().append("svg:rect")
.attr("fill", "red")
.attr("height", function(d){return d * pixels})
.attr("width", pixels/2)
.attr("x", function(d,i){return i * pixels})
.attr("y", 0)
.append("title") //Adding the title element to the rectangles.
.text(function(d){return d});
This code should create five rectangles with their data element present in a tooltip if you mouseover the rectangle.
Hope this helps.
Edit based on comment:
For a bottom to top graph, you can change the y attribute like so:
.attr("y", function(d){return size - d * pixels})
This addition will cause the bar to start at the difference between the maxHeight of your graph and the size of the bar, effectively turning a top-to-bottom graph into a bottom-to-top graph.
Is there a way to limit the size of a brush, even though the extent is larger?
I put together a brush with only an x-scale that can be moved and resized. I would like to be able to limit the extent to which it can be resized by the user (basically only up to a certain point).
In the following example, the brush function stops updating when the brush gets bigger than half the maximum extent. The brush itself, though, can still be extended. Is there a way to prevent this from happening? Or is there a better way of handling this?
Many thanks!
See this code in action here: http://bl.ocks.org/3691274 (EDIT: This demo now works)
bar = function(range) {
var x_range = d3.scale.linear()
.domain([0, range.length])
.range([0, width]);
svg.selectAll("rect.items").remove();
svg.selectAll("rect.items")
.data(range)
.enter().append("svg:rect")
.attr("class", "items")
.attr("x", function(d, i) {return x_range(i);})
.attr("y", 0)
.attr("width", width/range.length-2)
.attr("height", 100)
.attr("fill", function(d) {return d})
.attr("title", function(d) {return d});
}
var start = 21;
bar(data.slice(0, start), true);
var control_x_range = d3.scale.linear()
.domain([0, data.length])
.range([0, width]);
controlBar = svg.selectAll("rect.itemsControl")
.data(data)
.enter().append("svg:rect")
.attr("class", "itemsControl")
.attr("x", function(d, i) {return control_x_range(i);})
.attr("y", 110)
.attr("width", width/data.length-2)
.attr("height", 20)
.attr("fill", function(d) {return d});
svg.append("g")
.attr("class", "brush")
.call(d3.svg.brush().x(d3.scale.linear().range([0, width]))
.extent([0,1*start/data.length])
.on("brush", brush))
.selectAll("rect")
.attr("y", 110)
.attr("height", 20);
function brush() {
var s = d3.event.target.extent();
if (s[1]-s[0] < 0.5) {
var start = Math.round((data.length-1)*s[0]);
var end = Math.round((data.length-1)*s[1]);
bar(data.slice(start,end));
};
}
I eventually solved it by redrawing the brush to its maximum allowed size when the size is exceeded:
function brush() {
var s = d3.event.target.extent();
if (s[1]-s[0] < 0.5) {
var start = Math.round((data.length-1)*s[0]);
var end = Math.round((data.length-1)*s[1]);
bar(data.slice(start,end));
}
else {d3.event.target.extent([s[0],s[0]+0.5]); d3.event.target(d3.select(this));}
}
Demo: http://bl.ocks.org/3691274
I'm still interested in reading better solutions.
Here's another strategy using d3.v4 an ES6:
brush.on('end', () => {
if (d3.event.selection[1] - d3.event.selection[0] > maxSelectionSize) {
// selection is too large; animate back down to a more reasonable size
let brushCenter = d3.event.selection[0] + 0.5 * (d3.event.selection[1] - d3.event.selection[0]);
brushSel.transition()
.duration(400)
.call(brush.move, [
brushCenter - 0.49 * maxSelectionSize,
brushCenter + 0.49 * maxSelectionSize
]);
} else {
// valid selection, do stuff
}
});
If the brush selection size is too large when the user lets go of it, it animates back down to the specified maximum size (maxSelectionSize). At that point, the 'end' event will fire again, with an acceptable selection size.
Note the 0.49 scalar: this is required to prevent floating point / rounding errors that could cause an infinite loop if the brush is move()d to a size that is still too large.
Here is an example of limiting the brush's minimum width as 100px and maximum width as 200ps. I added a few lines into d3.v4.js, for setting the limitation of the brush width.
Added brush.limit([min, max]) for set the limitation:
var _limit = null;
brush.limit = function (l) {
_limit = l;
}
Break mouse move event in move() function:
if (_limit && e1 - w1 < _limit[0]) {
return;
}
(Demo) (Source Code)
Nice piece of code, but I found a little bug.
It does lock your brush whenever s[1]-s[0] < 0.5, but if you keep pressed the resize and bring all your brush to the oposite direction, it starts "moving" the brush without any action (i.e. it doesn't behave as it should).
I am pretty sure I can come up with a reasonable solution. If so, I'll re-post it here.
(sorry to post it here as an answer, but as you've already answered it once, I cannot post as a comment, I guess).