Creating d3 bar chart - javascript

I have a csv file that contains the top songs on Spotify from 2010-2019. The attributes of each song have things like genre, artist, year, popularity, bpm, etc. I wanted to create a bar graph that has the year on the x-axis, popularity on the y-axis, and then each genre represents a different color bar on the chart. I have attached the csv file I am using, any help would be appreciated.
link to csv:
https://github.com/moonpieluvincutie/Spotify

I'll assume you are looking for stacked bars but if not, un-stacked would be the easier approach. Using this example as a base, we need to first transform the data to represent the configuration you described in your post. Essentially we need to group by year, genre and transform it in a way that is suitable for stacking. For example something like this...
const data = [
{ month: new Date(2015, 0, 1), apples: 3840, bananas: 1920, cherries: 960, dates: 400 },
];
or in the case of your data something like this...
const data = [
{ year: 2015, genre1: popularity1, genre2: popularity1, ...rest },
];
Fortunately d3 offers a plethora of transformations for us to use.
Note: I am using d3 v6 not v4 (like the example) in the following code, the API is slightly different but essentially the same concepts.
I created the following working snippet for you to review. I used d3.rollups to group the data in the way described above. I would advise you to log each transform to see what the new structure looks like. I took the d3.mean of the popularity if there was more than 1 song per genre per year. Once you have the structure in the shape defined above, we pass that to d3.stack to format the data with correct y0 and y1 values for each series (aka group or genre).
Finally, by adding the stacked dataset to the rendering logic from the base example with a few tweaks, you get something like this below. Sorry, it's a bit unpolished but this should give you a good start point to iterate from.
const svg = d3.select("svg"),
margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Set x, y and colors
const x = d3.scaleBand()
.rangeRound([10, width - 10])
.padding(0.2)
const y = d3.scaleLinear()
.range([height, 0]);
const z = d3.scaleOrdinal(d3.schemePaired);
d3.csv('https://raw.githubusercontent.com/moonpieluvincutie/Spotify/main/top10s%20(version%201).xlsb.csv', (d) => ({
year: +d.year,
pop: d.pop,
genre: d['top genre'],
})).then((data) => {
// Transform the data into groups by genre and year
const groupedDataset = d3.rollups(data, v => d3.mean(v, d => d.pop), d => d.year, d => d.genre)
const flattenedDataset = groupedDataset.map(([year, values]) => {
return {
year,
...values.reduce((acc, [genre, pop]) => {
acc[genre] = pop;
return acc;
}, {}),
}
});
const genres = d3.rollups(data, () => null, d => d.genre).map(([genre]) => genre);
// Stack grouped data
const dataset = d3.stack()
.keys(genres)
.value((d, key) => d[key] ?? 0)
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone)
(flattenedDataset)
// Assign x, y and z (aka genre group) domains
x.domain(groupedDataset.map(([year]) => year));
y.domain([0, d3.max(
dataset, (d) => {
return d3.max(d, ([y]) => y)
}
)]).nice();
z.domain(genres);
const yAxis = d3.axisLeft()
.scale(y)
.ticks(5)
.tickSize(-width, 0, 0)
.tickFormat( (d) => { return d } );
const xAxis = d3.axisBottom()
.scale(x)
g.append("g")
.selectAll("g")
.data(dataset)
.enter().append("g")
.attr("fill", (d) => z(d.key))
.selectAll("rect")
.data((d) => d)
.enter().append("rect")
.attr("x", (d) => x(+d.data.year))
.attr("y", (d) => y(d[1]))
.attr("height", (d) => y(d[0]) - y(d[1]))
.attr("width", x.bandwidth());
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
g.append("g")
.attr("class", "axis")
.call(yAxis)
.append("text")
.attr("x", 2)
.attr("y", y(y.ticks().pop()) + 0.5)
.attr("dy", "0.32em")
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "start")
.text("Popularity");
const legend = g.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "end")
.selectAll("g")
.data(genres.slice().reverse())
.enter().append("g")
.attr("transform", (d, i) => "translate(0," + i * 20 + ")");
legend.append("rect")
.attr("x", width - 19)
.attr("width", 19)
.attr("height", 19)
.attr("fill", z);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9.5)
.attr("dy", "0.32em")
.text((d) => d);
})
.axis .domain {
display: none;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="https://d3js.org/d3.v6.min.js" charset="utf-8"></script>
</head>
<body>
<!-- chart appended to svg in javaScript -->
<svg width="960" height="500"></svg>
</body>
</html>

Related

How do I match up text labels in a legend created in d3

I am building a data visualization project utilizing the d3 library. I have created a legend and am trying to match up text labels with that legend.
To elaborate further, I have 10 rect objects created and colored per each line of my graph. I want text to appear adjacent to each rect object corresponding with the line's color.
My Problem
-Right now, an array containing all words that correspond to each line appears adjacent to the top rect object. And that's it.
I think it could be because I grouped my data using the d3.nest function. Also, I noticed only one text element is created in the HTML. Can anyone take a look and tell me what I'm doing wrong?
JS Code
const margin = { top: 20, right: 30, bottom: 30, left: 0 },
width = 1000 - margin.left - margin.right;
height = 600 - margin.top - margin.bottom;
// maybe a translate line
// document.body.append(svg);
const div_block = document.getElementById("main-div");
// console.log(div_block);
const svg = d3
.select("svg")
.attr("width", width + margin.left + margin.right) // viewport size
.attr("height", height + margin.top + margin.bottom) // viewport size
.append("g")
.attr("transform", "translate(40, 20)"); // center g in svg
// load csv
d3.csv("breitbartData.csv").then((data) => {
// convert Count column values to numbers
data.forEach((d) => {
d.Count = +d.Count;
d.Date = new Date(d.Date);
});
// group the data with the word as the key
const words = d3
.nest()
.key(function (d) {
return d.Word;
})
.entries(data);
// create x scale
const x = d3
.scaleTime() // creaters linear scale for time
.domain(
d3.extent(
data,
// d3.extent returns [min, max]
(d) => d.Date
)
)
.range([margin.left - -30, width - margin.right]);
// x axis
svg
.append("g")
.attr("class", "x-axis")
.style("transform", `translate(-3px, 522px)`)
.call(d3.axisBottom(x))
.append("text")
.attr("class", "axis-label-x")
.attr("x", "55%")
.attr("dy", "4em")
// .attr("dy", "20%")
.style("fill", "black")
.text("Months");
// create y scale
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.Count)])
.range([height - margin.bottom, margin.top]);
// y axis
svg
.append("g")
.attr("class", "y-axis")
.style("transform", `translate(27px, 0px)`)
.call(d3.axisLeft(y));
// line colors
const line_colors = words.map(function (d) {
return d.key; // list of words
});
const color = d3
.scaleOrdinal()
.domain(line_colors)
.range([
"#e41a1c",
"#377eb8",
"#4daf4a",
"#984ea3",
"#ff7f00",
"#ffff33",
"#a65628",
"#f781bf",
"#999999",
"#872ff8",
]); //https://observablehq.com/#d3/d3-scaleordinal
// craete legend variable
const legend = svg
.append("g")
.attr("class", "legend")
.attr("height", 100)
.attr("width", 100)
.attr("transform", "translate(-20, 50)");
// create legend shapes and locations
legend
.selectAll("rect")
.data(words)
.enter()
.append("rect")
.attr("x", width + 65)
.attr("y", function (d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {
return color(d.key);
});
// create legend labels
legend
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i * 20 + 9;
})
// .attr("dy", "0.32em")
.text(
words.map(function (d, i) {
return d.key; // list of words
})
);
// returning an array as text
// });
svg
.selectAll(".line")
.data(words)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function (d) {
return color(d.key);
})
.attr("stroke-width", 1.5)
.attr("d", function (d) {
return d3
.line()
.x(function (d) {
return x(d.Date);
})
.y(function (d) {
return y(d.Count);
})(d.values);
});
});
Image of the problem:
P.S. I cannot add a JSfiddle because I am hosting this page on a web server, as that is the only way chrome can read in my CSV containing the data.
My Temporary Solution
function leg_labels() {
let the_word = "";
let num = 0;
for (i = 0; i < words.length; i++) {
the_word = words[i].key;
num += 50;
d3.selectAll(".legend")
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i + num;
})
// .attr("dy", "0.32em")
.text(the_word);
}
}
leg_labels();
Problem
Your problem has to do with this code
legend
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i * 20 + 9;
})
// .attr("dy", "0.32em")
.text(
words.map(function (d, i) {
return d.key; // list of words
})
);
You are appending only a single text element and in the text function you are returning the complete array of words, which is why all words are shown.
Solution
Create a corresponding text element for each legend rectangle and provide the correct word. There are multiple ways to go about it.
You could use foreignObject to append HTML inside your SVG, which is very helpful for text, but for single words, plain SVG might be enough.
I advise to use a g element for each legend item. This makes positioning a lot easier, as you only need to position the rectangle and text relative to the group, not to the whole chart.
Here is my example:
let legendGroups = legend
.selectAll("g.legend-item")
.data(words)
.enter()
.append("g")
.attr("class", "legend-item")
.attr("transform", function(d, i) {
return `translate(${width + 65}px, ${i * 20}px)`;
});
legendGroups
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {
return color(d.key);
});
legendGroups
.append("text")
.attr("x", 20)
.attr("y", 9)
.text(function(d, i) { return words[i].key; });
This should work as expected.
Please note the use of groups for easier positioning.

