I have zero experience using d3 and I use Javascript and jQuery only sporadically.
I am trying to create a simple scatterplot with a slider in d3 (and jQuery). The purpose of the slider is to select the dataset to plot. I have a JSON object all_data, a list of maps. For each map in the list, I use the "SI" and "F1" lists as x and y values, respectively.
When the slider is moved, I call the plotData() function with as argument an integer number that represents the index of the dataset in all_data (So plotData(5) would plot the 6th dataset in all_data).
Now, for some reason, the plot axes construct nicely, but the data points do not plot. I have tried to debug the code by placing console.log(data) and
function (d,i) { console.log(d); return x(d['SI']) at relevant sections in the code (see the comment lines). The data object contains data at all points in the function. The problem seems to arise after g.selectAll("scatter-dots") part. I suspect the final call to the function plotting the 'dots' is not called, but I cannot figure out why.
My question is: how can I fix my code such that the data points are added to the plot? Any other tips on coding, design or performance improvements are more than welcome - here to learn.
EDIT: I use a custom d3.slider extension from GitHub
var all_data = [{"REGRESSION_Y": [ list_of_floats ], "F1": [ list_of_floats ], "SI": [ list_of_floats ], "f+": some_float_val }
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>D3 Test</title>
<link rel="stylesheet" href="d3/d3-slider-master/d3.slider.css" />
<link rel="stylesheet" href="plot.css" />
<script type="text/javascript" src="jquery/jquery-3.1.1.min.js"></script>
<script type="text/javascript" src="d3/d3.v3.min.js"></script>
<script type="text/javascript" src="json_data.js"></script>
<script type="text/javascript" src="d3/d3-slider-master/d3.slider.js"></script>
</head>
<body>
<h2><span id="slider3text">Data points selected: 5</span></h2>
<div id='container' style='width: 75%; height: 100%' class='centered'>
<div id="plotfield" style='width: 100%; height: 90%'></div>
<div id="slider3" style='height: auto'></div>
</div>
</body>
<script type="text/javascript">
d3.select('#slider3').call(d3.slider().axis(true).min( 5 ).max( all_data.length-1 ).step(1).on("slide", function(evt, value) {
d3.select('#slider3text').text("Data points selected: " + value);
plotData(value)
}));
</script>
<script type="text/javascript">
function plotData(i) {
var data = all_data[i]
$("#plotfield").empty()
var margin = {top: 50, right: 5, bottom: 80, left: 5}
, width = $("#container").width() - margin.left - margin.right
, height = $("#container").height() - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d['SI']; })])
.range([ 0, width ]);
var y = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d['F1']; })])
.range([ height, 0 ]);
var chart = d3.select('#plotfield')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart')
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main')
// draw the x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(xAxis);
// draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(yAxis);
var g = main.append("svg:g");
//console.log(data), this worked here
g.selectAll("scatter-dots")
.data(data)
.enter().append("svg:circle")
// I have tried function (d,i) { console.log(d); return x(d['SI'])
//in the below anonymous functions, but that doesn't yield an output,
//causing me to suspect that the functions are not executed.
.attr("cx", function (d,i) { return x(d['SI']); } )
.attr("cy", function (d) { return y(d['F1']); } )
.attr("r", 3);
}
and the CSS file:
.centered {
position: fixed;
top: 50%;
left: 50%;
/* bring your own prefixes */
transform: translate(-50%, -50%);
}
body {
font: 11px sans-serif;
}
.tooltip {
position: absolute;
width: 200px;
height: 28px;
pointer-events: none;
}
.axis line, .axis path {
shape-rendering: crispEdges;
stroke: black;
fill: none;
}
circle {
fill: steelblue;
}
From what I see the data[i] returned an object. This is of course not a problem, but the d3 method enter() only supports arrays.
This means that you cannot have things like d['f1'] when using enter() to add data. Converting them into arrays before using them fixes this and you can use d (the array entry) or i the array entry index.
The problem turned out to be the fact dat the var data = all_data[i] returned an object, instead of an array. A colleague of mine provided the following edit to make it work. (Also some other code was changed to add a trend line to the script).
<script type="text/javascript">
function plotData(i) {
// extracting data
var data = all_data[i];
var REGRESSION_Y = data.REGRESSION_Y;
var F1 = data.F1;
var SI = data.SI;
// removing previous plot
$("#plotfield").empty()
// margins and spacings (slightly larger, y-axis was invinsible)
var margin = {top: 50, right: 15, bottom: 80, left: 15}
, width = $("#container").width() - margin.left - margin.right
, height = $("#container").height() - margin.top - margin.bottom;
var x = d3.scale.linear()
.domain([0, d3.max(SI)])
.range([ 0, width ]);
var y = d3.scale.linear()
.domain([0, d3.max(F1)])
.range([ height, 0]);
var chart = d3.select('#plotfield')
.append('svg:svg')
.attr('width', width + margin.right + margin.left)
.attr('height', height + margin.top + margin.bottom)
.attr('class', 'chart')
var main = chart.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('width', width)
.attr('height', height)
.attr('class', 'main')
// draw the x axis
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom');
main.append('g')
.attr('transform', 'translate(0,' + height + ')')
.attr('class', 'main axis date')
.call(xAxis);
// draw the y axis
var yAxis = d3.svg.axis()
.scale(y)
.orient('left');
main.append('g')
.attr('transform', 'translate(0,0)')
.attr('class', 'main axis date')
.call(yAxis);
// append extra g part in svg and append to it
var g = main.append("svg:g")
// append the line
g.append('line')
.attr('x1', x(d3.min(SI)))//function(d, i) { return x(d[]) + i; })
.attr('x2', x(d3.max(SI)))//(2)//function(d, i) { return y(d['REGRESSION_Y']); });
.attr('y1', y(d3.min(REGRESSION_Y)))
.attr('y2', y(d3.max(REGRESSION_Y)))
.style('stroke', '#1B3277')
.style('stroke-width','1')
// append the dots
g.selectAll("scatter-dots")
.data(SI)
.enter().append("svg:circle")
.attr("cx", function (d,i) { return x(SI[i]); } )
.attr("cy", function (d,i) { return y(F1[i]); } )
.attr("r", 3);
}
</script>
Related
When I zoom in on my d3 line chart, I can't get daily ticks to display. The lowest tick size that will show up are weekly ticks. Maybe my domain is incorrect? Or I need to use transform.rescaleX() like shown here https://bl.ocks.org/stepheneb/29134b7a51a1ede49f6fc57c5a2a5f38
https://jsfiddle.net/urb1qvch/1/
Javascript
// Prep data
var x = d3.timeDays(new Date(2010, 06, 01), new Date(2020, 10, 30));
var y = Array.from({length: x.length}, Math.random).map(n => Math.floor(n * 10) + 5);
var data = x.map((v, i) => {
return {
"x": v,
"y": y[i]
}
});
// Use the margin convention practice
var margin = {top: 50, right: 50, bottom: 50, left: 50}
var width = 600 - margin.left - margin.right // Use the window's width
var height = 400 - margin.top - margin.bottom; // Use the window's height
// zoom function for d3v6
// adapt to v6 from this v5 block https://bl.ocks.org/LemoNode/7ac1d41fe75fe7d2d9cb85e78aad6303
var zoom = d3.zoom()
.on('zoom', (event) => {
xScale
.domain(event.transform.rescaleX(xScale2).domain())
.range([0, width].map(d => event.transform.applyX(d))); //
svg.select(".line")
.attr("d", line); // zooms line
svg.select(".x-axis")
.call(d3.axisBottom(xScale)
.tickSizeOuter(0)
.ticks(20)); // zooms axis
})
.scaleExtent([1, 32]); // adjust 32 to less zoom less
// X scale - use min and max of scale
var xScale = d3.scaleUtc()
.domain([d3.min(x), d3.max(x)]) // input
.range([0, width]); // output
// constant reference point whilst zooming
var xScale2 = d3.scaleUtc()
.domain([d3.min(x), d3.max(x)]) // input
.range([0, width]); // output
// Y scale - add 5 for bit of whitespace at top of graph
var yScale = d3.scaleLinear()
.domain([0, d3.max(y) + 5]) // input
.range([height, 0]); // output
// d3's line generator
var line = d3.line()
.x(function(d) { return xScale(d.x); }) // set the x values for the line generator
.y(function(d) { return yScale(d.y); }) // set the y values for the line generator
// Add the SVG to the page and employ #2
var svg = d3.select("#my_chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom) // call the zoom
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// clippath to stop line and x-axis 'spilling over'
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("width", width)
.attr("height", height);
// call x-axis and apply the clip from the defs
svg.append("g")
.attr("class", "x-axis")
.attr("clip-path", "url(#clip)") // add the clip path!
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)); // Create an axis component with d3.axisBottom
// Call the y-axis
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(yScale)); // Create an axis component with d3.axisLeft
// Append the path, bind the data, and call the line generator
svg.append("path")
.datum(data) // 10. Binds data to the line
.attr("class", "line") // Assign a class for styling
.attr("clip-path", "url(#clip)") // add the clip path!
.attr("d", line); // 11. Calls the line generator
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
</head>
<body>
<h1>My Chart</h1>
<div id="my_chart"></div>
</body>
</html>
CSS
.line {
fill: none;
stroke: #ffab00;
stroke-width: 1.5;
}
.overlay {
fill: none;
pointer-events: all;
}
/* Style the dots by assigning a fill and stroke */
.dot {
fill: #ffab00;
stroke: #fff;
}
.focus circle {
fill: none;
stroke: steelblue;
}
I need to design a d3 component like the one shown in the figure below.
I referred to an existing code sample from this link, and modified it to create something like this.
Left was changing the width of the axis, which I tried by changing the stroke-width property of the domain class. However, I ended with something like this.
Problems:
The slider handle isn't aligning with the axis.
The axis color imprints on the slider.
The ends of the axis are not perfectly round.
Questions:
I can't figure out what do I translate/transform to align the sliders and the axis.
I tried fiddling around with the opacity values, but didn't help.
I set stroke-linecap to round, but it's still not completely round.
I am using d3 v4 for this. And the jsfiddle for my final code is here.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
.tick{
visibility:hidden;
}
.domain {
stroke: grey;
stroke-width:10px;
stroke-linecap: round;
}
.selection {
fill:red
}
</style>
</head>
<body>
<div style="margin-left: 20px;margin-top: 20px;">
<span></span> to <span></span>
</div>
<script>
var margin = 20,
width = 400 - margin * 2,
height = 15;
// v3 = var x = d3.scale.linear()
var x = d3.scaleLinear()
.domain([0,100])
.range([0, width]);
/*
var brush = d3.svg.brush()
.x(x)
.extent([20, 50]);
*/
var brush = d3.brushX()
.extent([[0,0], [width,height]])
.on("brush", brushed);
var svg = d3.select("body").append("svg")
.attr("width", width + margin * 2)
.attr("height", 100)
.append("g")
.attr("transform", "translate(" + margin + "," + margin + ")")
.call(d3.axisBottom()
.scale(x)
.tickSize(0));
var brushg = svg.append("g")
.attr("class", "brush")
.call(brush)
// left circle
var left_text = brushg.append("text")
.attr("class", "label")
.attr("fill", "black")
.attr("text-anchor", "middle")
.text("hello world")
.attr("transform", "translate(0," + (35) + ")")
var right_text = brushg.append("text")
.attr("class", "label")
.attr("fill", "black")
.attr("text-anchor", "middle")
.text("hello world")
.attr("transform", "translate(0," + (35) + ")")
/*
Height of the brush's rect is now
generated by brush.extent():
brushg.selectAll("rect")
.attr("height", height);
*/
function brushed() {
/*
The brush attributes are no longer stored
in the brush itself, but rather in the
element it is brushing. That's where much of
the confusion around v4's brushes seems to be.
The new method is a little difficult to adapt
to, but seems more efficient. I think much of
this confusion comes from the fact that
brush.extent() still exists, but means
something completely different.
Instead of calling brush.extent() to get the
range of the brush, call
d3.brushSelection(node) on what is being
brushed.
d3.select('#start-number')
.text(Math.round(brush.extent()[0]));
d3.select('#end-number')
.text(Math.round(brush.extent()[1]));
*/
var range = d3.brushSelection(this)
.map(x.invert);
console.log('range->'+range)
d3.selectAll("span")
.text(function(d, i) {
console.log(Math.round(range[i]))
return Math.round(range[i])
})
left_text.attr("x", x(range[0]));
left_text.text(Math.round(range[0]));
right_text.attr("x", x(range[1]));
right_text.text(Math.round(range[1]));
d3.selectAll("rect").attr("dy", "-5em")
}
// v3: brushed();
brush.move(brushg, [20, 40].map(x));
</script>
</body>
</html>
The axis and the brush are actually perfectly aligned!
You can see this if you set the stroke-width to 1px:
.as-console-wrapper { max-height: 30% !important;}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
.tick{
visibility:hidden;
}
.domain {
stroke: grey;
stroke-width:1px;
stroke-linecap: round;
}
.selection {
fill:red
}
</style>
</head>
<body>
<div style="margin-left: 20px;margin-top: 20px;">
<span></span> to <span></span>
</div>
<script>
var margin = 20,
width = 400 - margin * 2,
height = 15;
// v3 = var x = d3.scale.linear()
var x = d3.scaleLinear()
.domain([0,100])
.range([0, width]);
/*
var brush = d3.svg.brush()
.x(x)
.extent([20, 50]);
*/
var brush = d3.brushX()
.extent([[0,0], [width,height]])
.on("brush", brushed);
var svg = d3.select("body").append("svg")
.attr("width", width + margin * 2)
.attr("height", 100)
.append("g")
.attr("transform", "translate(" + margin + "," + margin + ")")
.call(d3.axisBottom()
.scale(x)
.tickSize(0));
var brushg = svg.append("g")
.attr("class", "brush")
.call(brush)
// left circle
var left_text = brushg.append("text")
.attr("class", "label")
.attr("fill", "black")
.attr("text-anchor", "middle")
.text("hello world")
.attr("transform", "translate(0," + (35) + ")")
var right_text = brushg.append("text")
.attr("class", "label")
.attr("fill", "black")
.attr("text-anchor", "middle")
.text("hello world")
.attr("transform", "translate(0," + (35) + ")")
/*
Height of the brush's rect is now
generated by brush.extent():
brushg.selectAll("rect")
.attr("height", height);
*/
function brushed() {
/*
The brush attributes are no longer stored
in the brush itself, but rather in the
element it is brushing. That's where much of
the confusion around v4's brushes seems to be.
The new method is a little difficult to adapt
to, but seems more efficient. I think much of
this confusion comes from the fact that
brush.extent() still exists, but means
something completely different.
Instead of calling brush.extent() to get the
range of the brush, call
d3.brushSelection(node) on what is being
brushed.
d3.select('#start-number')
.text(Math.round(brush.extent()[0]));
d3.select('#end-number')
.text(Math.round(brush.extent()[1]));
*/
var range = d3.brushSelection(this)
.map(x.invert);
console.log('range->'+range)
d3.selectAll("span")
.text(function(d, i) {
console.log(Math.round(range[i]))
return Math.round(range[i])
})
left_text.attr("x", x(range[0]));
left_text.text(Math.round(range[0]));
right_text.attr("x", x(range[1]));
right_text.text(Math.round(range[1]));
d3.selectAll("rect").attr("dy", "-5em")
}
// v3: brushed();
brush.move(brushg, [20, 40].map(x));
</script>
</body>
</html>
So, what's happening here? The issue is that when you tell the browser to take a line (in this case it's a path, but it doesn't matter) and increase its stroke to, let's say, 100 pixels, it will increase 50 pixels to one side and 50 pixels to the other side. So, the middle of that thick axis is right on the top of the brush's rectangle.
There are several solutions here, like drawing an rectangle. If, however, you want to keep your approach of increasing the .domain stroke-width, let's break the selections and move the axis half its stroke-width down (here I'm increasing the width to 20 pixels, so it's easier to see the alignment):
.as-console-wrapper { max-height: 30% !important;}
<!DOCTYPE html>
<meta charset="utf-8">
<script src="//d3js.org/d3.v4.min.js"></script>
<!--
axes and brushes are styled out of the box,
so this is no longer needed
<style>
.axis path, .axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.brush .extent {
fill-opacity: .125;
shape-rendering: crispEdges;
}
</style>
-->
<style>
.tick {
visibility: hidden;
}
.domain {
stroke: grey;
stroke-width: 20px;
stroke-linecap: round;
}
.selection {
fill: red
}
</style>
<body>
<div style="margin-left: 20px;margin-top: 20px;">
<span></span> to <span></span>
</div>
</body>
<script>
var margin = 20,
width = 400 - margin * 2,
height = 20;
// v3 = var x = d3.scale.linear()
var x = d3.scaleLinear()
.domain([0, 100])
.range([0, width]);
/*
var brush = d3.svg.brush()
.x(x)
.extent([20, 50]);
*/
var brush = d3.brushX()
.extent([
[0, 0],
[width, height]
])
.on("brush", brushed);
var svg = d3.select("body").append("svg")
.attr("width", width + margin * 2)
.attr("height", 100);
svg.append("g")
.attr("transform", "translate(" + margin + "," + (margin + 10) + ")")
.call(d3.axisBottom()
.scale(x)
.tickSize(0));
var brushg = svg.append("g")
.attr("transform", "translate(" + margin + "," + margin + ")")
.attr("class", "brush")
.call(brush)
// left circle
var left_text = brushg.append("text")
.attr("class", "label")
.attr("fill", "black")
.attr("text-anchor", "middle")
.text("hello world")
.attr("transform", "translate(0," + (35) + ")")
var right_text = brushg.append("text")
.attr("class", "label")
.attr("fill", "black")
.attr("text-anchor", "middle")
.text("hello world")
.attr("transform", "translate(0," + (35) + ")")
/*
Height of the brush's rect is now
generated by brush.extent():
brushg.selectAll("rect")
.attr("height", height);
*/
function brushed() {
/*
The brush attributes are no longer stored
in the brush itself, but rather in the
element it is brushing. That's where much of
the confusion around v4's brushes seems to be.
The new method is a little difficult to adapt
to, but seems more efficient. I think much of
this confusion comes from the fact that
brush.extent() still exists, but means
something completely different.
Instead of calling brush.extent() to get the
range of the brush, call
d3.brushSelection(node) on what is being
brushed.
d3.select('#start-number')
.text(Math.round(brush.extent()[0]));
d3.select('#end-number')
.text(Math.round(brush.extent()[1]));
*/
var range = d3.brushSelection(this)
.map(x.invert);
console.log('range->' + range)
d3.selectAll("span")
.text(function(d, i) {
console.log(Math.round(range[i]))
return Math.round(range[i])
})
left_text.attr("x", x(range[0]));
left_text.text(Math.round(range[0]));
right_text.attr("x", x(range[1]));
right_text.text(Math.round(range[1]));
d3.selectAll("rect").attr("dy", "-5em")
}
// v3: brushed();
brush.move(brushg, [20, 40].map(x));
</script>
The path in the axis is a closed shape and stroking that gives problems. Also you don't want ticks so why not draw the "axis" yourself. Then the round edge will be drawn correct.
var svg = d3.select("body").append("svg")
.attr("width", width + margin * 2)
.attr("height", 100)
.append("g")
.attr("transform", "translate(" + margin + "," + margin + ")")
// .call(d3.axisBottom()
// .scale(x)
// .tickSize(0))
;
svg.append("path")
.attr("class", "domain")
.attr("d", `M${x(0)},0 ${x(100)},0`);
You have to match the brush extent to the stroked path surface
var margin = 20,
width = 400 - margin * 2,
height = 10; // same as stroke width
var brush = d3.brushX()
.extent([[0,-height*0.5], [width,height*0.5]])
.on("brush", brushed);
The dy attribute has no purpose
//d3.selectAll("rect").attr("dy", "-5em")
Set the fill-opacity of the selection
.selection {
fill:red;
fill-opacity: 1;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
.tick{
visibility:hidden;
}
.domain {
stroke: grey;
stroke-width:10;
stroke-linecap: round;
}
.selection {
fill:red;
fill-opacity: 1;
}
</style>
</head>
<body>
<div style="margin-left: 20px;margin-top: 20px;">
<span></span> to <span></span>
</div>
<script>
var margin = 20,
width = 400 - margin * 2,
height = 10; // same as stroke width
// v3 = var x = d3.scale.linear()
var x = d3.scaleLinear()
.domain([0,100])
.range([0, width]);
/*
var brush = d3.svg.brush()
.x(x)
.extent([20, 50]);
*/
var brush = d3.brushX()
.extent([[0,-height*0.5], [width,height*0.5]])
.on("brush", brushed);
var svg = d3.select("body").append("svg")
.attr("width", width + margin * 2)
.attr("height", 100)
.append("g")
.attr("transform", "translate(" + margin + "," + margin + ")")
// .call(d3.axisBottom()
// .scale(x)
// .tickSize(0))
;
svg.append("path")
.attr("class", "domain")
.attr("d", `M${x(0)},0 ${x(100)},0`);
var brushg = svg.append("g")
.attr("class", "brush")
.call(brush)
// left circle
var left_text = brushg.append("text")
.attr("class", "label")
.attr("fill", "black")
.attr("text-anchor", "middle")
.text("hello world")
.attr("transform", "translate(0," + (35) + ")")
var right_text = brushg.append("text")
.attr("class", "label")
.attr("fill", "black")
.attr("text-anchor", "middle")
.text("hello world")
.attr("transform", "translate(0," + (35) + ")")
/*
Height of the brush's rect is now
generated by brush.extent():
brushg.selectAll("rect")
.attr("height", height);
*/
function brushed() {
/*
The brush attributes are no longer stored
in the brush itself, but rather in the
element it is brushing. That's where much of
the confusion around v4's brushes seems to be.
The new method is a little difficult to adapt
to, but seems more efficient. I think much of
this confusion comes from the fact that
brush.extent() still exists, but means
something completely different.
Instead of calling brush.extent() to get the
range of the brush, call
d3.brushSelection(node) on what is being
brushed.
d3.select('#start-number')
.text(Math.round(brush.extent()[0]));
d3.select('#end-number')
.text(Math.round(brush.extent()[1]));
*/
var range = d3.brushSelection(this)
.map(x.invert);
//console.log('range->'+range)
d3.selectAll("span")
.text(function(d, i) {
//console.log(Math.round(range[i]))
return Math.round(range[i])
})
left_text.attr("x", x(range[0]));
left_text.text(Math.round(range[0]));
right_text.attr("x", x(range[1]));
right_text.text(Math.round(range[1]));
//d3.selectAll("rect").attr("dy", "-5em")
}
// v3: brushed();
brush.move(brushg, [20, 40].map(x));
</script>
</body>
</html>
Hi everyone i have been following some youtube tutorials on making bar charts in D3.js and i seemed to have goofed up a few things as my output went haywire
Here is my html file
<!DOCTYPE html>
<html>
<head>
<!-- meta -->
<meta charset="utf-8">
<title>My Data Record</title>
<!-- CSS stylesheet -->
<link rel="stylesheet" type="text/css" href="stylesheet.css">
<!-- D3.js CDN source -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js" charset="utf-8"></script>
</head>
<body>
<!-- Title -->
<h1 style="text-align:center;">Monthly Dispensed Amount</h1>
<!-- Your D3 code for bar graph -->
<script type="text/javascript" src="gdpBarGraph.js"></script>
</body>
</html>
and here is my javascript file
var margin = {top: 20, right: 10, bottom: 100, left:50},
width = 700 - margin.right - margin.left,
height = 500 - margin.top - margin.bottom;
var svg = d3.select("body")
.append("svg")
.attr ({
"width": width + margin.right + margin.left,
"height": height + margin.top + margin.bottom
})
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.right + ")");
// defining x and y scales
var xScale = d3.scale.ordinal()
.rangeRoundBands([0,width], 0.2, 0.2);
var yScale = d3.scale.linear()
.range([height, 0]);
// defining x axis and y axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
d3.csv("newcashdata.csv", function(error,data) {
if(error) console.log("Error: data could not be loaded!");
data.forEach(function(d) {
d.date = d.date;
d.amount = +d.amount;
console.log(d.amount);
});
// sort the values to show at which date the cash collection was the highest
data.sort(function(a,b) {
return b.amount - a.amount;
});
// Specify the domains of the x and y scales
xScale.domain(data.map(function(d) { return d.date; }) );
yScale.domain([0, d3.max(data, function(d) { return d.amount; } ) ]);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr("height", 0)
.attr("y", height)
.transition().duration(3000)
.delay( function(d,i) { return i * 200; })
.attr({
"x": function(d) { return xScale(d.date); },
"y": function(d) { return yScale(d.amount); },
"width": xScale.rangeBand(),
"height": function(d) { return height - yScale(d.amount); }
})
.style("fill", function(d,i) { return 'rgb(20, 20, ' + ((i * 30) + 100) + ')'});
svg.selectAll('text')
.data(data)
.enter()
.append('text')
.text(function(d){
return d.amount;
})
.attr({
"x": function(d){ return xScale(d.date) + xScale.rangeBand()/2; },
"y": function(d){ return yScale(d.amount)+ 12; },
"font-family": 'sans-serif',
"font-size": '13px',
"font-weight": 'bold',
"fill": 'white',
"text-anchor": 'middle'
});
// Drawing x axis and positioning the label
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.attr("dx", "-.8em")
.attr("dy", ".25em")
.attr("transform", "rotate(-60)" )
.style("text-anchor", "end")
.attr("font-size", "10px");
// Drawing y Axis and positioning the label
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("x", -height/2)
.attr("dy", "-2em")
.style("text-anchor", "middle")
.text("Amount Dispensed");
});
and lastly my stylesheet:
svg {
margin-left: auto;
margin-right: auto;
display: block;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis text{
font: Times;
font-size: 20px;
font-weight: bold;
}
here's what i am getting as output:
as its evident i made a mess out of my Y axis label "Amount Dispensed" and i can't think of a way to change that is it because of my font size in stylesheet or some other mistake in my code, any help will be highly appreciated.
edit: here is my csv file
here is the output after right axis changes
The y axis label did came back however the numbers on individual bars seem to be too big to depict is there a way to shorten them for isntance say 950000 to 950K and likewise
Here is a full fiddle
I increased your margin.left to 75 from 50. I also modified your yAxis creation to fix scale number formatting the s will change numbers into their corresponding prefix (7000 to 7k etc.)
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left").tickFormat(d3.format("s"));
The data I used was just randomly created as the issues were just in axis formatting.
I also moved over the label you added to the yAxis from y: -2em to y: -3em
Hope someone can help, I have a slight problem in that the horizontal axis label 100 gets cut off the end of the stacked horizontal barchart. I can't seem to figure out what is wrong in the code. Thanks in advance for your help. Please see code below.
<!DOCTYPE html>
-->
<html>
<head>
<title>Horizontal stacked bar</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<script type="text/javascript" src="d3/d3.js"> </script>
<style>
.axis{
font-size: 14px;
}
#h{
}
</style>
</head>
<body>
<script>
var margin = {
top: 12,
left: 15,
right: 15,
bottom: 14
};
var w = 500 - margin.left - margin.right;
var h = 300 - margin.top - margin.bottom;
var dataset = [
[
{x:0,y:20}
],
[
{x:0,y:30}
],
[
{x:0,y:50}
]
];
//Set up stack method
var stack = d3.layout.stack();
//Data, stacked
stack(dataset);
//Set up scales
var xScale = d3.scale.linear()
.domain([0,d3.max(dataset, function(d) {return d3.max(d, function(d)
{return d.y0 + d.y;}); }) ])
// note use of margin + right to get axis to scale width
.range([0, w + margin.right]);
var yScale = d3.scale.ordinal()
.domain(d3.range(dataset[0].length))
.rangeRoundBands([0,w ], 0.05);
//Easy colors accessible via a 10-step ordinal scale
var colors = d3.scale.category10();
//or make your own colour palet
var color = d3.scale.ordinal()
.range(["#1459D9", "#148DD9", "#87ceeb", "#daa520"]);
// good site for colour codes http://www.colorpicker.com/113EF2
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
;
// Add a group for each row of data
var groups = svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
.style("fill", function(d,i){return color(i);})
;
// Add a rect for each data value
var rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d) {return xScale(d.y0) ;}) //+99 will move axis right
.attr("y", 180)
.attr("height", 90)
.attr("width", yScale.rangeBand());
//Add an axis
var xAxis = d3.svg.axis()
.scale(xScale);
svg.append("g")
.call(xAxis)
;
</script>
</body>
</html>
You are really better off using the xScale for both dimensions, x and y. After all, your y is really a width. Here is what I mean:
...
//Set up scales
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function (d) {
return d3.max(d, function (d) {
return d.y0 + d.y;
});
})])
.range([0, w]); // no need to tamper with margins since w already accounts for that
...
// Add a rect for each data value
var rects = groups.selectAll("rect")
.data(function (d) {return d;})
.enter()
.append("rect")
.attr("x", function (d) {
return xScale(d.y0); // use x scale
})
.attr("y", 50)
.attr("height", 50)
.attr("width", function (d) {
return xScale(d.y); // use x scale
})
...
And here is the updated FIDDLE. You can go ahead and make changes to the right margin value and any of your data y values (I placed comments in the code to that effect) and you can see that this solution scales well.
New to D3. I am trying to modify the simple bar chart example shown here. I'm trying to update the data but am missing something fundamental. I'm trying to follow along here, where Mike talks about object constancy. Specifically, I am trying to achieve the following in my code:
Key functions can be useful for improving performance independent of transitions. For example, if you filter a large table, you can use a key function to reduce the number of DOM modifications: reorder DOM elements in the update selection rather than regenerating them. We used this technique at Square to improve the performance of merchant analytics, and it’s one of the reasons that D3 is faster than most template frameworks.
(In my case, my key function is simply ".data(data)" (which is ok, according to this post)
My code below works, but I suspect isn't the most performance friendly. For instance, frequency "70" is in both sets of data, but by "removing" the data, I am effectively redrawing it. (If I don't "remove" the data first, then another chart is drawn, rather than the old chart just getting updated data). How do I modify the code below to adhere to the key function and so that data that exists in both datasets doesn't get redrawn?
My code for the bar chart:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.bar {
fill: steelblue;
}
.x.axis path {
display: none;
}
</style>
<body>
<button id="change" name="change">Update</button>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js" type="text/javascript"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
$(document).ready(function () {
var old_data = [{"letter": "A","frequency": "50"},
{"letter": "B","frequency": "60"},
{"letter": "C","frequency": "70"}, // this also appears in new_data
{"letter": "D","frequency": "80"},
];
draw_chart(old_data);
$("#change").click(function(){
var new_data = [{"letter": "A","frequency": "10"},
{"letter": "B","frequency": "20"},
{"letter": "C","frequency": "70"}, // this appears in old_data
{"letter": "D","frequency": "30"},
];
var bar = d3.select('body').selectAll('svg').remove();// delete this line and you'll get multiple charts rather than just updating the data in the original chart
draw_chart(new_data);
});
});
</script>
<script>
function draw_chart(data){
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
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 + ")");
data.forEach(function(d) {
d.frequency = +d.frequency;
});
x.domain(data.map(function(d) { return d.letter; }));
y.domain([0, d3.max(data, function(d) { return d.frequency; })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Frequency");
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.letter); })
.attr("width", x.rangeBand())
.attr("y", height)
.attr("height","0")
.transition()
.delay(function(d, i) { return i*300 })
.duration(1000)
.attr("y", function(d) { return y(d.frequency); })
.attr("height", function(d) { return height - y(d.frequency); });
}
</script>
First, the reason why you have the line,
var bar d3.select('body')...remove() // delete this line and you'll get double...
Is beacuse in your draw_chart your always appending to the page when it's called. You need to change this line,
var svg = d3.select("body").append("svg")
to something that doesn't continually append a new svg
If I have more time i'll take a look at the main question.