You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修改D3代码实现柱状图滚动触发向下飞入动画?

问题描述

我用D3.js制作柱状图,当前柱子会从左上角斜向飞入(受D3坐标系影响),想要实现柱子直接向下飞入的动画效果,并且仅当用户滚动到该元素所在区域时才触发动画(页面较长,用户需要滚动很久才能到达)。以下是我的代码:

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) + '%';
    })

解决方案

一、修改柱子动画为向下飞入

当前动画同时过渡x、y、height属性导致斜向运动,要实现向下飞入,需让柱子初始状态固定x位置,从顶部(y=0)、高度为0的状态开始,仅过渡y和height属性:

关键修改代码:

// 绘制柱子
vis.bars = vis.svg.selectAll('.bar')
    .data(vis.data)

vis.bars.exit().remove();

// 初始设置柱子状态:x位置正确,y在顶部,高度为0
const barsEnter = vis.bars
    .enter()
    .append('rect')
    .attr('class', 'bar')
    .attr('x', d => vis.xScale(d.color))
    .attr('y', 0)
    .attr('width', vis.xScale.bandwidth())
    .attr('height', 0)
    .attr('fill', 'red')
    // 保留原mouseover/mouseout逻辑...

// 合并后仅过渡y和height,实现向下飞入
barsEnter.merge(vis.bars)
    .transition()
    .duration(1200)
    .attr('y', d => vis.yScale(d.plotValue))
    .attr('height', function(d) { return vis.height - vis.yScale(d.plotValue); })

二、添加滚动触发动画逻辑

通过监听页面滚动,判断图表元素是否进入视口,仅在进入时执行动画:

关键修改代码:

// 封装动画函数
function animateBars() {
    barsEnter.merge(vis.bars)
        .transition()
        .duration(1200)
        .attr('y', d => vis.yScale(d.plotValue))
        .attr('height', function(d) { return vis.height - vis.yScale(d.plotValue); })
    
    // 可选:让数值标签和柱子同步动画
    vis.text.merge(vis.text)
        .transition()
        .duration(1200)
        .attr('y', d => vis.yScale(d.plotValue) - 10)
        .attr('opacity', 1)
}

// 获取图表容器元素
const chartContainer = document.getElementById(vis.parentElement);

// 检查元素是否在视口内
function checkScroll() {
    const rect = chartContainer.getBoundingClientRect();
    const isInView = rect.top < window.innerHeight && rect.bottom > 0;
    if (isInView) {
        animateBars();
        // 仅执行一次动画,移除监听
        window.removeEventListener('scroll', checkScroll);
    }
}

// 初始检查(页面加载时元素可能已在视口)
checkScroll();
// 添加滚动监听
window.addEventListener('scroll', checkScroll);

额外调整:数值标签初始状态

如果要让数值标签和柱子同步出现,需设置初始隐藏状态:

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', 0 - 10) // 初始在顶部
    .attr('opacity', 0) // 初始隐藏
    .text( function (d) {
        return '-' + (100 - d.plotValue) + '%';
    })

内容的提问来源于stack exchange,提问作者equanimity

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 11:21:07