d3.scaleTime dates between 1890 and 1910 appear as :00 (d3.v4)

I am creating a line plot in d3 using an example I found here -> https://www.d3-graph-gallery.com/graph/line_brushZoom.html
My data contains observations from 1890 through to 2018 in the following format:
1880-01-01,1
1890-01-01,3
1890-02-02,1
1890-02-17,1
1890-03-29,1
1890-04-04,1
1890-05-04,1
1890-06-02,1
1890-06-05,1
1890-06-11,1
1890-07-01,1
1890-10-28,1
1890-12-24,1
1890-12-25,1
1891-01-29,1
1891-03-03,1
1891-06-07,1
1892-05-09,1
1893-08-20,1
1893-10-06,1
1894-03-28,1
1895-10-17,1
1896-05-25,1
1897-02-05,1
1897-07-29,1
1897-08-26,1
1898-07-05,1
1900-01-01,1
1900-08-12,1
1901-09-21,1
1903-08-16,1
1903-09-23,1
1904-02-13,1
1904-09-02,1
1904-09-04,1
1905-05-08,1
1905-07-06,1
1905-11-19,1
1906-09-24,1
1908-02-03,1
1909-01-01,1
1910-09-26,1
I noticed that the x axis scale renders dates between 1890 and 1910 with the following ticks :00
rather than 1890, 1900, 1910
The original chart code gives the following line to set the
// Add X axis --> it is a date format
var x = d3.scaleTime()
//.domain(d3.extent(data, function(d) { return d.date; }))// original line
.domain([new Date(1880, 0, 1), new Date(2018, 0, 1)]) // debugline
.range([ 0, width ]);
xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
d3.v5 does it. little different
const xScale = d3.scaleTime().range([0,width]);
const yScale = d3.scaleLinear().rangeRound([height, 0]);
xScale.domain(d3.extent(data, function(d){
return timeConv(d.date)}));
yScale.domain([(0), d3.max(slices, function(c) {
return d3.max(c.values, function(d) {
return d.measurement + 4; });
})
]);
I can't figure out what the problem is, I tried the same data in d3.v5 and I can't reproduce the problem. I am wondering if there is some additional parsing that is required for distinct types?
Thanks
Jonathan
PS Full code was requested (copy and pasted from the d3-gallery)
<!-- Code from d3-graph-gallery.com -->
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<script>
// set the dimensions and margins of the graph
var margin = {top: 50, right: 30, bottom: 30, left: 60},
width = 900 - margin.left - margin.right,
height = 600 - 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 + ")");
//Read the data
d3.csv("all_cases.csv",
// When reading the csv, I must format variables:
function(d){
return { date : d3.timeParse("%Y-%m-%d")(d.date), value : d.value }
},
// Now I can use this dataset:
function(data) {
// Add X axis --> it is a date format
var x = d3.scaleTime()
// .domain(d3.extent(data, function(d) { return d.date; }))
.domain([new Date(1880, 0, 1), new Date(2018, 0, 1)])
.range([ 0, width ]);
xAxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, d3.max(data, function(d) { return +d.value; })+5])
.range([ height, 0 ]);
yAxis = svg.append("g")
.call(d3.axisLeft(y));
// Add a clipPath: everything out of this area won't be drawn.
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width )
.attr("height", height )
.attr("x", 0)
.attr("y", 0);
// Add brushing
var brush = d3.brushX() // Add the brush feature using the d3.brush function
.extent( [ [0,0], [width,height] ] ) // initialise the brush area: start at 0,0 and finishes at width,height: it means I select the whole graph area
.on("end", updateChart) // Each time the brush selection changes, trigger the 'updateChart' function
// Create the line variable: where both the line and the brush take place
var line = svg.append('g')
.attr("clip-path", "url(#clip)")
// Add the line
line.append("path")
.datum(data)
.attr("class", "line") // I add the class line to be able to modify this line later on.
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
.attr("d", d3.line()
.x(function(d) { return x(d.date) })
.y(function(d) { return y(d.value) })
)
// Add the brushing
line
.append("g")
.attr("class", "brush")
.call(brush);
// A function that set idleTimeOut to null
var idleTimeout
function idled() { idleTimeout = null; }
// A function that update the chart for given boundaries
function updateChart() {
// What are the selected boundaries?
extent = d3.event.selection
// If no selection, back to initial coordinate. Otherwise, update X axis domain
if(!extent){
if (!idleTimeout) return idleTimeout = setTimeout(idled, 350); // This allows to wait a little bit
x.domain([ 4,8])
}else{
x.domain([ x.invert(extent[0]), x.invert(extent[1]) ])
line.select(".brush").call(brush.move, null) // This remove the grey brush area as soon as the selection has been done
}
// Update axis and line position
xAxis.transition().duration(1000).call(d3.axisBottom(x))
line
.select('.line')
.transition()
.duration(1000)
.attr("d", d3.line()
.x(function(d) { return x(d.date) })
.y(function(d) { return y(d.value) })
)
}
// If user double click, reinitialize the chart
svg.on("dblclick",function(){
x.domain(d3.extent(data, function(d) { return d.date; }))
xAxis.transition().call(d3.axisBottom(x))
line
.select('.line')
.transition()
.attr("d", d3.line()
.x(function(d) { return x(d.date) })
.y(function(d) { return y(d.value) })
)
});
})
svg.append("text")
.attr("x", 400)
.attr("y", -5)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("text-decoration", "solid")
.text("Outbreaks 1890 - 2018");
</script>
You're using D3 v4, and the axis works with D3 v5:
<script src="https://d3js.org/d3.v4.min.js"></script>
To make your code work with v5, you have to change the syntax of d3.csv slightly, as it uses promises in v5. You don't need to change anything else.
d3.csv("all_cases.csv",
function(d){
return { date : d3.timeParse("%Y-%m-%d")(d.date), value : d.value }
})
.then(function(data) {
// code
})

d3 Update stacked bar graph using selection.join

I am creating a stacked bar graph which should update on changing data and I want to use d3v5 and selection.join as explained here https://observablehq.com/#d3/learn-d3-joins?collection=#d3/learn-d3.
When entering the data everything works as expected, however the update function is never called (that's the console.log() for debugging.).
So it looks like it is just entering new data all the time.
How can I get this to work as expected?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
.y.axis .domain {
display: none;
}
</style>
</head>
<body>
<div id="chart"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
let xVar = "year";
let alphabet = "abcdef".split("");
let years = [1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003];
let margin = { left:80, right:20, top:50, bottom:100 };
let width = 600 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
let g = d3.select("#chart")
.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 + ")");
let color = d3.scaleOrdinal(["#66c2a5","#fc8d62","#8da0cb","#e78ac3","#a6d854","#ffd92f"])
let x = d3.scaleBand()
.rangeRound([0, width])
.domain(years)
.padding(.25);
let y = d3.scaleLinear()
.rangeRound([height, 0]);
let xAxis = d3.axisBottom(x);
let yAxis = d3.axisRight(y)
.tickSize(width)
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
g.append("g")
.attr("class", "y axis")
.call(yAxis);
let stack = d3.stack()
.keys(alphabet)
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone);
redraw(randomData());
d3.interval(function(){
redraw(randomData());
}, 1000);
function redraw(data){
// update the y scale
y.domain([0, d3.max(data.map(d => d.sum ))])
g.select(".y")
.transition().duration(1000)
.call(yAxis);
groups = g.append('g')
.selectAll('g')
.data(stack(data))
.join('g')
.style('fill', (d,i) => color(d.key));
groups.selectAll('.stack')
.data(d => d)
.attr('class', 'stack')
.join(
enter => enter.append('rect')
.data(d => d)
.attr('x', d => x(d.data.year))
.attr('y', y(0))
.attr('width', x.bandwidth())
.call(enter => enter.transition().duration(1000)
.attr('y', d => y(d[1]))
.attr('height', d => y(d[0]) - y(d[1]))
),
update => update
.attr('x', d => x(d.data.year))
.attr('y', y(0))
.attr('width', x.bandwidth())
.call(update => update.transition().duration(1000)
.attr('y', d => y(d[1]))
.attr('height', d => y(d[0]) - y(d[1]))
.attr(d => console.log('update stack'))
)
)
}
function randomData(data){
return years.map(function(d){
let obj = {};
obj.year = d;
let nums = [];
alphabet.forEach(function(e){
let num = Math.round(Math.random()*2);
obj[e] = num;
nums.push(num);
});
obj.sum = nums.reduce((a, b) => a + b, 0);
return obj;
});
}
</script>
</body>
</html>
Here is it in a working jsfiddle: https://jsfiddle.net/blabbath/yeq5d1tp/
EDIT: I provided a wrong link first, here is the right one.
My example is heavily based on this: https://bl.ocks.org/HarryStevens/7e3ec1a6722a153a5d102b6c42f4501d
I had the same issue a few days ago. The way I did it is as follows:
We have two .join, the parent one is for the stack and the child is for the rectangles.
In the enter of the parent join, we call the updateRects in order to draw the rectangles for the first time, this updateRects function will do the child .join, this second join function will draw the rectangles.
For the update we do the same, but instead of doing it in the enter function of the join, we do it in the update.
Also, my SVG is structured in a different way, I have a stacks groups, then I have the stack group, and a bars group, in this bars group I add the rectangles. In the fiddle below, you can see that I added the parent group with the class stack.
The two functions are below:
updateStack:
function updateStack(data) {
// update the y scale
y.domain([0, d3.max(data.map((d) => d.sum))]);
g.select(".y").transition().duration(1000).call(yAxis);
const stackData = stack(data);
stackData.forEach((stackedBar) => {
stackedBar.forEach((stack) => {
stack.id = `${stackedBar.key}-${stack.data.year}`;
});
});
let bars = g
.selectAll("g.stacks")
.selectAll(".stack")
.data(stackData, (d) => {
return d.key;
});
bars.join(
(enter) => {
const barsEnter = enter.append("g").attr("class", "stack");
barsEnter
.append("g")
.attr("class", "bars")
.attr("fill", (d) => {
return color(d.key);
});
updateRects(barsEnter.select(".bars"));
return enter;
},
(update) => {
const barsUpdate = update.select(".bars");
updateRects(barsUpdate);
},
(exit) => {
return exit.remove();
}
);
}
updateRects:
function updateRects(childRects) {
childRects
.selectAll("rect")
.data(
(d) => d,
(d) => d.id
)
.join(
(enter) =>
enter
.append("rect")
.attr("id", (d) => d.id)
.attr("class", "bar")
.attr("x", (d) => x(d.data.year))
.attr("y", y(0))
.attr("width", x.bandwidth())
.call((enter) =>
enter
.transition()
.duration(1000)
.attr("y", (d) => y(d[1]))
.attr("height", (d) => y(d[0]) - y(d[1]))
),
(update) =>
update.call((update) =>
update
.transition()
.duration(1000)
.attr("y", (d) => y(d[1]))
.attr("height", (d) => y(d[0]) - y(d[1]))
),
(exit) =>
exit.call((exit) =>
exit
.transition()
.duration(1000)
.attr("y", height)
.attr("height", height)
)
);
}
Here is an update jsfiddle https://jsfiddle.net/5oqwLxdj/1/
I hope it helps.
It is getting called, but you can't see it. Try changing .attr(d => console.log('update stack')) to .attr(console.log('update stack')).

