D3 二维图表的绘制系列(三)多列柱状图

上一篇: 基础柱状图 https://blog.csdn.net/zjw_python/article/details/98201470

下一篇: 堆叠柱状图 https://blog.csdn.net/zjw_python/article/details/98207916

代码结构和初始化画布的Chart对象介绍,请先看 https://blog.csdn.net/zjw_python/article/details/98182540

本图完整的源码地址: https://github.com/zjw666/D3_demo/tree/master/src/barChart/multiBarChart

1 图表效果

在这里插入图片描述

2 数据

date,food,transportation,education
Mon,30,40,50
Tue,20,80,100
Wed,20,50,80
Thu,10,30,40
Fri,15,20,35
Sat,10,30,70
Sun,20,50,60

3 关键代码

导入数据

d3.csv('./data.csv', function(d){
    return {
        date: d.date,
        food: +d.food,
        transportation: +d.transportation,
        education: +d.education
    };
}).then(function(data){
.....

配置一些样式参数

const config = {
        barOuterPadding: 0.15,
        barColor: chart._colors(0),
        margins: {top: 80, left: 80, bottom: 50, right: 80},
        textColor: 'black',
        gridColor: 'gray',
        tickShowGrid: [20, 40, 60, 80, 100],
        title: '多列直方图',
        barInnerPadding: 2,
        hoverColor: 'white',
        animateDuration: 1000
    }

尺度转换,注意与基础柱状图不同的是,Y轴的最大值为三个属性和的最大值

/* ----------------------------尺度转换------------------------  */
    chart.scaleX = d3.scaleBand()
                    .domain(data.map((d) => d.date))
                    .range([0, chart.getBodyWidth()])
                    .padding(config.barOuterPadding);
    
    chart.scaleY = d3.scaleLinear()
                    .domain([0, d3.max(data, (d) => d3.max([d.food, d.transportation, d.education]))])
                    .range([chart.getBodyHeight(), 0])

渲染柱形, 注意我们将数据绑定到g元素上,之后多个柱形平分一个bandwidth的宽度

/* ----------------------------渲染柱形------------------------  */
    chart.renderBars = function(){
        //改变数据结构,方便渲染
        const multiData = d3.zip.apply(this, data.map((d) => {
            let item = [];
            Object.keys(d).forEach((key) => {
                if (key !== 'date'){
                    item.push([d.date, d[key], key]);
                }
            });
            return item;
        }));

        let groups = chart.body().selectAll('.g')
                            .data(multiData);
        let bars = groups.enter()
                            .append('g')
                         .merge(groups)
                            .attr('class', (d) => 'g ' + d[0][2])
                            .attr('fill', (d,i) => chart._colors(i))
                            .selectAll('.bar')
                            .data((d) => d);
            
            groups.exit()
                    .remove();

            bars.enter()
                    .append('rect')
                    .attr('class','bar')
                .merge(bars)
                    .attr('x', (d) => {
                        return chart.scaleX(d[0]) + chart.scaleX.bandwidth() / multiData.length * (data.columns.indexOf(d[2]) - 1);
                    })
                    .attr('y', chart.scaleY(0))
                    .attr('width', chart.scaleX.bandwidth() / multiData.length - config.barInnerPadding * (multiData.length-1))
                    .attr('height', 0)
                    .transition().duration(config.animateDuration)
                    .attr('height', (d) => chart.getBodyHeight() - chart.scaleY(d[1]))
                    .attr('y', (d) => chart.scaleY(d[1]));
            
            bars.exit()
                    .remove();
    }

渲染坐标轴,与基础柱状图类似

/* ----------------------------渲染坐标轴------------------------  */
    chart.renderX = function(){
        chart.svg().insert('g','.body')
                .attr('transform', 'translate(' + chart.bodyX() + ',' + (chart.bodyY() + chart.getBodyHeight()) + ')')
                .attr('class', 'xAxis')
                .call(d3.axisBottom(chart.scaleX));
    }

    chart.renderY = function(){
        chart.svg().insert('g','.body')
                .attr('transform', 'translate(' + chart.bodyX() + ',' + chart.bodyY() + ')')
                .attr('class', 'yAxis')
                .call(d3.axisLeft(chart.scaleY));
    }

    chart.renderAxis = function(){
        chart.renderX();
        chart.renderY();
    }

渲染文本标签和网格线,与基础柱状图类似

/* ----------------------------渲染文本标签------------------------  */
    chart.renderText = function(){
        d3.select('.xAxis').append('text')
                            .attr('class', 'axisText')
                            .attr('x', chart.getBodyWidth())
                            .attr('y', 0)
                            .attr('fill', config.textColor)
                            .attr('dy', 30)
                            .text('日期');

        d3.select('.yAxis').append('text')
                            .attr('class', 'axisText')
                            .attr('x', 0)
                            .attr('y', 0)
                            .attr('fill', config.textColor)
                            .attr('transform', 'rotate(-90)')
                            .attr('dy', -40)
                            .attr('text-anchor','end')
                            .text('每日支出(元)');
    }

    /* ----------------------------渲染网格线------------------------  */
    chart.renderGrid = function(){
        d3.selectAll('.yAxis .tick')
            .each(function(d, i){
                if (config.tickShowGrid.indexOf(d) > -1){
                    d3.select(this).append('line')
                        .attr('class','grid')
                        .attr('stroke', config.gridColor)
                        .attr('x1', 0)
                        .attr('y1', 0)
                        .attr('x2', chart.getBodyWidth())
                        .attr('y2', 0);
                }
            });
    }

最后绑定鼠标交互事件

/* ----------------------------绑定鼠标交互事件------------------------  */
    chart.addMouseOn = function(){
        //防抖函数
        function debounce(fn, time){
            let timeId = null;
            return function(){
                const context = this;
                const event = d3.event;
                timeId && clearTimeout(timeId)
                timeId = setTimeout(function(){
                    d3.event = event;
                    fn.apply(context, arguments);
                }, time);
            }
        }

        d3.selectAll('.bar')
            .on('mouseover', function(d){
                const e = d3.event;
                const position = d3.mouse(chart.svg().node());

                d3.select(e.target)
                    .attr('fill', config.hoverColor);
                
                chart.svg()
                    .append('text')
                    .classed('tip', true)
                    .attr('x', position[0]+5)
                    .attr('y', position[1])
                    .attr('fill', config.textColor)
                    .text( d[2] + ':' + d[1] + '元');
            })
            .on('mouseleave', function(d){
                const e = d3.event;
                
                d3.select(e.target)
                    .attr('fill', chart._colors(data.columns.indexOf(d[2]) - 1));
                    
                d3.select('.tip').remove();
            })
            .on('mousemove', debounce(function(){
                    const position = d3.mouse(chart.svg().node());
                    d3.select('.tip')
                    .attr('x', position[0]+5)
                    .attr('y', position[1]-5);
                }, 6)
            );
    }

大功告成!!!

发布了250 篇原创文章 · 获赞 88 · 访问量 19万+

猜你喜欢

转载自blog.csdn.net/zjw_python/article/details/98205614