I'm using D3 to create a bar chart that looks like this:
Currently, the bars are floating in on an angle from the top left corner (because of the D3 coordinate system). I would like the bars to float directly downward, but only when the user scrolls to that part of the page (note: the page is quite lenghty and it would take a few minutes for the user to scroll to that element).
Here is my code:
let data = [
{"color": "Blue", "plotValue": 11},
{"color": "Red", "plotValue": 20},
{"color": "Orange", "plotValue": 21},
{"color": "Purple", "plotValue": 30},
{"color": "Green", "plotValue": 35},
{"color": "Violet", "plotValue": 40},
]
vis.margin = {top: 70, right: 90, bottom: 90, left: 80};
vis.width = 800 - vis.margin.left - vis.margin.right;
// initialize the drawing area
vis.svg = d3.select('#' vis.parentElement).append('svg')
.attr('width', vis.width vis.margin.left vis.margin.right)
.attr('height', vis.height vis.margin.top vis.margin.bottom)
.append('g')
.attr('transform', `translate (${vis.margin.left}, ${vis.margin.top})`);
// scales and axes
vis.xScale = d3.scaleBand()
.range( [ 0, vis.width ] )
.padding(0.4);
vis.yScale = d3.scaleLinear()
.range( [ vis.height, 0 ] );
vis.xAxis = d3.axisBottom()
.scale(vis.xScale);
vis.yAxis = d3.axisLeft()
.scale(vis.yScale);
// add chart title
vis.svg.append('g')
.attr('class', 'title bar-chart-title')
.append('text')
.text(vis.chartTitle)
.attr('transform', `translate(${vis.width / 2 45}, -50)`)
.attr('text-anchor', 'middle');
// tooltip
vis.tooltip = d3.select('body').append('div')
.attr('class', 'tooltip')
.attr('id', 'barChartTooltip');
// create the axis groups
vis.xAxisGroup = vis.svg.append('g')
.attr('class', 'x-axis axis')
.attr('transform', 'translate(0, ' vis.height ')');
vis.yAxisGroup = vis.svg.append('g')
.attr('class', 'y-axis axis');
let vis = this;
// update the domains
vis.xScale.domain(vis.data.map(function (d) { return d.color; }));
vis.yScale.domain( [ 0, 100 ] );
// draw the bars
vis.bars = vis.svg.selectAll('.bar')
.data(vis.data)
vis.bars.exit().remove();
vis.bars
.enter()
.append('rect')
.attr('class', 'bar')
.on('mouseover', function(event, d) {
d3.select(this)
.attr('stroke-width', '2px')
.attr('stroke', 'grey')
.attr('fill', 'blue')
vis.tooltip
.style('opacity', 1)
.style('left', event.pageX 20 'px')
.style('top', event.pageY 'px')
.html(`
<div style='border: thin solid grey; border-radius: 5px; background: lightgrey; padding: 20px'>
<h3>${d.color}</h3>
<h4> ${100- d.plotValue.toLocaleString()}% decline </h4>
</div>
`)
})
.on('mouseout', function(event, d) {
d3.select(this)
.attr('stroke-width', 1)
.attr('stroke', '#456983')
.attr('fill', function(d) {
return 'red'
})
vis.tooltip
.style('opacity', 0)
.style('left', 0)
.style('top', 0)
.html(``);
})
.merge(vis.bars)
.transition()
.duration(1200)
.attr('x', d => vis.xScale(d.color) )
.attr('y', d => vis.yScale(d.plotValue) )
.attr('width', vis.xScale.bandwidth() )
.attr('height', function(d) { return vis.height - vis.yScale(d.plotValue); })
.attr('fill', 'red')
// .attr('stroke', 'grey');
// add the axes
vis.xAxisGroup
.transition()
.duration(500)
.style('font-size', '15px')
.style('color', 'blue')
.call(d3.axisBottom((vis.xScale)))
.selectAll('text')
.attr('y', 30)
.attr('x', -35)
.attr('dy', '.35em')
.attr('transform', 'rotate(-30)')
;
vis.yAxisGroup
.transition()
.duration(500)
.style('font-size', '15px')
.style('color', 'blue')
.call(d3.axisLeft(vis.yScale));
vis.text = vis.svg.selectAll('.text')
.data(vis.data)
vis.text
.enter()
.append('text')
.attr('class', 'text')
.attr('text-anchor', 'middle')
.attr('x', d => vis.xScale(d.color) 27)
.attr('y', d => vis.yScale(d.plotValue) - 10)
.text( function (d) {
return '-' (100 - d.plotValue) '%';
})
How would I amend my code to get the bars to float directly downward, and only triggered when the user scrolls to that part of the page?
Thanks in advance for any advice you can give!
CodePudding user response:
There's no such a thing like D3 coordinate system, that's just the SVG coordinate system: 0,0
is the top left corner.
For fixing your issue you just need to set the x
, y
and width
attributes when the bars enter, and then change the y
and height
attributes on the transition. I believe showing the bars going up is way better for the user, but showing them coming from the top is trivial (just change the original y
position):
const vis = {};
vis.data = [{
"color": "Blue",
"plotValue": 11
},
{
"color": "Red",
"plotValue": 20
},
{
"color": "Orange",
"plotValue": 21
},
{
"color": "Purple",
"plotValue": 30
},
{
"color": "Green",
"plotValue": 35
},
{
"color": "Violet",
"plotValue": 40
},
];
vis.margin = {
top: 70,
right: 90,
bottom: 90,
left: 80
};
vis.width = 800 - vis.margin.left - vis.margin.right;
vis.height = 400 - vis.margin.left - vis.margin.right;
// initialize the drawing area
vis.svg = d3.select("body").append('svg')
.attr('width', vis.width vis.margin.left vis.margin.right)
.attr('height', vis.height vis.margin.top vis.margin.bottom)
.append('g')
.attr('transform', `translate (${vis.margin.left}, ${vis.margin.top})`);
// scales and axes
vis.xScale = d3.scaleBand()
.range([0, vis.width])
.padding(0.4);
vis.yScale = d3.scaleLinear()
.range([vis.height, 0]);
vis.xAxis = d3.axisBottom()
.scale(vis.xScale);
vis.yAxis = d3.axisLeft()
.scale(vis.yScale);
// add chart title
vis.svg.append('g')
.attr('class', 'title bar-chart-title')
.append('text')
.text(vis.chartTitle)
.attr('transform', `translate(${vis.width / 2 45}, -50)`)
.attr('text-anchor', 'middle');
// tooltip
vis.tooltip = d3.select('body').append('div')
.attr('class', 'tooltip')
.attr('id', 'barChartTooltip');
// create the axis groups
vis.xAxisGroup = vis.svg.append('g')
.attr('class', 'x-axis axis')
.attr('transform', 'translate(0, ' vis.height ')');
vis.yAxisGroup = vis.svg.append('g')
.attr('class', 'y-axis axis');
// update the domains
vis.xScale.domain(vis.data.map(function(d) {
return d.color;
}));
vis.yScale.domain([0, 100]);
// draw the bars
vis.bars = vis.svg.selectAll('.bar')
.data(vis.data)
vis.bars.exit().remove();
vis.bars
.enter()
.append('rect')
.attr('class', 'bar')
.attr('x', d => vis.xScale(d.color))
.attr('y', d => vis.yScale(0))
.attr('width', vis.xScale.bandwidth())
.attr("height", 0)
.on('mouseover', function(event, d) {
d3.select(this)
.attr('stroke-width', '2px')
.attr('stroke', 'grey')
.attr('fill', 'blue')
vis.tooltip
.style('opacity', 1)
.style('left', event.pageX 20 'px')
.style('top', event.pageY 'px')
.html(`
<div style='border: thin solid grey; border-radius: 5px; background: lightgrey; padding: 20px'>
<h3>${d.color}</h3>
<h4> ${100- d.plotValue.toLocaleString()}% decline </h4>
</div>
`)
})
.on('mouseout', function(event, d) {
d3.select(this)
.attr('stroke-width', 1)
.attr('stroke', '#456983')
.attr('fill', function(d) {
return 'red'
})
vis.tooltip
.style('opacity', 0)
.style('left', 0)
.style('top', 0)
.html(``);
})
.merge(vis.bars)
.transition()
.duration(1200)
.attr('y', d => vis.yScale(d.plotValue))
.attr('height', function(d) {
return vis.height - vis.yScale(d.plotValue);
})
.attr('fill', 'red')
// .attr('stroke', 'grey');
// add the axes
vis.xAxisGroup
.transition()
.duration(500)
.style('font-size', '15px')
.style('color', 'blue')
.call(d3.axisBottom((vis.xScale)))
.selectAll('text')
.attr('y', 30)
.attr('x', -35)
.attr('dy', '.35em')
.attr('transform', 'rotate(-30)');
vis.yAxisGroup
.transition()
.duration(500)
.style('font-size', '15px')
.style('color', 'blue')
.call(d3.axisLeft(vis.yScale));
vis.text = vis.svg.selectAll('.text')
.data(vis.data)
vis.text
.enter()
.append('text')
.attr('class', 'text')
.attr('text-anchor', 'middle')
.attr('x', d => vis.xScale(d.color) 27)
.attr('y', d => vis.yScale(d.plotValue) - 10)
.text(function(d) {
return '-' (100 - d.plotValue) '%';
})
<script src="https://d3js.org/d3.v7.min.js"></script>
Regarding doing the animation only when the user gets to the SVG, thats a completely different question, please keep just 1 issue per question here at SO. Thus, feel free to post another question for that separate issue (you don't need to use the d3.js tag in that new question).