I am learning d3.js, and I have this problem:
The following code in d3 basically draws a bar chart, with an update button that is sorting the data once in a descending order and once in ascending order. Also, numeric labels appear on the bars.
I would like to transition the numeric label from the current value to the updated value. For example, if the first bar has a numeric label of 20, and the new updated value after sorting is 100, I would like this label to transition from 20 to 100 as (20, 21, ..., 100) during the specific transition time, and vice versa, if the original label is 100 and the updated value is 20 the transition goes as 100, 99, ..., 20.
I know I can just transition the numeric value with the bar, but I would like to know how to do the transition from the current numeric value to the new update value as an exercise.
const data = [
{key: 0, value: 50},
{key: 1, value: 20},
{key: 2, value: 100},
{key: 3, value: 30},
{key: 4, value: 40},
{key: 5, value: 70}
]
// const dataset = [50, 20, 100, 30, 40]
const svgWidth = 800;
const svgHeight = 400;
const xScale = d3.scaleBand()
.domain(d3.range(data.length))
.rangeRound([0, svgWidth])
.paddingInner(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, svgHeight]);
const svg = d3.select('#chart')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
let bars = svg.selectAll('rect').data(data, d => d.key);
let labels = svg.selectAll('text').data(data);
bars.enter()
.append('rect')
.each(function(d){return this._old = d;})
.attr('width', xScale.bandwidth)
.attr('height', d => yScale(d.value))
.attr('fill', d => `rgb(${d.value}, ${d.value * 2}, ${d.value * 3})`)
.attr('x', (d, i) => xScale(i))
.attr('y', d => svgHeight - yScale(d.value))
.attr('stroke', 'black')
.attr('stroke-width', 3)
labels.enter()
.append('text')
.attr('x', (d, i) => xScale(i) + (xScale.bandwidth() / 2))
.attr('y', d => svgHeight - yScale(d.value) + 20)
.attr('font-size', 20)
.attr('text-anchor', 'middle')
.attr('fill', 'white')
.text(d => d.value);
let asc = false;
d3.select('button').on('click', () => {
if(!asc){
data.sort((a,b) => b.value - a.value );
}else{
data.sort((a,b) => a.value - b.value );
};
asc = !asc;
bars = svg.selectAll('rect').data(data, d => d.key);
labels = svg.selectAll('text').data(data);
bars
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.each(function(d){return this._old = d;})
.attr('x', (d, i) => xScale(i))
.attr('height', d => yScale(d.value))
.attr('y', d => svgHeight - yScale(d.value));
labels
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.tween("text", function(d) {
var that = this;
var i = d3.interpolate(0, d.value); // Number(d.percentage.slice(0, -1))
return function(t) {
d3.select(that).text(i(t).toFixed(0));
}
})
.attr('y', d => svgHeight - yScale(d.value) + 20);
})
I found the "tween" function included in the above code for a similar but not exactly the same question. I don't know how to make the interpolation start from the current value instead of 0. I know I need somehow to store the old value, and access it in the tween, but not sure how.
Another question regarding the tween function: why do we assign var that = this and select that in the returned function?
Thanks in advance
You can get the current value for each text by different ways.
For instance, with vanilla JavaScript:
var current = +(this.textContent);
Or using a D3 getter:
var current = +(d3.select(this).text());
Here is your code with that change:
const data = [{
key: 0,
value: 50
},
{
key: 1,
value: 20
},
{
key: 2,
value: 100
},
{
key: 3,
value: 30
},
{
key: 4,
value: 40
},
{
key: 5,
value: 70
}
]
// const dataset = [50, 20, 100, 30, 40]
const svgWidth = 800;
const svgHeight = 400;
const xScale = d3.scaleBand()
.domain(d3.range(data.length))
.rangeRound([0, svgWidth])
.paddingInner(0.1);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, svgHeight]);
const svg = d3.select('body')
.append('svg')
.attr('width', svgWidth)
.attr('height', svgHeight);
let bars = svg.selectAll('rect').data(data, d => d.key);
let labels = svg.selectAll('text').data(data);
bars.enter()
.append('rect')
.each(function(d) {
return this._old = d;
})
.attr('width', xScale.bandwidth)
.attr('height', d => yScale(d.value))
.attr('fill', d => `rgb(${d.value}, ${d.value * 2}, ${d.value * 3})`)
.attr('x', (d, i) => xScale(i))
.attr('y', d => svgHeight - yScale(d.value))
.attr('stroke', 'black')
.attr('stroke-width', 3)
labels.enter()
.append('text')
.attr('x', (d, i) => xScale(i) + (xScale.bandwidth() / 2))
.attr('y', d => svgHeight - yScale(d.value) + 20)
.attr('font-size', 20)
.attr('text-anchor', 'middle')
.attr('fill', 'white')
.text(d => d.value);
let asc = false;
d3.select('button').on('click', () => {
if (!asc) {
data.sort((a, b) => b.value - a.value);
} else {
data.sort((a, b) => a.value - b.value);
};
asc = !asc;
bars = svg.selectAll('rect').data(data, d => d.key);
labels = svg.selectAll('text').data(data);
bars
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.each(function(d) {
return this._old = d;
})
.attr('x', (d, i) => xScale(i))
.attr('height', d => yScale(d.value))
.attr('y', d => svgHeight - yScale(d.value));
labels
.transition()
.delay((d, i) => (i * 10))
.duration(3000)
.tween("text", function(d) {
var current = +(d3.select(this).text());
var that = this;
var i = d3.interpolate(current, d.value); // Number(d.percentage.slice(0, -1))
return function(t) {
d3.select(that).text(i(t).toFixed(0));
}
})
.attr('y', d => svgHeight - yScale(d.value) + 20);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button>Click</button>
<br>
Related
When drag one node, the links should be updated with new coordinates. current code do not clear the old path. I use join method with the group element but group element not clean up sub-elements appended.
svg_arc()
function svg_arc() {
var svg = d3.select('body')
.append('svg')
.attr('width',410)
.attr('height',500)
//.style('border','10px solid red')
.style('background','#ececec')
var g = svg.append('g')
.attr('stroke','black')
.attr('stroke-width',1)
.attr('fill','black')
var data = [
{
id:"pointA",
x:100,
y:350,
r:6,
text:'A'
},{
id:"pointB",
x:250,
y:50,
r:6,
text:'B'
},{
id:"pointC",
x:400,
y:350,
r:6,
text:'C'
}
]
var node = g.selectAll('.node')
.data(data)
.join('g')
.attr('class','node')
.attr('transform',d => `translate(${d.x},${d.y})`)
.attr("pointer-events", "all")
var circle = node.append('circle')
.attr('id',d => d.id)
.attr('cx',0)
.attr('cy',0)
.attr('r',d => d.r)
.attr("pointer-events", "all")
var text = node.append('text')
.attr('class','text')
.text(d => d.text)
.attr('x',0)
.attr('y',0)
.attr('dx',-15)
.attr('dy',-15)
node.call(
d3.drag()
.on("start",function(event,d) {
d3.select(this).raise().classed("active", true);
})
.on("drag",function(event,d) {
d.x += event.dx
d.y += event.dy
update_links(g,data)
d3.select(this).attr('transform',`translate(${d.x},${d.y})`)
})
.on("end", function dragEnd(event,d) {
d3.select(this).classed("active", false);
})
)
update_links(g,data)
}
function update_links(g,n) {
var data = [0]//dummy data
var line = g.selectAll('.lines')
.data(data)
.join('g')
.attr('class','lines')
line.append('path')
.attr('class','AB')
.attr('d',['M',n[0].x,n[0].y,'L',n[1].x,n[1].y].join(' '))
.attr('stroke','black')
line.append('path')
.attr('class','BC')
.attr('d',['M',n[1].x,n[1].y,'L',n[2].x,n[2].y].join(' '))
.attr('stroke','black')
line.append('path')
.attr('class','AC')
.attr('d',['M',n[0].x,n[0].y,'Q',n[1].x,n[1].y,n[2].x,n[2].y].join(' '))
.attr('stroke','black')
.attr('fill','none')
line.append('path')
.attr('class','MID')
.attr('d',['M',(n[0].x+n[1].x)/2,(n[0].y+n[1].y)/2,'L',(n[1].x+n[2].x)/2,(n[1].y+n[2].y)/2].join(' '))
.attr('fill','none')
}
<script src="https://unpkg.com/d3#7.0.4/dist/d3.min.js"></script>
You're using join for the group, that's correct, but then you are just appending the paths to the group. In other words, you need an enter/update/exit selection for the paths themselves.
Here's the code with that change:
svg_arc()
function svg_arc() {
var svg = d3.select('body')
.append('svg')
.attr('width', 410)
.attr('height', 500)
//.style('border','10px solid red')
.style('background', '#ececec')
var g = svg.append('g')
.attr('stroke', 'black')
.attr('stroke-width', 3)
.attr('fill', 'black')
var data = [{
id: "pointA",
x: 100,
y: 350,
r: 6,
text: 'A'
}, {
id: "pointB",
x: 250,
y: 50,
r: 6,
text: 'B'
}, {
id: "pointC",
x: 400,
y: 350,
r: 6,
text: 'C'
}]
var node = g.selectAll('.node')
.data(data)
.join('g')
.attr('class', 'node')
.attr('transform', d => `translate(${d.x},${d.y})`)
.attr("pointer-events", "all")
var circle = node.append('circle')
.attr('id', d => d.id)
.attr('cx', 0)
.attr('cy', 0)
.attr('r', d => d.r)
.attr("pointer-events", "all")
var text = node.append('text')
.attr('class', 'text')
.text(d => d.text)
.attr('x', 0)
.attr('y', 0)
.attr('dx', -15)
.attr('dy', -15)
node.call(
d3.drag()
.on("start", function(event, d) {
d3.select(this).raise().classed("active", true);
})
.on("drag", function(event, d) {
d.x += event.dx
d.y += event.dy
update_links(g, data)
d3.select(this).attr('transform', `translate(${d.x},${d.y})`)
})
.on("end", function dragEnd(event, d) {
d3.select(this).classed("active", false);
})
)
update_links(g, data)
}
function update_links(g, n) {
var data = [0] //dummy data
var group = g.selectAll('.lines')
.data(data)
.join('g')
.attr('class', 'lines');
const line = group.selectAll(".line")
.data([0, 1, 2, 3])
.join("path")
.attr("class", "line")
.attr("d", d => d === 0 ? ['M', n[0].x, n[0].y, 'L', n[1].x, n[1].y].join(' ') :
d === 1 ? ['M', n[1].x, n[1].y, 'L', n[2].x, n[2].y].join(' ') :
d === 2 ? ['M', n[0].x, n[0].y, 'Q', n[1].x, n[1].y, n[2].x, n[2].y].join(' ') : ['M', (n[0].x + n[1].x) / 2, (n[0].y + n[1].y) / 2, 'L', (n[1].x + n[2].x) / 2, (n[1].y + n[2].y) / 2].join(' '))
.attr('stroke', 'black')
.attr('fill', d => d > 1 ? 'none' : null)
}
<script src="https://unpkg.com/d3#7.0.4/dist/d3.min.js"></script>
Have in mind that this is just to show you why your code was behaving the way it was. There are still lots and lots of problems to fix here, from just unnecessary bits to patterns that effectively worsen performance (for instance, the very use of join inside the drag callback is a big no) , but that's out of the scope of this answer.
Hi i figured out how to add log labels at Y axis axis but cant make chart line scale log, and i dont really know how to make X axis labels plain text instead of converting it to date
If i just change scaleLinear to scaleLog chart line dissapear
Also removing const parseTime = d3.timeParse('%Y/%m/%d'); and change parseTime(val.date) just to val.date, and changing scaleTime to scaleOrdinal does not work as i would
Also i don't understand why Y axis labels are so blured
Sorry for asking but cant findout how this pluggin works.
using D3 7.2.1 and JQ 3.5.1
(function (d3){
const lineChartData = [
{
currency: "data",
values: [
{
date: "2018/01/01",
close: 0
},
{
date: "2018/02/01",
close: 5
},
{
date: "2018/03/01",
close: 10
},
{
date: "2018/04/01",
close: 50
},
{
date: "2018/05/01",
close: 100
},
{
date: "2018/06/01",
close: 500
},
{
date: "2018/07/01",
close: 1000
},
{
date: "2018/08/01",
close: 5000
},
{
date: "2018/09/01",
close: 10000
},
]
}
];
const margin = {
top: 20,
bottom: 20,
left: 50,
right: 20
};
const width = 400 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const createGradient = select => {
const gradient = select
.select('defs')
.append('linearGradient')
.attr('id', 'gradient')
.attr('x1', '0%')
.attr('y1', '100%')
.attr('x2', '0%')
.attr('y2', '0%');
gradient
.append('stop')
.attr('offset', '0%')
.attr('style', 'stop-color:#FF6500; stop-opacity:0');
gradient
.append('stop')
.attr('offset', '100%')
.attr('style', 'stop-color:#FF6500; stop-opacity: 1');
}
const createGlowFilter = select => {
const filter = select
.select('defs')
.append('filter')
.attr('id', 'glow')
//stdDeviation is px count for make blur around main chart line
filter
.append('feGaussianBlur')
.attr('stdDeviation', '0')
.attr('result', 'coloredBlur');
const femerge = filter
.append('feMerge');
femerge
.append('feMergeNode')
.attr('in', 'coloredBlur');
femerge
.append('feMergeNode')
.attr('in', 'SourceGraphic');
}
const svg = d3.select('#line-chart')
.append('svg')
.attr('width', 700 + margin.left + margin.right)
.attr('height', 300 + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
svg.append('defs');
svg.call(createGradient);
svg.call(createGlowFilter);
const parseTime = d3.timeParse('%Y/%m/%d');
const parsedData = lineChartData.map(company => ({
ticker: company.ticker,
values: company.values.map(val => ({
close: val.close,
date: parseTime(val.date)
}))
}));
const xScale = d3.scaleTime()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.date)),
d3.max(parsedData, d => d3.max(d.values, v => v.date))
])
.range([0, width]);
const yScale = d3.scaleLinear()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.close)),
d3.max(parsedData, d => d3.max(d.values, v => v.close))
])
.range([height, 0]);
const line = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.close))
.curve(d3.curveCatmullRom.alpha(0.5));
svg.selectAll('.line')
.data(parsedData)
.enter()
.append('path')
.attr('d', d => {
const lineValues = line(d.values).slice(1);
const splitedValues = lineValues.split(',');
return `M0,${height},${lineValues},l0,${height - splitedValues[splitedValues.length - 1]}`
})
.style('fill', 'url(#gradient)')
svg.selectAll('.line')
.data(parsedData)
.enter()
.append('path')
.attr('d', d => line(d.values))
.attr('stroke-width', '2')
.style('fill', 'none')
.style('filter', 'url(#glow)')
.attr('stroke', '#FF6500');
const tick = svg.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale).ticks(9))
.selectAll('.tick')
.style('transition', '.2s');
//Y dashes
//stroke color of line in background
//stroke-dasharray
//first paramter is length
//second parameter is space between
tick
.selectAll('line')
.attr('stroke-dasharray', `4, 7`)
.attr('stroke', '#5E779B')
.attr('y2', `-${height}px`)
tick
.append('rect')
.attr('width', `${(width / 12) + 10}px`)
.attr('x', `-${width / 24 + 5}px`)
.attr('y', `-${height}px`)
.attr('height', `${height + 30}px`)
.style('fill', 'transparent');
svg.selectAll('.tick')
.append('circle')
.attr('r', '5px')
.style('fill', '#ffffff')
.style('stroke', '#FF6500')
.attr('cy', (x, i) => - height + yScale(parsedData[0].values[i].close));
svg.select('.domain')
.attr('stroke', '#5E779B')
.attr('stroke-dasharray', `4, 7`)
var yscale = d3.scaleLog()
.domain([1, 100000])
.nice()
.range([height - 10, -10]);
var y_axis = d3.axisLeft(yscale);
y_axis.ticks(5);
svg.append("g")
.call(d3.axisLeft(xScale).ticks(5))
.attr("transform", "translate(0, 10)")
.attr('stroke', '#5E779B')
.attr('stroke-dasharray', `4, 7`)
.call(y_axis)
})
(d3);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<body>
<div id="line-chart"></div>
</body>
I'll start with the log scale issues. You have two y-scales, yScale and yscale. The first is a linear scale that is used in the line generator and to position the circles. The second is a log scale that is only used for the axis. You should only have one y-scale and use it both for positioning the elements and for the axis.
Also, log scales cannot handle the value 0:
As log(0) = -∞, a log scale domain must be strictly-positive or strictly-negative; the domain must not include or cross zero.
The data point that has a close value of 0 cannot be shown on a log scale.
Next, the y-axis labels look wrong because of the "stroke" and "stroke-dasharray" attributes that you set on the y-axis group.
Finally, for the x-scale you are correct to convert the strings to Date objects and use a d3.scaleTime. For the x-axis, you can then do something like this:
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeMonth.every(1), '%b');
The first argument to ticks says that you want to have one tick for each month. The second argument is a date format specifier that defines how the tick labels should be formatted. %b puts an abbreviated month name at each tick mark. If you want the tick marks to have the same format as the original string, then you can use %Y/%m/%d, but you might find that the labels are too long and overlap each other.
Here is an example that fixes the issues mentioned above.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="line-chart"></div>
<script>
// data
const lineChartData = [
{
ticker: "ABC",
values: [
{
date: "2018/01/01",
close: 1
},
{
date: "2018/02/01",
close: 5
},
{
date: "2018/03/01",
close: 10
},
{
date: "2018/04/01",
close: 50
},
{
date: "2018/05/01",
close: 100
},
{
date: "2018/06/01",
close: 500
},
{
date: "2018/07/01",
close: 1000
},
{
date: "2018/08/01",
close: 5000
},
{
date: "2018/09/01",
close: 10000
},
]
}
];
const parseTime = d3.timeParse('%Y/%m/%d');
const parsedData = lineChartData.map(company => ({
ticker: company.ticker,
values: company.values.map(val => ({
close: val.close,
date: parseTime(val.date)
}))
}));
// gradient
const createGradient = defs => {
const gradient = defs.append('linearGradient')
.attr('id', 'gradient')
.attr('x1', '0%')
.attr('y1', '100%')
.attr('x2', '0%')
.attr('y2', '0%');
gradient.append('stop')
.attr('offset', '0%')
.attr('style', 'stop-color:#FF6500; stop-opacity:0');
gradient.append('stop')
.attr('offset', '100%')
.attr('style', 'stop-color:#FF6500; stop-opacity: 1');
}
// filter
const createGlowFilter = defs => {
const filter = defs.append('filter')
.attr('id', 'glow')
//stdDeviation is px count for make blur around main chart line
filter.append('feGaussianBlur')
.attr('stdDeviation', '2')
.attr('result', 'coloredBlur');
const femerge = filter.append('feMerge');
femerge.append('feMergeNode')
.attr('in', 'coloredBlur');
femerge.append('feMergeNode')
.attr('in', 'SourceGraphic');
}
// set up
const margin = { top: 20, right: 20, bottom: 20, left: 50 };
const width = 400 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;
const svg = d3.select('#line-chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.call(svg => svg.append('defs')
.call(createGradient)
.call(createGlowFilter))
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
// scales
const xScale = d3.scaleTime()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.date)),
d3.max(parsedData, d => d3.max(d.values, v => v.date))
])
.range([0, width]);
const yScale = d3.scaleLog()
.domain([
d3.min(parsedData, d => d3.min(d.values, v => v.close)),
d3.max(parsedData, d => d3.max(d.values, v => v.close))
])
.range([height, 0]);
// line and area generators
const area = d3.area()
.x(d => xScale(d.date))
.y1(d => yScale(d.close))
.y0(yScale(yScale.domain()[0]))
.curve(d3.curveCatmullRom.alpha(0.5));
const line = area.lineY1();
// axes
const xAxis = d3.axisBottom(xScale)
.ticks(d3.timeMonth.every(1), '%b');
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
// add vertical grid lines
.call(g =>
g.selectAll('.tick>line')
.clone()
.attr('stroke', '#5E779B')
.attr('stroke-dasharray', '4, 7')
.attr('y0', 0)
.attr('y1', -height)
)
const yAxis = d3.axisLeft(yScale)
.ticks(5);
svg.append('g')
.call(yAxis);
// draw area, line, and circles
// create one group for each company
const companies = svg.append('g')
.selectAll('g')
.data(parsedData)
.join('g');
// add the area
companies.append('path')
.attr('d', d => area(d.values))
.attr('fill', 'url(#gradient)');
// add the line
companies.append('path')
.attr('d', d => line(d.values))
.attr('stroke-width', 2)
.attr('fill', 'none')
.attr('stroke', '#FF6500')
.attr('filter', 'url(#glow)');
// add the circles
companies.selectAll('circle')
.data(d => d.values)
.join('circle')
.attr('fill', 'white')
.attr('stroke', '#FF6500')
.attr('r', '3')
.attr('cx', d => xScale(d.date))
.attr('cy', d => yScale(d.close));
</script>
</body>
</html>
Is it possible to create a chart with d3.js as given in the attached picture? Especially the information box. The context is from a specific start and end date if the data is missing on date place a dot instead of a bar in the chart. The difficulty I am facing is to attach the information box with the dots using a line using d3.js.
The whole chart should be implemented using (SVG) d3.js.
Can anyone give a solution example with any dataset?
This is just an example, hopefully it will be enough to get you started.
const url = "https://raw.githubusercontent.com/freeCodeCamp/ProjectReferenceData/master/GDP-data.json"
const margin = {
top: 30,
left: 40,
right: 40,
bottom: 100
};
const width = 800;
const height = 300;
const svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
const notice = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top + height})`)
notice.append('rect')
.classed('notice-box', true)
.attr('x', 0)
.attr('y', margin.top)
.attr('width', width)
.attr('height', margin.bottom - margin.top);
const warning = notice.append('text')
.attr('x', 10)
.attr('y', margin.top + 30);
const format = d3.timeFormat("Q%q %Y")
const setWarning = (data) => {
warning.text(`Missing data for ${data.map(d => format(d.date)).join(', ')}`);
notice.selectAll('line')
.data(data)
.enter()
.append('line')
.attr('stroke', 'black')
.attr('x1', d => x(d.date))
.attr('y1', margin.top)
.attr('x2', d => x(d.date))
.attr('y2', y(0) - height);
notice.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('fill', 'black')
.attr('r', 3)
.attr('cx', d => x(d.date))
.attr('cy', y(0) - height);
}
var x = d3.scaleTime().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
// Since v5 d3.json is promise-based, hence the change.
d3.json(url)
.then(response => response.data)
.then(data => data.map(([date, value]) => ({
date: new Date(date),
value: value
})))
.then(data => {
data.filter(({
date
}) => date.getFullYear() >= 2000 && date.getFullYear() <= 2005 && date.getMonth() === 0)
.forEach(d => d.value = null);
x.domain(d3.extent(data.map(({
date
}) => date)));
const barWidth = width / data.length;
y.domain([0, d3.max(data.map(({
value
}) => value))]);
g.append('g')
.call(d3.axisBottom().scale(x))
.attr('id', 'x-axis')
.attr('transform', `translate(0, ${height})`);
g.append('g')
.call(d3.axisLeft(y))
.attr('id', 'y-axis');
g.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', d => x(d.date))
.attr('y', d => y(d.value))
.attr('width', barWidth)
.attr('height', d => height - y(d.value))
.style('fill', '#33adff');
const missing = data.filter(({
value
}) => value === null);
setWarning(missing);
});
#y-axis path {
stroke: black;
stroke-width: 1;
fill: none;
}
#x-axis path {
stroke: black;
stroke-width: 1;
fill: none;
}
.notice-box {
fill: none;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>
I have a codepen here - https://codepen.io/anon/pen/xpaYYw?editors=0010
Its a simple test graph but the date will be formatted like this.
I have dates on the x axis and amounts on the y
How can I use the x scale to set the width and x position of the bars.
layers.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('height', function(d, i) {
return height - y(d.one);
})
.attr('y', function(d, i) {
return y(d.one);
})
.attr('width', function(d, i) {
return 50;
})
.attr('x', function(d, i) {
return 80*i;
})
.style('fill', (d, i) => {
return colors[i];
});
The problem with your question has nothing to do with programming, or JavaScript, or D3... the problem is a basic dataviz concept (that's why I added the data-visualization tag in your question):
What you're trying to do is not correct! You should not use bars with a time scale. Time scales are for time series (in which we use dots, or dots connected by lines).
If you use bars with time in the x axis you'll face problems:
Positioning the bar: the left margin of the bar will be always at the date you set. The whole bar will lie after that date;
Setting the width of the bar: in a real bar chart, which uses categorical variables for the x axis, the width has no meaning. But in a time scale the width represents time.
However, just for the sake of explanation, let's create this bar chart with a time scale (despite the fact that this is a wrong choice)... Here is how to do it:
First, set the "width" of the bars in time. Let's say, each bar will have 10 days of width:
.attr("width", function(d){
return x(d3.timeDay.offset(d.date, 10)) - x(d.date)
})
Then, set the x position of the bar to the current date less half its width (that is, less 5 days in our example):
.attr('x', function(d, i) {
return x(d3.timeDay.offset(d.date, -5));
})
Finally, don't forget to create a "padding" in the time scale:
var x = d3.scaleTime()
.domain([d3.min(data, function(d) {
return d3.timeDay.offset(d.date, -10);
}), d3.max(data, function(d) {
return d3.timeDay.offset(d.date, 10);
})])
.range([0, width]);
Here is your code with those changes:
var keys = [];
var legendKeys = [];
var maxVal = [];
var w = 800;
var h = 450;
var margin = {
top: 30,
bottom: 40,
left: 50,
right: 20,
};
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;
var colors = ['#FF9A00', '#FFEBB6', '#FFC400', '#B4EDA0', '#FF4436'];
var data = [{
"one": 4306,
"two": 2465,
"three": 2299,
"four": 988,
"five": 554,
"six": 1841,
"date": "2015-05-31T00:00:00"
}, {
"one": 4378,
"two": 2457,
"three": 2348,
"four": 1021,
"five": 498,
"six": 1921,
"date": "2015-06-30T00:00:00"
}, {
"one": 3404,
"two": 2348,
"three": 1655,
"four": 809,
"five": 473,
"six": 1056,
"date": "2015-07-31T00:00:00"
},
];
data.forEach(function(d) {
d.date = new Date(d.date)
})
for (var i = 0; i < data.length; i++) {
for (var key in data[i]) {
if (!data.hasOwnProperty(key) && key !== "date")
maxVal.push(data[i][key]);
}
}
var x = d3.scaleTime()
.domain([d3.min(data, function(d) {
return d3.timeDay.offset(d.date, -10);
}), d3.max(data, function(d) {
return d3.timeDay.offset(d.date, 10);
})])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, d3.max(maxVal, function(d) {
return d;
})])
.range([height, 0]);
var svg = d3.select('body').append('svg')
.attr('class', 'chart')
.attr('width', w)
.attr('height', h);
var chart = svg.append('g')
.classed('graph', true)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var layersArea = chart.append('g')
.attr('class', 'layers');
var layers = layersArea.append('g')
.attr('class', 'layer');
layers.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('height', function(d, i) {
return height - y(d.one);
})
.attr('y', function(d, i) {
return y(d.one);
})
// .attr('width', function(d, i) {
// return 50;
// })
.attr("width", function(d) {
return x(d3.timeDay.offset(d.date, 10)) - x(d.date)
})
.attr('x', function(d, i) {
return x(d3.timeDay.offset(d.date, -5));
})
.style('fill', (d, i) => {
return colors[i];
});
chart.append('g')
.classed('x axis', true)
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.tickFormat(d3.timeFormat("%Y-%m-%d")).tickValues(data.map(function(d) {
return new Date(d.date)
})));
chart.append('g')
.classed('y axis', true)
.call(d3.axisLeft(y)
.ticks(10));
<script src="https://d3js.org/d3.v4.min.js"></script>
I have a codepen here - https://codepen.io/anon/pen/xpaYYw?editors=0010
Its a simple test graph but the date will be formatted like this.
I have dates on the x axis and amounts on the y
How can I use the x scale to set the width and x position of the bars.
layers.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('height', function(d, i) {
return height - y(d.one);
})
.attr('y', function(d, i) {
return y(d.one);
})
.attr('width', function(d, i) {
return 50;
})
.attr('x', function(d, i) {
return 80*i;
})
.style('fill', (d, i) => {
return colors[i];
});
The problem with your question has nothing to do with programming, or JavaScript, or D3... the problem is a basic dataviz concept (that's why I added the data-visualization tag in your question):
What you're trying to do is not correct! You should not use bars with a time scale. Time scales are for time series (in which we use dots, or dots connected by lines).
If you use bars with time in the x axis you'll face problems:
Positioning the bar: the left margin of the bar will be always at the date you set. The whole bar will lie after that date;
Setting the width of the bar: in a real bar chart, which uses categorical variables for the x axis, the width has no meaning. But in a time scale the width represents time.
However, just for the sake of explanation, let's create this bar chart with a time scale (despite the fact that this is a wrong choice)... Here is how to do it:
First, set the "width" of the bars in time. Let's say, each bar will have 10 days of width:
.attr("width", function(d){
return x(d3.timeDay.offset(d.date, 10)) - x(d.date)
})
Then, set the x position of the bar to the current date less half its width (that is, less 5 days in our example):
.attr('x', function(d, i) {
return x(d3.timeDay.offset(d.date, -5));
})
Finally, don't forget to create a "padding" in the time scale:
var x = d3.scaleTime()
.domain([d3.min(data, function(d) {
return d3.timeDay.offset(d.date, -10);
}), d3.max(data, function(d) {
return d3.timeDay.offset(d.date, 10);
})])
.range([0, width]);
Here is your code with those changes:
var keys = [];
var legendKeys = [];
var maxVal = [];
var w = 800;
var h = 450;
var margin = {
top: 30,
bottom: 40,
left: 50,
right: 20,
};
var width = w - margin.left - margin.right;
var height = h - margin.top - margin.bottom;
var colors = ['#FF9A00', '#FFEBB6', '#FFC400', '#B4EDA0', '#FF4436'];
var data = [{
"one": 4306,
"two": 2465,
"three": 2299,
"four": 988,
"five": 554,
"six": 1841,
"date": "2015-05-31T00:00:00"
}, {
"one": 4378,
"two": 2457,
"three": 2348,
"four": 1021,
"five": 498,
"six": 1921,
"date": "2015-06-30T00:00:00"
}, {
"one": 3404,
"two": 2348,
"three": 1655,
"four": 809,
"five": 473,
"six": 1056,
"date": "2015-07-31T00:00:00"
},
];
data.forEach(function(d) {
d.date = new Date(d.date)
})
for (var i = 0; i < data.length; i++) {
for (var key in data[i]) {
if (!data.hasOwnProperty(key) && key !== "date")
maxVal.push(data[i][key]);
}
}
var x = d3.scaleTime()
.domain([d3.min(data, function(d) {
return d3.timeDay.offset(d.date, -10);
}), d3.max(data, function(d) {
return d3.timeDay.offset(d.date, 10);
})])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, d3.max(maxVal, function(d) {
return d;
})])
.range([height, 0]);
var svg = d3.select('body').append('svg')
.attr('class', 'chart')
.attr('width', w)
.attr('height', h);
var chart = svg.append('g')
.classed('graph', true)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var layersArea = chart.append('g')
.attr('class', 'layers');
var layers = layersArea.append('g')
.attr('class', 'layer');
layers.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('height', function(d, i) {
return height - y(d.one);
})
.attr('y', function(d, i) {
return y(d.one);
})
// .attr('width', function(d, i) {
// return 50;
// })
.attr("width", function(d) {
return x(d3.timeDay.offset(d.date, 10)) - x(d.date)
})
.attr('x', function(d, i) {
return x(d3.timeDay.offset(d.date, -5));
})
.style('fill', (d, i) => {
return colors[i];
});
chart.append('g')
.classed('x axis', true)
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.tickFormat(d3.timeFormat("%Y-%m-%d")).tickValues(data.map(function(d) {
return new Date(d.date)
})));
chart.append('g')
.classed('y axis', true)
.call(d3.axisLeft(y)
.ticks(10));
<script src="https://d3js.org/d3.v4.min.js"></script>