D3js 实现bar-chart-race

1,455 阅读4分钟

最近PM让我用d3实现bar-chart-race,然后我上d3官网,看了一下例子,有现成的,哦豁,真好!(^▽^) 链接 observablehq.com/@d3/bar-cha…

直接拿里面的用,改成vuejs实现的即可 以下是我用vue实现的

<template>
    <div id="bar-chart-race"></div>
</template>
<script>
import RaceData from './beatBar';
var margin = {
    top: 16,
    right: 6,
    bottom: 6,
    left: 0,
};
var height;
var barSize = 48;
var n = 6;
var width = 600;
var color;
var data;
var x;
var y;
var next;
var nameframes;
var keyframes;
var datevalues;
var rank;
var names;
var prev;
var duration = 500;
export default {
    data() {
        return {
            id: 'bar-chart-race',
            raceData: RaceData // 数据结构
        };
    },
    mounted() {
        data = this.raceData;
        this.initBarChartRaceBase();
    },
    methods: {
        async initBarChartRaceBase() {
            d3.select('#svg' + this.id).remove();
            // //指定容器的宽高
            // 通过一个category,bar的color值相同
            var getColor = function () {
                const scale = d3.scaleOrdinal(d3.schemeTableau10);
                if (data.some(d => d.category !== undefined)) {
                    const categoryByName = new Map(data.map(d => [d.name, d.category]))
                    scale.domain(Array.from(categoryByName.values()));
                    return d => scale(categoryByName.get(d.name));
                }
                return d => scale(d.name);
            };
            color = getColor();

            height = margin.top + barSize * n + margin.bottom;

            const svg = d3.select('#' + this.id).append('svg')
                .attr('width', width)
                .attr('height', height).attr("id", 'svg' + this.id);
            x = d3.scaleLinear([0, 1], [margin.left, width - margin.right]);
            y = d3.scaleBand()
                .domain(d3.range(n + 1))
                .rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
                .padding(0.1);
            /*获取names*/
            names = new Set(data.map(d => d.name));
            /* 获取keyframes*/
            datevalues = Array.from(d3A.rollup(data, ([d]) => d.value, d => d.date, d => d.name))
                .map(([date, data]) => [new Date(date), data])
                .sort(([a], [b]) => d3.ascending(a, b))
            console.log(datevalues);
            /*rank函数*/
            rank = function(value) {
                const data = Array.from(names, name => ({name, value: value(name)}));
                data.sort((a, b) => d3.descending(a.value, b.value));
                for (let i = 0; i < data.length; ++i) data[i].rank = Math.min(n, i);
                return data;
            };
            /* 获取keyframes*/
            var k = 10;
            var getKeyframes = function () {
                const keyframes = [];
                let ka, a, kb, b;
                for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
                    for (let i = 0; i < k; ++i) {
                        const t = i / k;
                        keyframes.push([
                            new Date(ka * (1 - t) + kb * t),
                            rank(name => (a.get(name) || 0) * (1 - t) + (b.get(name) || 0) * t)
                        ]);
                    }
                }
                keyframes.push([new Date(kb), rank(name => b.get(name) || 0)]);
                return keyframes;
            };
            keyframes = getKeyframes();

            /* 获取nameframes*/
            nameframes = d3A.groups(keyframes.flatMap(([, data]) => data), d => d.name);
            next = new Map(nameframes.flatMap(([, data]) => d3.pairs(data)));
            prev = new Map(nameframes.flatMap(([, data]) => d3.pairs(data, (a, b) => [b, a])));
            /*------bar-----*/
            var bars = function(svg) {
                let bar = svg.append("g")
                    .attr("fill-opacity", 0.6)
                    .selectAll("rect");

                return ([date, data], transition) => bar = bar
                    .data(data.slice(0, n), d => d.name)
                    .join(
                        enter => enter.append("rect")
                            .attr("fill", color)
                            .attr("height", y.bandwidth())
                            .attr("x", x(0))
                            .attr("y", d => y((prev.get(d) || d).rank))
                            .attr("width", d => x((prev.get(d) || d).value) - x(0)),
                        update => update,
                        exit => exit.transition(transition).remove()
                            .attr("y", d => y((next.get(d) || d).rank))
                            .attr("width", d => x((next.get(d) || d).value) - x(0))
                    )
                    .call(bar => bar.transition(transition)
                        .attr("y", d => y(d.rank))
                        .attr("width", d => x(d.value) - x(0)));
            };
            /*------axis x轴-----*/
            var axis = function (svg) {
                const g = svg.append("g")
                    .attr("transform", `translate(0,${margin.top})`);

                const axis = d3.axisTop(x)
                    .ticks(width / 160)
                    .tickSizeOuter(0)
                    .tickSizeInner(-barSize * (n + y.padding()));

                return (_, transition) => {
                    g.transition(transition).call(axis);
                    g.select(".tick:first-of-type text").remove();
                    g.selectAll(".tick:not(:first-of-type) line").attr("stroke", "white");
                    g.select(".domain").remove();
                };
            };
            /*---label显示--*/
            var labels = function (svg) {
                let label = svg.append("g")
                    .style("font", "bold 12px var(--sans-serif)")
                    .style("font-variant-numeric", "tabular-nums")
                    .attr("text-anchor", "end")
                    .selectAll("text");

                return ([date, data], transition) => label = label
                    .data(data.slice(0, n), d => d.name)
                    .join(
                        enter => enter.append("text")
                            .attr("transform", d => `translate(${x((prev.get(d) || d).value)},${y((prev.get(d) || d).rank)})`)
                            .attr("y", y.bandwidth() / 2)
                            .attr("x", -6)
                            .attr("dy", "-0.25em")
                            .text(d => d.name)
                            .call(text => text.append("tspan")
                                .attr("fill-opacity", 0.7)
                                .attr("font-weight", "normal")
                                .attr("x", -6)
                                .attr("dy", "1.15em")),
                        update => update,
                        exit => exit.transition(transition).remove()
                            .attr("transform", d => `translate(${x((next.get(d) || d).value)},${y((next.get(d) || d).rank)})`)
                            .call(g => g.select("tspan").tween("text", d => textTween(d.value, (next.get(d) || d).value)))
                    )
                    .call(bar => bar.transition(transition)
                        .attr("transform", d => `translate(${x(d.value)},${y(d.rank)})`)
                        .call(g => g.select("tspan").tween("text", d => textTween((prev.get(d) || d).value, d.value))));
            };

            var formatNumber = d3.format(",d");
            var formatDate = d3.utcFormat("%Y");

            var textTween = function(a, b) {
                const i = d3.interpolateNumber(a, b);
                return function(t) {
                    this.textContent = formatNumber(i(t));
                };
            };

            var ticker = function(svg) {
                const now = svg.append("text")
                    .style("font", `bold ${barSize}px var(--sans-serif)`)
                    .style("font-variant-numeric", "tabular-nums")
                    .attr("text-anchor", "end")
                    .attr("x", width - 6)
                    .attr("y", margin.top + barSize * (n - 0.45))
                    .attr("dy", "0.32em")
                    .text(formatDate(keyframes[0][0]));

                return ([date], transition) => {
                    transition.end().then(() => now.text(formatDate(date)));
                };
            };
            const updateBars = bars(svg);
            const updateAxis = axis(svg);
            const updateLabels = labels(svg);
            const updateTicker = ticker(svg);

            for (const keyframe of keyframes) {
                const transition = svg.transition()
                    .duration(duration)
                    .ease(d3.easeLinear);

                x.domain([0, keyframe[1][0].value]);

                updateAxis(keyframe, transition);
                updateBars(keyframe, transition);
                updateLabels(keyframe, transition);
                updateTicker(keyframe, transition);
                await transition.end();
            }
        }
    }
};
</script>

