最近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