D3 二维图表的绘制系列(二十)河流图

上一篇: 封闭图 https://blog.csdn.net/zjw_python/article/details/98591118

下一篇: 仪表盘图 https://blog.csdn.net/zjw_python/article/details/98596174

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

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

1 图表效果

在这里插入图片描述

2 数据

date,food,transportation,education,house,clothes
2019-06-01,30,40,50,56,30
2019-06-02,20,80,56,42,60
2019-06-03,20,50,80,57,54
2019-06-04,10,30,40,36,62
2019-06-05,15,20,35,50,43
2019-06-06,10,30,70,73,34
2019-06-07,20,56,60,37,20
2019-06-08,15,20,65,46,25
2019-06-09,40,34,43,64,45
2019-06-10,36,46,60,75,62
2019-06-11,25,32,40,60,38
2019-06-12,10,18,32,34,55
2019-06-13,34,23,42,43,32
2019-06-14,30,46,25,25,23
2019-06-15,22,62,52,20,26
2019-06-16,17,45,42,10,21
2019-06-17,13,50,72,21,30
2019-06-18,10,34,65,34,20
2019-06-19,25,34,56,44,15
2019-06-21,15,14,32,56,32
2019-06-22,30,32,42,24,42
2019-06-23,20,25,23,32,23
2019-06-24,5,14,52,42,10
2019-06-25,18,36,25,36,12
2019-06-26,34,40,30,20,22
2019-06-27,12,32,34,54,34
2019-06-28,10,50,24,68,20
2019-06-29,26,55,32,35,36
2019-06-30,21,40,20,53,30

3 关键代码

导入数据

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

一些样式参数配置

 const config = {
        margins: {top: 80, left: 80, bottom: 50, right: 80},
        textColor: 'black',
        gridColor: 'gray',
        title: '基础河流图',
        animateDuration: 1000
    }

尺度转换,河流图的样子与堆叠面积图有几分类似,两者都运用d3.stack进行布局,区别在于,河流图堆叠的基准线不再是X轴。虽然布局算法不同,但我们只需要稍微更改一下配置选项即可,非常简单

/* ----------------------------尺度转换------------------------  */
    chart.scaleX = d3.scaleTime()
                    .domain([new Date(data[0].date), new Date(data[data.length-1].date)])
                    .range([0, chart.getBodyWidth()]);

    chart.scaleY = d3.scaleLinear()
                    .domain([0, (Math.floor((
                        d3.max(data, (d) => d.house) +
                        d3.max(data, (d) => d.food) +
                        d3.max(data, (d) => d.education) +
                        d3.max(data, (d) => d.transportation) +
                        d3.max(data, (d) => d.clothes)
                        )/10) + 1)*10])
                    .range([chart.getBodyHeight(), 0])

    chart.stack = d3.stack()
                    .keys(['house', 'food', 'transportation', 'education', 'clothes'])
                    .order(d3.stackOrderInsideOut)
                    .offset(d3.stackOffsetWiggle);

经过stack函数处理后的数据自带布局信息,但为达到从左向右过渡动画的效果,我们还是对数据点进行线性插个值,然后运用中间帧函数实现动画效果

/* ----------------------------渲染面------------------------  */
    chart.renderArea = function(){
        const areas = chart.body().insert('g',':first-child')
                        .attr('transform', 'translate(0, -' +  d3.max(data, (d) => d3.mean(Object.values(d))) + ')')   // 使流图的位置处于Y轴中部
                        .selectAll('.area')
                        .data(chart.stack(data));

              areas.enter()
                        .append('path')
                        .attr('class', (d) => 'area area-' + d.key)
                    .merge(areas)
                        .style('fill', (d,i) => chart._colors(i))
                        .transition().duration(config.animateDuration)
                        .attrTween('d', areaTween);

        //中间帧函数
        function areaTween(_d){
            if (!_d) return;
            const generateArea = d3.area()
                        .x((d) => d[0])
                        .y0((d) => d[1])
                        .y1((d) => d[2])
                        .curve(d3.curveCardinal.tension(0));

            const pointX = data.map((d) => chart.scaleX(new Date(d.date)));
            const pointY0 = _d.map((d) => chart.scaleY(d[0]));
            const pointY1 = _d.map((d) => chart.scaleY(d[1]));

            const interpolate = getAreaInterpolate(pointX, pointY0, pointY1);

            const ponits = [];

            return function(t){
                ponits.push([interpolate.x(t), interpolate.y0(t), interpolate.y1(t)]);
                return generateArea(ponits);
            }
        }

        //点插值
        function getAreaInterpolate(pointX, pointY0, pointY1){

            const domain = d3.range(0, 1, 1/(pointX.length-1));
            domain.push(1);

            const interpolateX = d3.scaleLinear()
                                    .domain(domain)
                                    .range(pointX);

            const interpolateY0 = d3.scaleLinear()
                                    .domain(domain)
                                    .range(pointY0);

             const interpolateY1 = d3.scaleLinear()
                                    .domain(domain)
                                    .range(pointY1);
            return {
                x: interpolateX,
                y0: interpolateY0,
                y1: interpolateY1
            };

        }

    }

接着就是渲染坐标轴、网格线和文本标签等常规操作,值得注意是,这里的X轴是时间尺度,为了美观,我们需要运用tickFormat将其格式化一下,只显示日期。

/* ----------------------------渲染坐标轴------------------------  */
    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).ticks(d3.timeDay.every(3)).tickFormat((d) => d3.timeFormat("%d")(d)));
    }

    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', 40)
                            .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('.xAxis .tick')
            .append('line')
            .attr('class','grid')
            .attr('stroke', config.gridColor)
            .attr('stroke-dasharray', '10,10')
            .attr('x1', 0)
            .attr('y1', 0)
            .attr('x2', 0)
            .attr('y2', -chart.getBodyHeight());
    }

最后绑定鼠标交互事件,当鼠标悬停时,突出显示当前选中块,其余块的透明度减小

/* ----------------------------绑定鼠标交互事件------------------------  */
    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('.area')
            .on('mouseover', function(d){
                const e = d3.event;
                const position = d3.mouse(chart.svg().node());
                e.target.style.cursor = 'hand'

                d3.selectAll('.area')
                    .attr('fill-opacity', 0.3);

                d3.select(e.target)
                    .attr('fill-opacity', 1);

                chart.svg()
                    .append('text')
                    .classed('tip', true)
                    .attr('x', position[0]+5)
                    .attr('y', position[1])
                    .attr('fill', config.textColor)
                    .text(d.key);
            })
            .on('mouseleave', function(){
                const e = d3.event;

                d3.selectAll('.area')
                    .attr('fill-opacity', 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/98592543
今日推荐