Related
i am using D3 to draw line graph in JavaScript. Line of Line graph is drawn right but date on x axis is not correct and it is also showing one extra tick on x axis. I had also tried changing tick format but i failed. I don't know where i am doing wrong. please help. here is my code
var margin = {top: 50, right: 50, bottom: 50, left: 50}
, width = window.innerWidth - margin.left - margin.right // Use the window's width
, height = window.innerHeight - margin.top - margin.bottom; // Use the window's height
// The number of datapoints
var n = 4;
var xScale = d3.scaleLinear()
.domain([0, n-1]) // input
.range([0, width]); // output
var parseTime = d3.timeParse("%d-%b-%y");
// 6. Y scale will use the randomly generate number
var yScale = d3.scaleLinear()
.domain([0, 100]) // input
.range([height, 0]); // output
d3.csv("Data_vis.csv", function(data){
//console.log(data);
var nov_11_percent= 22;
var oct_16_percent= 25;
var nov_13_percent= 24;
var oct_22_percent= 21;
var dataset2=[{"date":'2018-09-11', "value": nov_11_percent},
{"date":'2018-10-16', "value": oct_16_percent},
{"date":'2018-10-22', "value": oct_22_percent},
{"date":'2018-11-13', "value": nov_13_percent}];
dataset2.forEach(function(d) {
d.date = new Date(d.date);
});
var line = d3.line()
.x(function(d, i) {
//alert(parseTime(d.date));
return xScale(d.date);
})
.y(function(d,i ) {
return yScale(d.value);
})
.curve(d3.curveMonotoneX)
xScale.domain(d3.extent(dataset2, function(d) {
return d.date; }));
yScale.domain([0, d3.max(dataset2, function(d) {
return d.value;
})]);
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 + ")");
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)
.ticks(4)
.tickFormat(d3.timeFormat("%Y-%m-%d")));
svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale));
svg.append("path")
.datum(dataset2)
.attr("class", "line")
.attr("d", line);
svg.selectAll(".dot")
.data(dataset2)
.enter().append("circle") // Uses the enter().append() method
.attr("class", "dot") // Assign a class for styling
.attr("cx", function(d) { return xScale(d.date) })
.attr("cy", function(d) { return yScale(d.value) })
.attr("r", 10);
});
any help would be much appreciated. thank you
Use a point scale instead of a linear scale for x.
var xScale = d3
.scalePoint()
.range([0, width])
.domain(dataset2.map(i => i.date))
const margin = { top: 50, right: 50, bottom: 50, left: 50 }
const width = window.innerWidth - margin.left - margin.right // Use the window's width
const height = window.innerHeight - margin.top - margin.bottom // Use the window's height
const dataset2 = [{ date: "2018-09-11", value: 22 },{ date: "2018-10-16", value: 25 },{ date: "2018-10-22", value: 24 },{ date: "2018-11-13", value: 21 }]
dataset2.forEach(function(d) {
d.date = new Date(d.date)
})
// 6. Y scale will use the randomly generate number
const yScale = d3
.scaleLinear()
.domain([
0,
d3.max(dataset2, function(d) {
return d.value
})
]) // input
.range([height, 0]) // output
const xScale = d3
.scalePoint()
.range([0, width])
.domain(dataset2.map(d => d.date))
const line = d3
.line()
.x(function(d, i) {
return xScale(d.date)
})
.y(function(d, i) {
return yScale(d.value)
})
.curve(d3.curveMonotoneX)
const 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 + ")")
svg
.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(
d3
.axisBottom(xScale)
.tickFormat(d3.timeFormat("%m/%d"))
)
svg
.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(yScale))
svg
.append("path")
.datum(dataset2)
.attr("class", "line")
.attr("d", line)
.attr("fill", "none")
.attr("stroke", "black")
svg
.selectAll(".dot")
.data(dataset2)
.enter()
.append("circle") // Uses the enter().append() method
.attr("class", "dot") // Assign a class for styling
.attr("cx", function(d) {
return xScale(d.date)
})
.attr("cy", function(d) {
return yScale(d.value)
})
.attr("r", 10)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Codepen
In my code below I'm appending multiple svg elements and get 3 different charts from it.
The problem I have is that the max value that's evaluated from the y.domain() for each y-axis is the max value from all of my data.
Is there some clever way to get the max value for each svg and set that to be the max value for each y-axis or do I have to make 3 different y scales?
Here's the code:
var data = [
{category: "Apples", total: 10, goal: 8},
{category: "Oranges", total: 20, goal: 18},
{category: "Bananas", total: 20, goal: 25},
];
chart(data);
function chart(result) {
var margin = {bottom: 25, right: 25, top: 25, left: 25},
width = 180 - margin.left - margin.right,
height = 230 - margin.top - margin.bottom;
var svg = d3.select("#chart").selectAll("svg")
.data(result)
.enter().append("svg")
.attr("width", width)
.attr("height", height)
var x = d3.scaleBand()
.range([margin.left, width - margin.right])
.domain(["Total", "Goal"])
.padding(.1)
.paddingOuter(.2)
var y = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, d3.max(result, d => Math.max(d.total, d.goal))]).nice()
var xAxis = g => g
.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.call(d3.axisBottom(x).tickSizeOuter(0))
var yAxis = g => g
.attr("transform", "translate(" + margin.left + ",0)")
.call(d3.axisLeft(y))
svg.append("g")
.attr("class", "x-axis")
.call(xAxis);
svg.append("g")
.attr("class", "y-axis")
.call(yAxis);
var total = svg.append("rect")
.attr("fill", "steelblue")
.attr("width", x.bandwidth())
.attr("x", x("Total"))
.attr("y", function() {
var d = d3.select(this.parentNode).datum()
return y(d.total)
})
.attr("height", function() {
var d = d3.select(this.parentNode).datum()
return y(0) - y(d.total)
})
var goal = svg.append("rect")
.attr("fill", "orange")
.attr("width", x.bandwidth())
.attr("x", x("Goal"))
.attr("y", function() {
var d = d3.select(this.parentNode).datum()
return y(d.goal)
})
.attr("height", function() {
var d = d3.select(this.parentNode).datum()
return y(0) - y(d.goal)
})
var text = svg.append("text")
.attr("dx", width / 2)
.attr("dy", 15)
.attr("text-anchor", "middle")
.text(function() {
return d3.select(this.parentNode).datum().category
})
}
<meta charset ="utf-8">
<script src="https://d3js.org/d3.v5.min.js "></script>
<div id="chart"></div>
I reckon that the best idea here is refactoring your code to create specific SVGs based on the data you pass to it. However, given the code you have right now, a new idiomatic D3 solution is using local variables.
Actually, according to Mike Bostock...
For instance, when rendering small multiples of time-series data, you might want the same x-scale for all charts but distinct y-scales to compare the relative performance of each metric.
... local variables are the solution for your exact case!
So, all you need is to set the local...
var local = d3.local();
svg.each(function(d) {
var y = local.set(this, d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, Math.max(d.total, d.goal)]).nice())
});
... and get it to create the axes and the bars. For instance:
.attr("y", function(d) {
return local.get(this)(d.total);
})
Have in mind that you don't need that var d = d3.select(this.parentNode).datum() to get the datum!
Here is your code with those changes:
var data = [{
category: "Apples",
total: 10,
goal: 8
},
{
category: "Oranges",
total: 20,
goal: 18
},
{
category: "Bananas",
total: 20,
goal: 25
},
];
var local = d3.local();
chart(data);
function chart(result) {
var margin = {
bottom: 25,
right: 25,
top: 25,
left: 25
},
width = 180 - margin.left - margin.right,
height = 230 - margin.top - margin.bottom;
var svg = d3.select("#chart").selectAll("svg")
.data(result)
.enter().append("svg")
.attr("width", width)
.attr("height", height)
var x = d3.scaleBand()
.range([margin.left, width - margin.right])
.domain(["Total", "Goal"])
.padding(.1)
.paddingOuter(.2);
svg.each(function(d) {
var y = local.set(this, d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, Math.max(d.total, d.goal)]).nice())
});
var xAxis = g => g
.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.call(d3.axisBottom(x).tickSizeOuter(0))
svg.append("g")
.attr("class", "x-axis")
.call(xAxis);
svg.each(function() {
var y = local.get(this);
var yAxis = g => g
.attr("transform", "translate(" + margin.left + ",0)")
.call(d3.axisLeft(y));
d3.select(this).append("g")
.attr("class", "y-axis")
.call(yAxis);
})
var total = svg.append("rect")
.attr("fill", "steelblue")
.attr("width", x.bandwidth())
.attr("x", x("Total"))
.attr("y", function(d) {
return local.get(this)(d.total);
})
.attr("height", function(d) {
return local.get(this)(0) - local.get(this)(d.total)
})
var goal = svg.append("rect")
.attr("fill", "orange")
.attr("width", x.bandwidth())
.attr("x", x("Goal"))
.attr("y", function(d) {
return local.get(this)(d.goal)
})
.attr("height", function(d) {
return local.get(this)(0) - local.get(this)(d.goal)
})
var text = svg.append("text")
.attr("dx", width / 2)
.attr("dy", 15)
.attr("text-anchor", "middle")
.text(function() {
return d3.select(this.parentNode).datum().category
})
}
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js "></script>
<div id="chart"></div>
I would restructure the data in the slightly different way. Doing this allows you to be more flexible when names change, more fruit types are added, words like total & goal get changed, etc
Either way, you can loop through the initial array and create a separate SVG (each with their own yScales) for each object in the array.
const data = [{
"category": "Apples",
"bars": [{
"label": "total",
"val": 10
},
{
"label": "goal",
"val": 8
}
]
},
{
"category": "Oranges",
"bars": [{
"label": "total",
"val": 20
},
{
"label": "goal",
"val": 18
}
]
},
{
"category": "Bananas",
"bars": [{
"label": "total",
"val": 20
},
{
"label": "goal",
"val": 25
}
]
}
]
data.forEach((d) => chart(d))
function chart(result) {
const margin = {
bottom: 25,
right: 25,
top: 25,
left: 25
},
width = 180 - margin.left - margin.right,
height = 230 - margin.top - margin.bottom
const svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
const x = d3.scaleBand()
.range([margin.left, width - margin.right])
.domain(result.bars.map((d) => d.label))
.padding(.1)
.paddingOuter(.2)
const y = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, d3.max(result.bars.map(z => z.val))])
const xAxis = g => g
.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.call(d3.axisBottom(x).tickSizeOuter(0))
const yAxis = g => g
.attr("transform", "translate(" + margin.left + ",0)")
.call(d3.axisLeft(y))
svg.append("g")
.attr("class", "x-axis")
.call(xAxis)
svg.append("g")
.attr("class", "y-axis")
.call(yAxis)
const barEnter = svg.selectAll("rect")
.data(result.bars)
.enter()
.append("rect")
.attr("fill", d => (d.label === 'total' ? "steelblue" : "green"))
.attr("width", x.bandwidth())
.attr("x", (d) => x(d.label))
.attr("y", (d) => y(d.val))
.attr("height", (d) => y(0) - y(d.val))
const text = svg.append("text")
.attr("dx", width / 2)
.attr("dy", 15)
.attr("text-anchor", "middle")
.text(result.category)
}
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js "></script>
<div id="chart"></div>
Update. If you can't optimise your data structure, you could do it this way
const data = [{
category: "Apples",
total: 10,
goal: 8
},
{
category: "Oranges",
total: 20,
goal: 18
},
{
category: "Bananas",
total: 20,
goal: 25
}
]
data.forEach((d) => chart(d))
function chart(result) {
const margin = {
bottom: 25,
right: 25,
top: 25,
left: 25
},
width = 180 - margin.left - margin.right,
height = 230 - margin.top - margin.bottom
const svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
const x = d3.scaleBand()
.range([margin.left, width - margin.right])
.domain(["total", "goal"])
.padding(.1)
.paddingOuter(.2)
const y = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, d3.max([result.total, result.goal])])
const xAxis = g => g
.attr("transform", "translate(0," + (height - margin.bottom) + ")")
.call(d3.axisBottom(x).tickSizeOuter(0))
const yAxis = g => g
.attr("transform", "translate(" + margin.left + ",0)")
.call(d3.axisLeft(y))
svg.append("g")
.attr("class", "x-axis")
.call(xAxis)
svg.append("g")
.attr("class", "y-axis")
.call(yAxis)
const totalBarEnter = svg.selectAll(".total")
.data([result.total])
.enter()
.append("rect")
.attr("class", "total")
.attr("fill", "steelblue")
.attr("width", x.bandwidth())
.attr("x", (d) => x("total"))
.attr("y", (d) => y(d))
.attr("height", (d) => y(0) - y(d))
const goalBarEnter = svg.selectAll(".goal")
.data([result.goal])
.enter()
.append("rect")
.attr("class", "goal")
.attr("fill", "green")
.attr("width", x.bandwidth())
.attr("x", (d) => x("goal"))
.attr("y", (d) => y(d))
.attr("height", (d) => y(0) - y(d))
const text = svg.append("text")
.attr("dx", width / 2)
.attr("dy", 15)
.attr("text-anchor", "middle")
.text(result.category)
}
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js "></script>
<div id="chart"></div>
Codepen
I would create different y_scale for each variable:
//build unique categories set
var categories = d3.set(function(d){return d.category}).values();
//define y scale
var y_scales = {};
//loop through categories, filter data and set y_scale on max.
for (c in categories){
var filtered_result = result.filter(function(d){if(d.category == categories[c]){return d});
y_scales[categories[c]] = d3.scaleLinear()
.range([height - margin.bottom, margin.top])
.domain([0, d3.max(filtered_result, d => Math.max(d.total, d.goal))]).nice()
}
I'm trying to create a function that creates a histogram for a given array. Clearly, there's no data for the X-Axis and I have to choose the bins arbitrarily. What would be the best way to do so?
My code:
var width = 700, height = 500, pad = 30, barPadding = 1;
function plotHistogram(element, dataset) {
var svg = d3.select(element)
.append("svg")
.attr("width", width)
.attr("height", height)
var bars = svg.append("g")
// Container box
var rectangle = svg.append("rect")
.attr("height", height)
.attr("width", width)
.attr("stroke-width", 2).attr("stroke", "#ccc")
.attr("fill", "transparent")
var xScale = d3.scaleLinear()
// Using default domain (0 - 1)
.range([pad, width - pad * 2])
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d; })])
.range([height - pad, pad])
var xAxis = d3.axisBottom(xScale)
var yAxis = d3.axisLeft(yScale)
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (height - pad) + ")")
.call(xAxis)
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + pad +", 0)")
.call(yAxis)
svg.selectAll("bars")
.data(dataset)
.enter()
.append("rect")
// Evenly spacing out bars
.attr("x", function(d, i) { return i * width/dataset.length; })
// Top of each bar as the top of svg. To remove inverted bar graph.
.attr("y", function(d) { return height - (d * 4); })
// To give padding between bars
.attr("width", width / dataset.length - barPadding)
// To make the bars taller
.attr("height", function(d) { return d * 4; })
.attr("fill", "teal");
}
// #normal is the id of the div element.
plotHistogram("#normal", [10, 20, 30, 40, 50, 60, 70, 80]);
Edit 1: I have decided to use the default bin size (0 - 1) for the xScale above. I'm facing problems creating the bars.
Generate the bins as in https://bl.ocks.org/mbostock/3048450
The array in your plotHistogram is like the array data in #mbostock's bl.ock...
HTH
I am having a lot of trouble with D3 bar chart where the length of the data I get from search is variable. I used the D3 bar chart example and built this. However as data length goes up, the graph gets less and less legible. The problem seems to be the range gets confused when it is beyond 100 points or so.
Code is below:
function makegraph(data,ctype) {
//var data=xdata.splice(-900)
var gwidth=data.length*2;
if(gwidth < 800) gwidth=800;
//gwidth=800
var margin = {top: 9, right: 20, bottom: 30, left: 90},
width = gwidth - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
//x=d3.scale.linear().domain([0,1]).range(0,width)
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickFormat(function(d) { if (d % 10) return ""; else return d;})
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// .ticks(10, "%");
d3.select("svg").remove()
var svg = d3.select("#graphchart").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 line = d3.svg.line()
.x(function(d) { return x(d.rowid); })
.y(function(d) { return y(d[ctype]); });
x.domain(data.map(function(d) { return d.rowid; }));
var ymax=d3.max(data, function(d) { return d[ctype]; });
var ymin=d3.min(data, function(d) { return d[ctype]; });
y.domain([ymin, ymax]);
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", Math.log10(ymax))
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Bytes");
// console.log(x.rangeBand())
var bar = svg.selectAll(".bar")
.data(data);
bar.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.rowid); })
.attr("width", x.rangeBand())
.attr("y", function(d) { return y(d[ctype]); })
.attr("height", function(d) { var xya= height - y(d[ctype]); /* console.log(xya+":"+d[ctype]); */ return height - y(d[ctype]); })
/* .attr("title", function(d) { return d['bytes']+" Bytes at "+d.stime; }) */
.on("mouseover", function(d) { $("#graphinfo").html(d3.format('0,000')(d['bytes'])+" Bytes at "+d.stime) })
.on("mouseout",function(d) { $("#graphinfo").html(' ')})
.on("click",function(d) { tablegraph(d)});
$('.bar').tooltip()
}
Do you have suggestions how I can make this graph so that the bar chart will grow horizontally for larger data sets? Thanks for any help and suggestions!
Vijay
I have this bar chart on D3.js... It works fine..
But I'm having problems with the scale... When a data series has a value much grater than the others, the <rect> does not fit into the scale.
Any idea how to solve this matter?
Here is the code:
var data = [
{"Anio":"1999","CONTRAVENCIONAL":"78484","PENAL":"0","FALTAS":"0","MULTAS":"0","OTROS":"0","TOTAL":"78484"},
{"Anio":"2000","CONTRAVENCIONAL":"92879","PENAL":"0","FALTAS":"0","MULTAS":"0","OTROS":"0","TOTAL":"92879"},
{"Anio":"2001","CONTRAVENCIONAL":"100018","PENAL":"0","FALTAS":"1818","MULTAS":"0","OTROS":"0","TOTAL":"101836"},
{"Anio":"2002","CONTRAVENCIONAL":"101380","PENAL":"0","FALTAS":"3692","MULTAS":"0","OTROS":"0","TOTAL":"105072 "},
{"Anio":"2003","CONTRAVENCIONAL":"86791","PENAL":"0","FALTAS":"7417","MULTAS":"0","OTROS":"0","TOTAL":"94208"},
{"Anio":"2004","CONTRAVENCIONAL":"47870","PENAL":"255","FALTAS":"1105","MULTAS":"1811","OTROS":"0","TOTAL":"51041"},
{"Anio":"2005","CONTRAVENCIONAL":"33013","PENAL":"348","FALTAS":"1473","MULTAS":"634","OTROS":"0","TOTAL":"35468"},
];
var margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 860 - margin.left - margin.right,
height = 400 - 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 arr_data = [];
for (var i = 0; i < data.length; i++) {
var o = {'name':data[i].Anio,'value':data[i].TOTAL};
arr_data.push(o);
};
var chart = d3.select(".chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom )
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(arr_data.map(function(d) { return d.name; }));
y.domain([0, d3.max(arr_data, function(d) { return d.value; })]);
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
var bar = chart.selectAll(".bar")
.data(arr_data)
.enter().append("g");
bar.append("rect")
.attr("class", "rect")
.attr("x", function(d) { return x(d.name); })
.attr("y", function(d) { return y(d.value) ; })
.attr("height", function(d) { return height - y(d.value); })
.attr("width", x.rangeBand())
.attr("fill","#632423")
.on('mouseover',function(d){
var a = d3.select(this)
.attr("fill","#733A39");
var a = d3.select("#tooltip")
.style("left","100px")
.style("top","20px")
.select("#value")
.text(d.value);
}).on('mouseout',function(d){
var a = d3.select(this)
.attr("fill","#632423"); //old color: #790018
});
bar.append("text")
.attr("class", "label")
.text(function(d) { return d.value; })
.attr("x", function(d) { return x(d.name)+4; })
.attr("y", function(d) { return y(d.value)+20 ; });
Your scale will actually handle series with larger values for you because you're dynamically setting it to go from 0 to the max value in your data:
y.domain([0, d3.max(arr_data, function(d) { return d.value; })]);
However, you're not converting your data to numbers from strings, so right now the scale is from [0, 94208] instead of [0, 105072]. This is because the character "9" is greater than "1". You can fix this by converting the values to numbers when you construct your arr_data, like this:
var o = {'name':data[i].Anio,'value':+data[i].TOTAL}; // <---- notice the '+'
This produces a better looking graph: