I am trying to replicate a histogram that is made up of dots. Here is a good reference:
My data has the following format:
{'name':'Company1', 'aum':42, 'growth':16},
{'name':'Company2', 'aum':36, 'growth':24},
{'name':'Company3', 'aum':34, 'growth':19},
...
In my case I'm binning by aum and color coding by growth. Everything seemed fine up until I went to set the cy attribute.
graphGroup.selectAll('circ')
.data(data)
.enter()
.append('circ')
.attr('r',8)
.attr('cx', function(d) { return xScale(d.aum);})
.attr('cy', ??????)
.style('fill', function(d) { return colorScale(d.growth);});
The only way I can think of to handle this is to re-engineer the data to give it an explicit y index value or something. However, this is not supported in my statistics suite and the n is too large for me to manually do it.
Question
Given my current data structure, can d3 help me with anything ad hoc to get the correct y value? I was thinking a counter might work, but I was unable to devise a bin-specific counter. Maybe d3 has a more elegant solution than a counter.
...can d3 help me with anything ad hoc to get the correct y value?
No, there is no native method for this. But here is where most people get D3 wrong: D3 is not a charting tool (except for the axis module, D3 doesn't paint anything!), it's just a collection of JavaScript methods. This fact simultaneously gives D3 its weakness (steep learning curve) and its advantage (you can do almost anything with D3).
In your case, for instance, all I'd do is separate the data in bins (using a histogram generator, for instance, or just manipulating the data directly) and then appending the circles in each bin/group using their indices.
It can be just something like this:
circle.attr("cy", function(_, i) {
return maxValue - circleRadius * i;
});
This will append the first circle of each bin (group) at the base of the SVG, and then each subsequent one in a smaller y coordinate, according to its index.
Check this basic demo:
const data = d3.range(100).map(() => Math.random());
const svg = d3.select("svg");
const xScale = d3.scalePoint(d3.range(10).map(d => d / 10), [20, 480])
.padding(0.5);
const binData = d3.histogram()
.domain([0, 1])(data);
const g = svg.selectAll(null)
.data(binData)
.enter()
.append("g")
.attr("transform", d => "translate(" + xScale(~~(100 * d.x0) / 100) + ",0)");
g.selectAll(null)
.data(d => d)
.enter()
.append("circle")
.attr("r", 5)
.attr("cy", function(_, i) {
return 220 - 13 * i;
})
.style("fill", "gray")
.style("stroke", "black");
const axisGroup = svg.append("g")
.attr("transform", "translate(0,230)")
.call(d3.axisBottom(xScale));
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="250"></svg>
Related
I have a map already drawed. I would like to add a legend using d3.js. For example when filering by length, the map should show differents colors. Since a week, I couldn't achieve this task. My map color seem to be good but the legend does not match.
Could anybody help me with my draw link function ?
https://jsfiddle.net/aba2s/xbn9euh0/12/)
I think it's the error is about the legend function.
Here is the function that change my map color Roads.eachLayer(function (layer) {layer.setStyle({fillColor: colorscale(layer.feature.properties.length)})});
function drawLinkLegend(dataset, colorscale, min, max) {
// Show label
linkLabel.style.display = 'block'
var legendWidth = 100
legendMargin = 10
legendLength = document.getElementById('legend-links-container').offsetHeight - 2*legendMargin
legendIntervals = Object.keys(colorscale).length
legendScale = legendLength/legendIntervals
// Add legend
var legendSvg = d3.select('#legend-links-svg')
.append('g')
.attr("id", "linkLegendSvg");
var bars = legendSvg.selectAll(".bars")
//.data(d3.range(legendIntervals), function(d) { return d})
.data(dataset)
.enter().append("rect")
.attr("class", "bars")
.attr("x", 0)
.attr("y", function(d, i) { return legendMargin + legendScale * (legendIntervals - i-1); })
.attr("height", legendScale)
.attr("width", legendWidth-50)
.style("fill", function(d) { return colorscale(d) })
// create a scale and axis for the legend
var legendAxis = d3.scaleLinear()
.domain([min, max])
.range([legendLength, 0]);
legendSvg.append("g")
.attr("class", "legend axis")
.attr("transform", "translate(" + (legendWidth - 50) + ", " + legendMargin + ")")
.call(d3.axisRight().scale(legendAxis).ticks(10))
}
D3 expects your data array to represent the elements you are creating. It appears you are passing an array of all your features: but you want your scale to represent intervals. It looks like you have attempted this approach, but you haven't quite got it.
We want to access the minimum and maximum values that will be provided to the scale. To do so we can use scale.domain() which returns an array containing the extent of the domain, the min and max values.
We can then create a dataset that contains values between (and including) these two endpoints.
Lastly, we can calculate their required height based on how high the visual scale is supposed to be by dividing the height of the visual scale by the number of values/intervals.
Then we can supply this information to the enter/update/exit cycle. The enter/update/exit cycle expects one item in the data array for every element in the selection - hence why need to create a new dataset.
Something like the following shold work:
var dif = colorscale.domain()[1] - colorscale.domain()[0];
var intervals = d3.range(20).map(function(d,i) {
return dif * i / 20 + colorscale.domain()[0]
})
intervals.push(colorscale.domain()[1]);
var intervalHeight = legendLength / intervals.length;
var bars = legendSvg.selectAll(".bars")
.data(intervals)
.enter().append("rect")
.attr("class", "bars")
.attr("x", 0)
.attr("y", function(d, i) { return Math.round((intervals.length - 1 - i) * intervalHeight) + legendMargin; })
.attr("height", intervalHeight)
.attr("width", legendWidth-50)
.style("fill", function(d, i) { return colorscale(d) })
In troubleshooting your existing code, you can see you have too many elements in the DOM when representing the scale. Also, Object.keys(colorscale).length won't produce information useful for generating intervals - the keys of the scale are not dependent on the data.
eg
I'd like to create what I think is best described as a either a force-directed histogram (or dot/bubble histogram), or a beeswarm where y-values are constrained along the x-axis.
That is, using the force-directed layout, assign x-positions based on some x-value and y-positions based on counts for that value, where y values must remain above some lower-boundary.
I've created a force-directed beeswarm plot that is almost what I want. I've just not yet figured out the y-axis constraint.
Thanks for any help/suggestions!
To apply such a constrain you can limit d.y value directly — just replace
.attr('cy', d => d.y)
with, for example,
.attr('cy', d => d.y = Math.min(d.y, height / 2))
This way nodes are forced to be above height / 2. It's important not only to constrain cy, but to update d.y too so this new position will be taken into account on the next iteration. If you don't like to assign in return statement, it's possible to modify d.y separately with each operator.
forceCenter here can mess things up and I'm not sure it's necessary. I doubt the necessity of manyBody force as well — why the circles must attract each other (you use positive strength = attraction)? It's better to attract them by the axis instead (forceY).
There is a compromise between accurate x position of the circles, the attraction of the x-axis and the speed (in iterations) of convergence. Some tuning of force strengths can be needed. Effects of strength are described in d3-force readme: https://github.com/d3/d3-force#x_strength. For most forces it's recommended to use strength in [0, 1] range.
const width = 500
const height = 150
const svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
const colorScale = d3.scaleOrdinal()
.range(['#FCFCFC', '#F7567C', '#FFFAE3', '#99E1D9', '#5D576B'])
const radius = 7
const sampleData = d3.range(150).map(() => ({r: radius,
value: width/2 + d3.randomNormal(0,75)()}))
// set params for force layout
//const manyBody = d3.forceManyBody().strength(2)
//const center = d3.forceCenter().x((width/2)).y((height/2))
// define force
let force = d3.forceSimulation()
//.force('charge', manyBody)
//.force('center', center)
.force('collision', d3.forceCollide(d => d.r).strength(1))
.velocityDecay(.48)
.force('x', d3.forceX(d => d.value).strength(3))
.force('y', d3.forceY(height - radius).strength(0.2))
.nodes(sampleData)
.on('tick', changeNetwork)
svg.selectAll('circle')
.data(sampleData)
.enter()
.append('circle')
.style('fill', (d,i) => colorScale(i))
.attr('r', d => d.r)
.attr('stroke', 'black')
.attr('stroke-width', .1)
function changeNetwork() {
d3.selectAll('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y = Math.min(d.y, height - radius - 1))
}
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
I have seen this type of question asked a lot but no answers i found solved it for me.
I created a fiddle here of a stripped down simplified version of my code
https://jsfiddle.net/00m2cv84/4/
here is a snippet of the ganttish object - best to check out the fiddle though for context
const ganttish = function (data) {
this.dataSet = data
this.fullWidth = +document.getElementById('chart').clientWidth
this.fullHeight = 700
this.pad = { top: 50, right: 50, bottom: 50, left: 50 }
this.h = this.fullHeight - this.pad.top - this.pad.bottom
this.w = this.fullWidth - this.pad.left - this.pad.right
this.svg = d3.select('#chart')
.append('svg')
.attr('width', this.fullWidth)
.attr('height', this.fullHeight)
}
ganttish.prototype = {
redraw (newDataSet) {
this.dataSet = newDataSet // this should overwrite the old data
this.draw()
},
draw () {
let y = d3.scaleBand()
.padding(0.1)
.range([0, this.h])
y.domain(this.dataSet.map(d => d.idea_id))
let x = d3.scaleTime().range([this.pad.left, this.w + this.pad.left])
x.domain([
d3.min(this.dataSet, d => new Date(Date.parse(d.start_date))),
d3.max(this.dataSet, d => new Date(Date.parse(d.end_date)))
])
let xAxis = d3.axisBottom(x).tickSize(-(this.h), 0, 0)
let yAxis = d3.axisLeft(y).tickSize(-(this.w), 0, 0)
let xA = this.svg.append('g')
.attr('transform', 'translate(0,' + this.h + ')')
.attr('class', 'x axis')
.call(xAxis)
let yA = this.svg.append('g')
.attr('transform', 'translate(' + this.pad.left + ', 0)')
.attr('class', 'y axis')
.call(yAxis)
let timeBlocks = this.svg.selectAll('.timeBlock')
.data(this.dataSet)
let tbGroup = timeBlocks
.enter()
.append('g')
tbGroup
.append('rect')
.attr('class', 'timeBlock')
.attr('rx', 5)
.attr('ry', 5)
.attr('x', d => x(new Date(Date.parse(d.start_date))))
.attr('y', d => y(d.idea_id))
.attr('width', d => parseInt(x(new Date(Date.parse(d.end_date))) - x(new Date(Date.parse(d.start_date)))))
.attr('height', d => y.bandwidth())
.style('fill', (d) => {
return d.color ? d.color : 'rgba(123, 173, 252, 0.7)'
})
.style('stroke', 'black')
.style('stroke-width', 1)
tbGroup
.append('text')
.attr('class', 'timeBlockText')
.attr('x', d => x(new Date(Date.parse(d.start_date))) + 10)
.attr('y', d => y(d.idea_id) + (y.bandwidth() / 2))
.attr('alignment-baseline', 'middle')
.attr('font-size', '1em')
.attr('color', 'black')
.text(d => d.name)
/**
I have literally tried exit().remove() on pretty much everything i could think of :(
this.svg.exit().remove()
timeBlocks.exit().remove()
this.svg.selectAll('.timeBlock').exit().remove()
this.svg.selectAll('.timeBlockText').exit().remove()
this.svg.select('.x').exit().remove()
this.svg.select('.y').exit().remove()
*/
}
Sidenote:
I have a Vue js application and i'm implementing a Gantt(ish) style horizontal bar chart. In the fiddle i have simulated the Vue part by creating an Object which then you call the redraw method on it, this pattern simulates a watcher in the component updating the dataset when the parent data changes. The issue i face is the same.
issue:
When i change the data to the chart it does not update my chart rects or text. It does however append the new axis' on top of the old ones.
i understand that i should be calling .exit().remove() on anything .enter() 'd when finished in order to clear them for the next data push, but everywhere i try this it fails. I can get it to work by creating a fresh svg on every draw, but i understand i won't be able to do any transitions - and it seems a very bad approach :)
What is twisting my noodle is that if i push extra data to the new data object, it appends it fine, once - then does not do it again. It seems once element X in the data array has been added it will not update again.
https://jsfiddle.net/00m2cv84/5/
I know that the this.dataSet is updating, it just does not seem to be accepted by D3
Any help would be greatly appreciated for a D3 noob :)
You're problem is that you're not handling your data join properly. I'd highly recommend reading some of Mike Bostock's examples probably starting with Three Little Circles. To summarize though:
D3 joins created by calling .data() contain 3 selections:
Enter (DOM elements that need to be created as they exist in the data but don't yet exist in the DOM)
Exit (DOM elements that need to be removed, as they're not longer represented in the data)
Update (DOM elements that already exist and still exist in the data)
Within your code you're handling the enter() selection with this bit of code:
let tbGroup = timeBlocks
.enter()
.append('g')
The problem is you're not handling any of the other selections. The way I'd go about doing this is:
let join = this.svg.selectAll('.timeBlock').data(this.dataSet);
// Get rid of any old data
join.exit().remove();
// Create the container groups. Note that merge takes the "new" selection
// and merges it with the "update" selection. We'll see why in a minute
const newGroups = join.enter()
.append('g')
.merge(join);
// Create all the new DOM elements that we need
newGroups.append("rect").attr("class", "timeBlock");
newGroups.append('text').attr('class', 'timeBlockText');
// We need to set values for all the "new" items, but we also want to
// reflect any changes that have happened in the data. Because we used
// `merge()` previously, we can access all the "new" and "update" items
// and set their values together.
join.select(".timeBlock")
.attr('rx', 5)
.attr('ry', 5)
.attr('x', d => x(new Date(Date.parse(d.start_date))))
.attr('y', d => y(d.idea_id))
...
join.select(".timeBlockText")
.attr('x', d => x(new Date(Date.parse(d.start_date))) + 10)
.attr('y', d => y(d.idea_id) + (y.bandwidth() / 2))
.attr('alignment-baseline', 'middle')
.attr('font-size', '1em')
.attr('color', 'black')
.text(d => d.name)
I'm learning to build D3 graph in a rails application. but somehow i can't load the graph.
So basically, what i did is download the d3.zip from d3 github. put d3.js and d3.min.js in assets/javascript/ and require them both in the application.js.
Here is the d3 code in my view,
<% content_for(:inline_javascript) do %>
<script>
var w = 200,
h = 200,
p = 10;
var data = [{count:100,year:1999},
{count:240,year:2010},
{count:290,year:2009}];
var bar_height = d3.scale.linear()
.domain(d3.extent(data, function(d) { return d.count; }) ) // min max of count
.range([p,h-p]); // min max of area to plot in
var bar_xpos = d3.scale.linear()
.domain(d3.extent(data, function(d) { return d.year; }) ) // min max of year
.range([p,w-p]); // min max of area to plot in
var svg = d3.select("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(d) {
return bar_xpos(d.year); })
.attr("y", function(d) {
return h - bar_height(d.count); })
.attr("width", 10)
.attr("height", function(d) {return bar_height(d.count); })
.attr("fill", "steelblue")
</script>
<%end%>
but the page show nothing. and there's no errors when i inspect elements on the browser. So i wonder if anyone can tell me what is wrong with what i did?
Thanks!
Edit:
so finally i found out in rails, i need to have a placeholder for the d3 graph, and so it needs a id to identify the graph. The idea is like that. but i only need to add line,
<svg class="chart"></svg>
The graph shows on the page!
D3's approach is somewhat similar to jQuery. It will not do anything unless you explicitly ask it to.
To do any visualization first it is required to select the node(s) to operate on.
D3 provides select and selectAll methods for this.
And If no elements in the current document match the specified selector, they return the empty selection, hence no errors will be shown.
I'm new to D3, and spent already a few hours to find out anything about dealing with structured data, but without positive result.
I want to create a bar chart using data structure below.
Bars are drawn (horizontally), but only for user "jim".
var data = [{"user":"jim","scores":[40,20,30,24,18,40]},
{"user":"ray","scores":[24,20,30,41,12,34]}];
var chart = d3.select("div#charts").append("svg")
.data(data)
.attr("class","chart")
.attr("width",800)
.attr("height",350);
chart.selectAll("rect")
.data(function(d){return d3.values(d.scores);})
.enter().append("rect")
.attr("y", function(d,i){return i * 20;})
.attr("width",function(d){return d;})
.attr("height", 20);
Could anyone point what I did wrong?
When you join data to a selection via selection.data, the number of elements in your data array should match the number of elements in the selection. Your data array has two elements (for Jim and Ray), but the selection you are binding it to only has one SVG element. Are you trying to create multiple SVG elements, or put the score rects for both Jim and Ray in the same SVG element?
If you want to bind both data elements to the singular SVG element, you can wrap the data in another array:
var chart = d3.select("#charts").append("svg")
.data([data])
.attr("class", "chart")
…
Alternatively, use selection.datum, which binds data directly without computing a join:
var chart = d3.select("#charts").append("svg")
.datum(data)
.attr("class", "chart")
…
If you want to create multiple SVG elements for each person, then you'll need a data-join:
var chart = d3.select("#charts").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "chart")
…
A second problem is that you shouldn't use d3.values with an array; that function is for extracting the values of an object. Assuming you wanted one SVG element per person (so, two in this example), then the data for the rect is simply that person's associated scores:
var rect = chart.selectAll("rect")
.data(function(d) { return d.scores; })
.enter().append("rect")
…
If you haven't already, I recommend reading these tutorials:
Thinking with Joins
Nested Selections
This may clarify the nested aspect, in addition to mbostock's fine answer.
Your data has 2 degrees of nesting. You have an array of 2 objects, each has an array of ints. If you want your final image to reflect these differences, you need to do a join for each.
Here's one solution: Each user is represented by a group g element, with each score represented by a rect. You can do this a couple of ways: Either use datum on the svg, then an identity function on each g, or you can directly join the data on the g. Using data on the g is more typical, but here are both ways:
Using datum on the svg:
var chart = d3.select('body').append('svg')
.datum(data) // <---- datum
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(function(d){ return d; }) // <----- identity function
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Using data on the group (g) element:
var chart = d3.select('body').append('svg')
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(data) // <--- attach directly to the g
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Again, you don't have to create these g elements, but by doing so I can now represent the user scores differently (they have different y from the transform) and I can also give them different styles, like this:
.jim {
fill: red;
}
.ray {
fill: blue;
}