I'm trying to have the chart tickets in a D3 bullet chart follow the data itself, as per the 2nd example here:
Bullet chart ticks & labels in D3.js
The issue is that the source of this (http://boothead.github.io/d3/ex/bullet.html) no longer exists on the internet, the only thing out there is the gif in this post that I've linked.
enter image description here
Does anyone have the original copy of this project or have any advice?
I'm using the first example by mbostock and trying to replicate the bottom one.
Many thanks
In the original bullet.js from the bostock example https://bl.ocks.org/mbostock/4061961
Instead of getting the ticks from the scale you get the values from the range, measure and mark
change around line 109
// var tickVals = x1.ticks(8);
var tickVals = rangez.concat(measurez).concat(markerz);
// Update the tick groups.
var tick = g.selectAll("g.tick")
.data(tickVals, function(d) {
return this.textContent || format(d);
});
Edit
There is a problem if you update the data based on a new fetch from the server. some of the ticks end up on the wrong location. If the number of markers,measures,ranges also change they also end up at the wrong location.
It depends on the selection you supply to the bullet call.
The confusion is caused by the poor naming of the main program.
var svg = d3.select("body").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "bullet")
.attr("width", svgWidth)
.attr("height", svgHeight)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(chart);
The name suggests that svg is a selection of svg elements. This is incorrect, it is a selection of g elements.
The update() function should reflect this
function updateData() {
d3.json("mailboxes.json", function (error, data) {
d3.select("body")
.selectAll("svg")
.select('g')
.data(data)
.call(chart.duration(500));
});
}
If the number of bullet graphs changes on the update there is the problem that they are not created or deleted if needed. So we need to make a function that can be used for initial and update calls.
function drawCharts() {
d3.json("mailboxes.json", function (error, data) {
var svg = d3.select("body").selectAll("svg").data(data);
svg.exit().remove();
svg.enter().append("svg")
.attr("class", "bullet")
.attr("width", svgWidth)
.attr("height", svgHeight)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.select("body")
.selectAll("svg")
.select('g')
.data(data)
.call(chart.duration(500));
});
}
A better change in bullet.js [109] would be:
// var tickVals = x1.ticks(8);
var tickVals = [];
rangez.concat(measurez).concat(markerz).forEach(e => {
if (tickVals.indexOf(e) == -1) tickVals.push(e);
});
// Update the tick groups.
var tick = g.selectAll("g.tick").data(tickVals);
That is do not use the value of the tick to match, in case we have multiple values in the ticks, and remove the duplicates.
We also need to change the update of the ticks, about 30 lines down
tickUpdate.select("text")
.attr("y", height * 7 / 6);
to
tickUpdate.select("text")
.text(format)
.attr("y", height * 7 / 6);
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 am working on a data visualization project, where I parsed every article on the frontpage of a particular news website, searching for 10 distinct words. Then I counted every occurrence of that word for that particular day.
I am trying to implement a line graph to display the usage of each word over time. What I want to do, is have a button exist for each word, and upon clicked, display a line graph corresponding to the data set of each word.
My 1st problem is that all this data is stored in on large dataset. No problem, I have formatted the data to now exist in one large object, with 10 keys for each word. Each of these keys contain an array in the value pair. This array contains objects with (date, and word_count) key and values respectively.
However, the example I am using to model my project off of uses a completely different data set, and is able to access the data different, so now problems arise.
Here is my newly formatted data, all existing in one object. This is the only way I found I was able to separate each word into it's own dataset. The words like "Extremist", "God", "Guns", etc. are the keys.
The values is an array of objects containing the (date and count) key, value pairs respectively
PROBLEM
This object exists in a variable called "data_count"
What I need to be able to do, similar to what the example states, is access all date and count variables with this syntax: data_count.Obama.date or data_count.Obama.count
Right now, to access all date or count values, I need to loop through them and access each index of the "Obama" array like so
data_count.Obama[i].date
The issue is, I need to insert these date and column values to be accepted as the "x" and "y" coordinates using the d3.line function. So I can't loop through them. As you can see in the example, the user accesses his key value pairs through simple dot notation. But when I replicate his process, the value appears as undefined.
Sorry for the wall of text! Hopefully someone can help me out! Thanks.
If I understand correctly, I believe you are over complicating the question: your data is very compatible, with the update function in the example you shared. You have data arrays just like the example, each item in the arrays represents a point on the line having properties representing both x and y values. The update function can be passed your data or the examples data with minor tweaks to account for the x axis being a date instead of a number:
You don't need to do anything differently beyond basic modifications: changing the scale to take a date, changing the extent of the x axis so that the min doesn't equal 0, change the properties passed to each scale, etc. Once you do that you can pass any property of current_data to your update function.
d3.csv("https://raw.githubusercontent.com/nickrinaldi88/BreitBart_DataVis/main/breitbartData.csv").then(function(csv) {
var current_data = {};
var parse = d3.timeParse("%Y-%m-%d")
for(var i=0; i < csv.length; i++) {
var item = csv[i];
if(!current_data[item.Word]) current_data[item.Word] = [];
current_data[item.Word].push({
date:parse(item.Date),
count:item.Count
})
}
d3.select("select")
.selectAll("option")
.data(Object.keys(current_data))
.enter()
.append("option")
.text(function(d) { return d; });
d3.select("select")
.on("change", function() {
update(current_data[this.value])
})
// set the dimensions and margins of the graph
var margin = {top: 10, right: 30, bottom: 30, left: 50},
width = 460 - margin.left - margin.right,
height = 250 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Initialise a X axis:
var x = d3.scaleTime().range([0,width]);
var xAxis = d3.axisBottom().scale(x);
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class","myXaxis")
// Initialize an Y axis
var y = d3.scaleLinear().range([height, 0]);
var yAxis = d3.axisLeft().scale(y);
svg.append("g")
.attr("class","myYaxis")
// Create a function that takes a dataset as input and update the plot:
function update(data) {
// Create the X axis:
x.domain(d3.extent(data, function(d) { return d.date }) );
svg.selectAll(".myXaxis").transition()
.duration(3000)
.call(xAxis);
// create the Y axis
y.domain([0, d3.max(data, function(d) { return +d.count }) ]);
svg.selectAll(".myYaxis")
.transition()
.duration(3000)
.call(yAxis);
// Create a update selection: bind to the new data
var u = svg.selectAll(".lineTest")
.data([data]);
// Updata the line
u
.enter()
.append("path")
.attr("class","lineTest")
.merge(u)
.transition()
.duration(3000)
.attr("d", d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.count); }))
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2.5)
}
// At the beginning, I run the update function on the first dataset:
update(current_data.Obama)
})
path {
stroke-width: 1px;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<select></select>
<div id="my_dataviz"></div>
I trying to understand how the D3 chord diagram works. My first step is to display the arcs for the diagram with following script. But for some reason, the arcs are not showing up.
See web page HERE
Can some one tell me what I am missing?
<body>
<script>
// Chart dimensions.
var width = 960,
height = 750,
innerRadius = Math.min(width, height) * .41,
outerRadius = innerRadius * 1.1;
//Create SVG element with chart dementions
var svg = d3. select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append ("g")
.attr("transform", "translate (" + width / 2 + "," + height / 2 + ")");
//------------Reformat Data ------------------------------------------
var matrix = []; // <- here is the data
d3.tsv('picData.tsv', function(err, data)
{
//console.log(data);
pictures = d3.keys(data[0]).slice(1);
//console.log(pictures);
data.forEach(function(row)
{
var mrow = [];
pictures.forEach(function(c)
{
mrow.push(Number(row[c]));
});
matrix.push(mrow);
//console.log(mrow);
});
//console.log('1st row: ' + matrix[0]);
//console.log(matrix);
});
//---------------- Define diagram layout ----------------------------
var chord = d3.layout.chord() //<-- produce a chord diagram from a matrix of input data
.matrix(matrix) //<-- data in matrix form
.padding(0.05)
.sortSubgroups(d3.descending);
var fill = d3.scale.category20(); //<-- https://github.com/mbostock/d3/wiki/API-Reference#d3scale-scales
//console.log(fill);
var g = svg.selectAll("g.group")
.data(chord.groups)
.enter().append("svg:g")
.attr("class", "group");
//console.log(g);
// create arcs
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
//console.log(arc);
g.append("path")
.attr("d", arc)
.style("fill", function(d) { console.log(d.index); return fill(d.index);})
.style("stroke", function(d) { return fill(d.index); })
.attr("id", function(d, i) { return"group-" + d.index });;
g.append("svg:text")
.attr("x", 6)
.attr("class", "picture")
.attr("dy", 15)
.append("svg:textPath")
.attr("xlink:href", function(d) { return "#group-" + d.index; })
.text(function(d) { return pictures[d.index]; });
//console.log(g);
</script>
</body>
Your problem stems from the fact that d3.tsv is asynchronous:
Issues an HTTP GET request for the comma-separated values (CSV) file at the specified url... The request is processed asynchronously.
As a result, all of your code under "Define diagram layout" is being executed before any data is loaded. Otherwise, your code works fine (See image below). So just move all your code into your d3.tsv(...) call and you'll be all set.
Your script is running without errors, but no elements are being created from your data join. That's usually a sign that you are passing in a zero-length data array.
In fact, you're not passing in an array at all; you're passing a function object. When d3 looks up the array length of that object, it returns undefined, which gets coerced to the number zero, and so no groups and no chords are created.
Relevant part of your code:
var g = svg.selectAll("g.group")
.data(chord.groups)
.enter().append("svg:g")
.attr("class", "group");
To actually get the array of chord group data objects, you need to call chord.groups(). Without the () at the end, chord.groups is just the name of the function as an object.
Edited to add:
Ooops, I hadn't even noticed that your drawing code wasn't included inside your d3.tsv callback function. Fix that, as described in mdml's answer, then fix the above.
I'm trying to get a multi-line date based chart to pan nicely across the X date axis and I simply cannot figure out what the problem is.
I have the zoom behaviour set up in the code but it's just not performing as expected. If you click on a point in a line and scroll it appears to be scrolling the axis, if it click on the labels in the axis it also scrolls but the actual visualisation of data doesn't scroll.
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 1])
.on("zoom", function () {
svg.select(".x.axis").call(xAxis);
svg.select(".lines").call(xAxis);
});
svg.call(zoom);
Also if you click directly on the back ground the mouse event doesn't seem to make it's way to the control at all.
I read a few examples on this and each seem to take a vastly different approach which I've tried but none have worked for my chart.
There are possibly a number of issues that exist as barriers to getting this working so I thought the best way to illustrate the problem was in a JsFiddle.
D3 Chart Panning Fiddle
What I'm trying to achieve is when there is a lot of data to visualise the chart can adapt to the data set and allow the data to extend beyond the bounds of the chart.
Currently clicking on the background does not allow panning because you have applied zoom behavior to the g element not to the svg.
var svg = d3.select('#host')
.data(plotData)
.append("svg")
.attr("id", "history-chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom);
Right now on zoom you have updated x and y axes but not the visualization. So you have update the lines and circles also like this.
var zoom = d3.behavior.zoom()
.x(x)
.scaleExtent([1, 1])
.on("zoom", function () {
svg.select(".x.axis").call(xAxis);
svg.select(".lines").call(xAxis);
svg.selectAll("path.lines")
.attr("d", function(d) { return line(d.values); });
svg.selectAll("circle")
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.value); });
});
Since you are panning the map you will have to use clip path for restricting visualization from moving outside the chart
var clip = svg.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
Apply clip path to the g elment which contains lines and cicrles.
var attribute = svg.selectAll(".attribute")
.data(plotData)
.enter().append("svg:g")
.attr("clip-path", "url(#clip)")
.attr("class", "attribute");
I'm working with a data set that's categorically identical from year to year, and I want to make a D3 pie chart with animated transitions from year to year. The data is in a 2-d array, each inner array is a year. Because the number of values isn't changing, I think I can just replace the data set for the transition, and I don't need to do a data join (?).
I have the pie chart working well initially, and I'm updating the data via click event. But my transitions aren't working. Here's the code for the first pie chart (there are variable declarations and other data managing that I've left out to save space, and because that stuff's working):
var outerRadius = w/2;
var innerRadius = 0;
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var svg= d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var arcs = svg.selectAll("g.arc")
.data(pie(datamod[0]))
.enter()
.append("g")
.attr("class", "arc")
.attr("transform", "translate(" + outerRadius + ", " +outerRadius + ")");
arcs.append("path")
.attr("fill", function(d,i){
return colors[i];
})
.attr("d", arc);
And then to update...clickToChange() is called when users click anywhere in the body. it's loading new data from the next spot in the 2-d array and also updates text for the year, and there's some code in here to keep it from restarting if it's already running... But the main problem I think is with the code to update the arcs...
function clickToChange()
{ if(!isRunning)
{
isRunning = true;
myTimer =setInterval(function() {if (yearcounter < 11)
{
yearcounter++;
}
else
{
yearcounter = 0;
stopDisplay();
}
var thisyear = 2000 + yearcounter; //updating happens here...
svg.selectAll("g.arc")
.data(pie(datamod[yearcounter]))
.transition()
.attr("class", "arc")
.attr("transform", "translate(" + outerRadius + ", " +outerRadius + ")");
arcs.attr("fill", function(d,i){
return colors[i];
// console.log(d.value);
// return "rgb(" + colorscale(d.value) + ",50,50)";
})
.attr("d", arc);
document.getElementById('year').innerHTML = thisyear;
}, 2000); //end set interval
}//end if
}
function stopDisplay()
{
clearInterval(myTimer);
isRunning = false;
}
I think the problem is that I'm possibly not binding the data properly to the correct elements, and if I'm using the correct notation to select the arcs?
Okay, I can see multiple issues/drawbacks with your approach.
1) In your code:
arcs.append("path")
.attr("fill", function(d,i){
return colors[i];
})
.attr("d", arc);
arc is a function call that you are making that doesn't actually exist in the code that you have shared with us, or you need to write. You have this arc function call multiple times, so this will need to be addressed.
2) I would check into using the .on("click", function(d,i) { do your transitions here in this function call }); method instead of setting the transition and attributes of each of the items. I have found that it makes the transition calls easier to manage if you start doing anything more fancy with the transitions. You can see an example of what I mean in the Chord Diagram at http://bl.ocks.org/mbostock/4062006
Hopefully this helps you out a bit.