beatBar的数据结构如下

const beatBar = [ { date: '2000-01-01', name: "广州", category: "水果", value: 72537 }, { date: '2000-01-01', name: "深圳", category: "蔬菜", value: 70196 }, { date: '2000-01-01', name: '汕头', category: '干货', value: 53183 }, { date: '2000-01-01', name: '东莞', category: '干货', value: 62152 }, { date: '2000-01-01', name: '珠海', category: '肉类', value: 21232 }, { date: '2000-01-01', name: '中山', category: '水果', value: 23211 },

{
    date: '2001-01-01',
    name: "广州",
    category: "水果",
    value: 91233
},
{
    date: '2001-01-01',
    name: "深圳",
    category: "蔬菜",
    value: 32244
},
{
    date: '2001-01-01',
    name: '汕头',
    category: '干货',
    value: 345433
},
{
    date: '2001-01-01',
    name: '东莞',
    category: '干货',
    value: 12134
},
{
    date: '2001-01-01',
    name: '珠海',
    category: '肉类',
    value: 42212
},
{
    date: '2001-01-01',
    name: '中山',
    category: '水果',
    value: 64221
},

{
    date: '2002-01-01',
    name: "广州",
    category: "水果",
    value: 1223
},
{
    date: '2002-01-01',
    name: "深圳",
    category: "蔬菜",
    value: 85312
},
{
    date: '2002-01-01',
    name: '汕头',
    category: '干货',
    value: 21334
},
{
    date: '2002-01-01',
    name: '东莞',
    category: '干货',
    value: 96343
},
{
    date: '2002-01-01',
    name: '珠海',
    category: '肉类',
    value: 62231
},
{
    date: '2002-01-01',
    name: '中山',
    category: '水果',
    value: 32322
},

{
    date: '2003-01-01',
    name: "广州",
    category: "水果",
    value: 34211
},
{
    date: '2003-01-01',
    name: "深圳",
    category: "蔬菜",
    value: 23455
},
{
    date: '2003-01-01',
    name: '汕头',
    category: '干货',
    value: 12333
},
{
    date: '2003-01-01',
    name: '东莞',
    category: '干货',
    value: 19433
},
{
    date: '2003-01-01',
    name: '珠海',
    category: '肉类',
    value: 93333
},
{
    date: '2003-01-01',
    name: '中山',
    category: '水果',
    value: 83333
},

{
    date: '2004-01-01',
    name: "广州",
    category: "水果",
    value: 42111
},
{
    date: '2004-01-01',
    name: "深圳",
    category: "蔬菜",
    value: 12333
},
{
    date: '2004-01-01',
    name: '汕头',
    category: '干货',
    value: 66644
},
{
    date: '2004-01-01',
    name: '东莞',
    category: '干货',
    value: 92222
},
{
    date: '2004-01-01',
    name: '珠海',
    category: '肉类',
    value: 12233
},
{
    date: '2004-01-01',
    name: '中山',
    category: '水果',
    value: 56211
}

];

packpage需要引入d3和d3-array

然后定义全局变量即可(方便使用) window.d3 = d3; window.d3A = d3A;

写这一篇文章,主要是记录一下这个demo