D3 Stack Line Chart

I'm working on a multi-line line chart in D3 (version 4) right now, and having some trouble. Here's what I am getting with my current code:
I think it has something to do with my x scale, but I can't figure out what is wrong. I've been following the tutorial here, with slight modifications because I am working with V4. Any help would be greatly appreciated.
var parseDate = d3.timeParse("%Y");
var color = d3.scaleOrdinal(["#969FFD","#7173BF","#4B4C7F","#262640", "red"]);
var mySvg = d3.select("#chart8").append('svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.bottom + margin.top)
var chartGroup = mySvg.append("g")
.attr("class","fullGroup")
.attr("transform", "translate("+margin.left+","+margin.top+")");
var parseDate = d3.timeParse("%Y");
d3.csv("../Assets/datasets/allDrugs3.csv", function(error, data){
if (error) throw error;
// FORMAT THE DATA //
var labelVar = "year";
var varNames = d3.keys(data[0]).slice(1);
// Alternatively --> .filter(function(key){ return key !== labelVar;})
console.log(varNames); // <-- Names of drugs, used for color array
// Add color domain of names
color.domain(varNames);
var seriesData = varNames.map(function(name){
return {
name: name,
values: data.map(function(d) {
return { name: name, label: parseDate(d[labelVar]), value: +d[name]};
})
};
});
console.log(seriesData);
// Y-SCALE //
var yScale = d3.scaleLinear()
.domain([
d3.min(seriesData, function(c){
return d3.min(c.values, function(d){ return d.value; });
}),
d3.max(seriesData, function(c){
return d3.max(c.values, function(d){ return d.value;});
})
])
.range([height, 0]);
console.log(
"The y domain is",
d3.min(seriesData, function(c){
return d3.min(c.values, function(d){ return d.value; })}),
"to",
d3.max(seriesData, function(c){
return d3.max(c.values, function(d){ return d.value;});
}));
// X-SCALE //
var xYears = data.map(function(d){return parseDate(d.year);});
var xScale = d3.scaleTime()
.domain(xYears)
.range([0, width]);
console.log(
"The x domain is",
xYears
)
var series = chartGroup.selectAll(".series")
.data(seriesData)
.enter().append("g")
.attr("class","series");
var line = d3.line()
.x(function(d){return xScale(d.label)})
.y(function(d){return yScale(d.value); })
.curve(d3.curveCardinal);
series.append("path")
.attr("d", function(d){ return line(d.values); })
.style("stroke", function (d) { return color(d.name); })
.style("stroke-width", "4px")
.style("fill", "none");
// Axes
var yAxis = d3.axisLeft(yScale).ticks(5).tickPadding(5);
var xAxis = d3.axisBottom(xScale).ticks(10);
chartGroup.append("g")
.attr("class","Xaxis")
.call(xAxis)
.attr("transform","translate(0,"+height+")")
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)");
chartGroup.append("g")
.attr("class","Yaxis")
.call(yAxis)
.selectAll("text")
.style("text-anchor", "middle")
.attr("dy", "-1em")
.attr("transform", "rotate(-90)");
});
Here's the data:
key,deaths,year
heroin,289,2007
heroin,360,2008
heroin,238,2009
heroin,247,2010
heroin,392,2011
heroin,399,2012
heroin,464,2013
heroin,578,2014
heroin,748,2015
heroin,1212,2016
opiods,280,2007
opiods,251,2008
opiods,311,2009
opiods,342,2010
opiods,311,2011
opiods,302,2012
opiods,316,2013
opiods,330,2014
opiods,351,2015
opiods,418,2016
alchohol,175,2007
alchohol,162,2008
alchohol,160,2009
alchohol,161,2010
alchohol,195,2011
alchohol,187,2012
alchohol,238,2013
alchohol,270,2014
alchohol,310,2015
alchohol,582,2016
benzodiazepine,48,2007
benzodiazepine,52,2008
benzodiazepine,58,2009
benzodiazepine,68,2010
benzodiazepine,73,2011
benzodiazepine,37,2012
benzodiazepine,69,2013
benzodiazepine,103,2014
benzodiazepine,91,2015
benzodiazepine,126,2016
cocaine,157,2007
cocaine,162,2008
cocaine,135,2009
cocaine,148,2010
cocaine,153,2011
cocaine,248,2012
cocaine,154,2013
cocaine,198,2014
cocaine,221,2015
cocaine,463,2016
There's a lot here, it might be easier just to post some working code. You idea is good, but there's a couple places it's breaking:
var varNames = d3.keys(data[0]).slice(1);
// Alternatively --> .filter(function(key){ return key !== labelVar;})
console.log(varNames); // <-- Names of drugs, used for color array
If you look at the console, you'll see this isn't giving you the drug names; it's giving you part of the first row of data, which isn't really useful.
For data in the format you have, one option is to use d3.nest(). This will take your array and give a new array grouped by the key of your choice. In this case we can group by drug like this:
var nested = d3.nest()
.key(d => d.key)
.map(data)
Now nested will be an array of objects like:
[{alchohol: [{key: "alchohol", deaths: "175", year: "2007"},...},
{benzodiazepine: [{key: "benzodiazepine", deaths: "48", year: "2007"}...
]
This makes the rest very natural. You can get the keys with nested.keys() and the arrays with .entries()
One thing to keep in mind with scales is that d3.max() returns the max in natural order — you need to to force the numerical order in the scale with .domain([0 , d3.max(data, d => +d.deaths) ])
Once that's all straightened out you can just pass nested into the data() function, and .entries() into your line generator and everything works pretty well.
Here's a working example that should help:
<html>
<head>
<script src="d3/d3.min.js"></script>
<script src="d3-selection-multi/d3-selection-multi.min.js"></script>
<style>
#chart div {
background-color: steelblue;
color: white;
padding: 8px;
text-align: right;
margin:1px;
font: 10px sans-serf
}
div#graphic {
display: inline-block;
/* border: 1px solid #333;*/
}
.title {
fill: #666;
font-family: Arial, Helvetica, sans-serif;
text-anchor: middle;
font-size: 24px;
}
.axis .domain, .axis .tick{
stroke: #000;
fill: none;
}
.bar {
fill:steelblue;
stroke: #444;
}
</style>
</head>
<body>
<div id ="chart"></div>
<div id="chart8"></div>
<button onclick="refresh()">refresh</button>
<script>
data = [{"key":"heroin","deaths":"289","year":"2007"},{"key":"heroin","deaths":"360","year":"2008"},{"key":"heroin","deaths":"238","year":"2009"},{"key":"heroin","deaths":"247","year":"2010"},{"key":"heroin","deaths":"392","year":"2011"},{"key":"heroin","deaths":"399","year":"2012"},{"key":"heroin","deaths":"464","year":"2013"},{"key":"heroin","deaths":"578","year":"2014"},{"key":"heroin","deaths":"748","year":"2015"},{"key":"heroin","deaths":"1212","year":"2016"},{"key":"opiods","deaths":"280","year":"2007"},{"key":"opiods","deaths":"251","year":"2008"},{"key":"opiods","deaths":"311","year":"2009"},{"key":"opiods","deaths":"342","year":"2010"},{"key":"opiods","deaths":"311","year":"2011"},{"key":"opiods","deaths":"302","year":"2012"},{"key":"opiods","deaths":"316","year":"2013"},{"key":"opiods","deaths":"330","year":"2014"},{"key":"opiods","deaths":"351","year":"2015"},{"key":"opiods","deaths":"418","year":"2016"},{"key":"alchohol","deaths":"175","year":"2007"},{"key":"alchohol","deaths":"162","year":"2008"},{"key":"alchohol","deaths":"160","year":"2009"},{"key":"alchohol","deaths":"161","year":"2010"},{"key":"alchohol","deaths":"195","year":"2011"},{"key":"alchohol","deaths":"187","year":"2012"},{"key":"alchohol","deaths":"238","year":"2013"},{"key":"alchohol","deaths":"270","year":"2014"},{"key":"alchohol","deaths":"310","year":"2015"},{"key":"alchohol","deaths":"582","year":"2016"},{"key":"benzodiazepine","deaths":"48","year":"2007"},{"key":"benzodiazepine","deaths":"52","year":"2008"},{"key":"benzodiazepine","deaths":"58","year":"2009"},{"key":"benzodiazepine","deaths":"68","year":"2010"},{"key":"benzodiazepine","deaths":"73","year":"2011"},{"key":"benzodiazepine","deaths":"37","year":"2012"},{"key":"benzodiazepine","deaths":"69","year":"2013"},{"key":"benzodiazepine","deaths":"103","year":"2014"},{"key":"benzodiazepine","deaths":"91","year":"2015"},{"key":"benzodiazepine","deaths":"126","year":"2016"},{"key":"cocaine","deaths":"157","year":"2007"},{"key":"cocaine","deaths":"162","year":"2008"},{"key":"cocaine","deaths":"135","year":"2009"},{"key":"cocaine","deaths":"148","year":"2010"},{"key":"cocaine","deaths":"153","year":"2011"},{"key":"cocaine","deaths":"248","year":"2012"},{"key":"cocaine","deaths":"154","year":"2013"},{"key":"cocaine","deaths":"198","year":"2014"},{"key":"cocaine","deaths":"221","year":"2015"},{"key":"cocaine","deaths":"463","year":"2016"}]
var margin = {
top: 20,
bottom: 20,
left: 20,
right: 20
}
var width = 800 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom
var parseDate = d3.timeParse("%Y");
var color = d3.scaleOrdinal(["#969FFD","#7173BF","#4B4C7F","#262640", "red"]);
var mySvg = d3.select("#chart8").append('svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.bottom + margin.top)
var chartGroup = mySvg.append("g")
.attr("class","fullGroup")
.attr("transform", "translate("+margin.left+","+margin.top+")");
var parseDate = d3.timeParse("%Y");
d3.csv("./allDrugs3.csv", function(error, data){
console.log(JSON.stringify(data))
if (error) throw error;
// FORMAT THE DATA //
var nested = d3.nest()
.key(d => d.key)
.map(data)
console.log("nest", nested.keys()) // <-- Names of drugs, used for color array
console.log(nested) // <-- the actual data
color.domain(nested);
// Y-SCALE //
var yScale = d3.scaleLinear()
.domain([0 , d3.max(data, d => +d.deaths) ])
.range([height, 0]);
// X-SCALE //
var xScale = d3.scaleTime()
.domain(d3.extent(data, d => d.year))
.range([0, width])
var line = d3.line()
.x(d => xScale(d.year))
.y(d => yScale(d.deaths))
.curve(d3.curveCardinal);
var series = chartGroup.selectAll(".series")
.data(nested.entries())
.enter().append("g")
.attr("class","series")
.append("path")
.attr("d", d => {console.log(d); return line(d.value)})
.style("stroke", function (d) { return color(d.key); })
.style("stroke-width", "4px")
.style("fill", "none");
// Axes
var yAxis = d3.axisLeft(yScale).ticks(5).tickPadding(5);
var xAxis = d3.axisBottom(xScale).ticks(10);
chartGroup.append("g")
.attr("class","Xaxis")
.call(xAxis)
.attr("transform","translate(0,"+height+")")
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-65)");
chartGroup.append("g")
.attr("class","Yaxis")
.call(yAxis)
.selectAll("text")
.style("text-anchor", "middle")
.attr("dy", "-1em")
.attr("transform", "rotate(-90)");
});
</script>
</body>
</html>

How to get the index of the data element in a histogram on mouseover?

I have a d3.v3.min.js histogram created using this as reference Histogram chart using d3 and I'd like to highlight in a separate plot (scatter plot) all the points that fall within one bar of this histogram. To this end I hook on the mouseover event of the rectangle to get the values within one bin. This works fine but I can't get their indices from the original input array:
var data = d3.layout.histogram().bins(xTicks)(values);
bar.append("rect")
.attr("x", 1)
.attr("width", (x(data[0].dx) - x(0)) - 1)
.attr("height", function(d) { return height - y(d.y); })
.attr("fill", function(d) { return colorScale(d.y) })
.on("mouseover", function (d, i) { console.log(d); });
d is an array containing all the values within the bin, and i is the bin index. I need the indices of the original data values I passed to the histogram function so that I can look them up in the other plot by index (as opposed to a binary search needed on the value).
Instead of just passing number values to the histogram generator you could create an array of objects carrying additional information:
// Generate a 1000 data points using normal distribution with mean=20, deviation=5
var f = d3.random.normal(20, 5);
// Create full-fledged objects instead of mere numbers.
var values = d3.range(1000).map(id => ({
id: id,
value: f()
}));
// Accessor function for the objects' value property.
var valFn = d => d.value;
// Generate a histogram using twenty uniformly-spaced bins.
var data = d3.layout.histogram()
.bins(x.ticks(20))
.value(valFn) // Provide accessor function for histogram generation
(values);
By providing an accessor function to the histogram generator you are then able to create the bins from this array of objects. Calling the histogram generator will consequently result in bins filled with objects instead of just raw numbers. In an event handler you are then able to access your data objects by reference. The objects will carry all the initial information, be it the id property as in my example, an index or anything else you put in them in the first place.
Have a look at the following snippet for a working demo:
var color = "steelblue";
var f = d3.random.normal(20, 5);
// Generate a 1000 data points using normal distribution with mean=20, deviation=5
var values = d3.range(1000).map(id => ({
id: id,
value: f()
}));
var valFn = d => d.value;
// A formatter for counts.
var formatCount = d3.format(",.0f");
var margin = {top: 20, right: 30, bottom: 30, left: 30},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var max = d3.max(values, valFn);
var min = d3.min(values, valFn);
var x = d3.scale.linear()
.domain([min, max])
.range([0, width]);
// Generate a histogram using twenty uniformly-spaced bins.
var data = d3.layout.histogram()
.bins(x.ticks(20))
.value(valFn)
(values);
var yMax = d3.max(data, function(d){return d.length});
var yMin = d3.min(data, function(d){return d.length});
var colorScale = d3.scale.linear()
.domain([yMin, yMax])
.range([d3.rgb(color).brighter(), d3.rgb(color).darker()]);
var y = d3.scale.linear()
.domain([0, yMax])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var svg = d3.select("body").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 + ")");
var bar = svg.selectAll(".bar")
.data(data)
.enter().append("g")
.attr("class", "bar")
.attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; })
.on("mouseover", d => { console.log(d)});
bar.append("rect")
.attr("x", 1)
.attr("width", (x(data[0].dx) - x(0)) - 1)
.attr("height", function(d) { return height - y(d.y); })
.attr("fill", function(d) { return colorScale(d.y) });
bar.append("text")
.attr("dy", ".75em")
.attr("y", -12)
.attr("x", (x(data[0].dx) - x(0)) / 2)
.attr("text-anchor", "middle")
.text(function(d) { return formatCount(d.y); });
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
/*
* Adding refresh method to reload new data
*/
function refresh(values){
// var values = d3.range(1000).map(d3.random.normal(20, 5));
var data = d3.layout.histogram()
.value(valFn)
.bins(x.ticks(20))
(values);
// Reset y domain using new data
var yMax = d3.max(data, function(d){return d.length});
var yMin = d3.min(data, function(d){return d.length});
y.domain([0, yMax]);
var colorScale = d3.scale.linear()
.domain([yMin, yMax])
.range([d3.rgb(color).brighter(), d3.rgb(color).darker()]);
var bar = svg.selectAll(".bar").data(data);
// Remove object with data
bar.exit().remove();
bar.transition()
.duration(1000)
.attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; });
bar.select("rect")
.transition()
.duration(1000)
.attr("height", function(d) { return height - y(d.y); })
.attr("fill", function(d) { return colorScale(d.y) });
bar.select("text")
.transition()
.duration(1000)
.text(function(d) { return formatCount(d.y); });
}
// Calling refresh repeatedly.
setInterval(function() {
var values = d3.range(1000).map(id => ({
id: id,
value: f()
}));
refresh(values);
}, 2000);
body {
font: 10px sans-serif;
}
.bar rect {
shape-rendering: crispEdges;
}
.bar text {
fill: #999999;
}
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.as-console-wrapper {
height: 20%;
}
<script src="https://d3js.org/d3.v3.min.js"></script>

Categories