极坐标系的柱状图
虽然标题是柱状图,但其实使用
arc
饼图生成器画得,所以感觉叫饼图也可以~~
预览
API
使用到的部分api
d3.scaleLinear
线性比例尺,类似一个线性函数,通过domain
属性定义输入域,range
属性定义值域,例如:
var x = d3.scaleLinear()
.domain([10, 130])
.range([0, 960]);
x(20); // 80
x(50); // 320
d3.scaleBand
分段比例尺,和scaleLinear
作用类似,不过输入域domain
不是连续的数据,而是一个数组,通过数组长度,将值域分成多个段,例如:
var x = d3.scaleBand()
.domain([1,2,3,4])
.range([0,100])
x(1) // 0
x(2) // 25
x(4) // 75
d3.mouse
d3.mouse(container)
返回一个包含x
和y
坐标的数组,坐标相对于指定的container元素,可以是html元素或svg元素。
d3.arc
常配合
d3.pie
api使用
用于在饼图或环形图中生成圆形、圆环、扇形。一个扇形需要指定起始角度、终止角度、内半径、外半径等。还有一些不常用的属性,例如扇形的拐角半径,间隔半径,间隔角度等。example:
var arc = d3.arc()
.innerRadius(0)
.outerRadius(100)
.startAngle(0)
.endAngle(Math.PI / 2);
arc(); // "M0,-100A100,100,0,0,1,100,0L0,0Z"
d3.pie
将一组数组转换为生成饼图和环形图需要的数据,这些角度信息可以被传递给arc
生成器。example:
var data = [1, 1, 2, 3, 5, 8, 13, 21]
var arcs = d3.pie()(data)
// arcs 数据
[
{"data": 1, "value": 1, "index": 6, "startAngle": 6.050474740247008, "endAngle": 6.166830023713296, "padAngle": 0},
{"data": 1, "value": 1, "index": 7, "startAngle": 6.166830023713296, "endAngle": 6.283185307179584, "padAngle": 0},
{"data": 2, "value": 2, "index": 5, "startAngle": 5.817764173314431, "endAngle": 6.050474740247008, "padAngle": 0},
{"data": 3, "value": 3, "index": 4, "startAngle": 5.468698322915565, "endAngle": 5.817764173314431, "padAngle": 0},
{"data": 5, "value": 5, "index": 3, "startAngle": 4.886921905584122, "endAngle": 5.468698322915565, "padAngle": 0},
{"data": 8, "value": 8, "index": 2, "startAngle": 3.956079637853813, "endAngle": 4.886921905584122, "padAngle": 0},
{"data": 13, "value": 13, "index": 1, "startAngle": 2.443460952792061, "endAngle": 3.956079637853813, "padAngle": 0},
{"data": 21, "value": 21, "index": 0, "startAngle": 0.000000000000000, "endAngle": 2.443460952792061, "padAngle": 0}
]
假如数据数组的元素是对象,可以通过.value
属性设置需要访问的值,example:
var data = [
{"number": 4, "name": "Locke"},
{"number": 8, "name": "Reyes"},
{"number": 15, "name": "Ford"},
{"number": 16, "name": "Jarrah"},
{"number": 23, "name": "Shephard"},
{"number": 42, "name": "Kwon"}
]
var arcs = d3.pie()
.value(function(d) { return d.number; })
(data)
source code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
body {
width: 100vw;
height: 100vh;
display: flex;
}
#radial-bar-chart {
margin: auto;
position: relative;
box-shadow: 0 0 10px 0 #eee;
}
.tooltip {
position: absolute;
left: 0;
top: 0;
box-shadow: 0 0 10px 0 #ccc;
width: 200px;
height: 100px;
border-radius: 4px;
box-sizing: border-box;
padding: 15px 10px;
color: #606060;
opacity: 0;
background-color: #fff;
}
.tooltip > .province {
margin-bottom: 15px;
}
</style>
</head>
<body>
<div id="radial-bar-chart"></div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
const provinces = [
'北京市',
'天津市',
'上海市',
'重庆市',
'河北省',
'河南省',
'云南省',
'辽宁省',
'黑龙江省',
'湖南省',
'安徽省',
'山东省',
'新疆维吾尔',
'江苏省',
'浙江省',
'江西省',
'湖北省',
'广西壮族',
'甘肃省',
'山西省',
'内蒙古',
'陕西省',
'吉林省',
'福建省',
'贵州省',
'广东省',
'青海省',
'西藏',
'四川省',
'宁夏回族',
'海南省',
'台湾省',
'香港特别行政区',
'澳门特别行政区'
]
const width = 800
const height = 600
const innerRadius = 5
const outerRadius = 200
const data = randomData()
const chartColor = 'rgba(7, 92, 214, .7)'
const fontColor = '#606060'
const gridLineColor = 'rgb(210, 214, 218)'
const radiusScale = d3.scaleLinear()
.range([0, outerRadius - 10])
.domain([0, d3.max(data, d => d.population)])
// 通过arc函数生成原型或圆环
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.padAngle(Math.PI / 400)
const svg = d3.select('#radial-bar-chart')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `translate(${width / 2}, ${height / 2})`)
drawGrid()
drawChart()
svg.append('g')
.attr('class', 'x-axis')
.call(xAxis)
renderToolTip()
function drawChart() {
// 饼图数据
const pieData = d3.pie().value(d => 10)(data)
const arcs = svg
.append('g')
.attr('class', 'arcs')
.selectAll('g')
.data(pieData)
.enter()
.append('path')
.attr('class', 'arc')
.attr('fill', chartColor)
.attr('d', function (d) {
// 动态设定外半径
arc.outerRadius(radiusScale(d.data.population))
// 注册path data
return arc(d)
})
}
function xAxis(g) {
// 计算x轴标签位置
const xLabel = d3.scaleBand()
.domain(data.map(d => d.province))
.range([0, 2 * Math.PI])
g.attr('class', 'x-axis')
.attr('text-anchor', 'start')
.attr('dominant-baseline', 'middle')
.attr('stroke', fontColor)
.attr('stroke-width', 1)
.style('font-size', 14)
.style('font-weight', 100)
.call(g => {
g.selectAll('g')
.data(data)
.enter()
.append('g')
.attr('transform', d => {
const rotateAngle = tsRadian2angle(xLabel(d.province) + xLabel.bandwidth() / 2) - 90
return `
rotate(${ (rotateAngle) })
translate(${ outerRadius + 5 }, 0)
`
})
.call(g => {
g.append('line')
.attr('x2', -5)
g.append('text')
.text(d => d.province)
.attr('transform', function (d, i) {
// 通过getBBox访问到text元素的长度
const val = tsRadian2angle(xLabel(d.province) + xLabel.bandwidth() / 2) > 180
? `rotate(180) translate(${ -this.getBBox().width - 10 }, 0)`
: 'translate(10, 0)'
return val
})
})
})
}
// 绘制背景网格
function drawGrid() {
const gridWrapper = svg.append('g')
.attr('class', 'grid-wrapper')
.attr('fill', 'transparent')
.attr('stroke', gridLineColor)
.attr('stroke-width', 1)
.attr('stroke-dasharray', '5,5')
.attr('text-anchor', 'end')
.attr('dominant-baseline', 'middle')
.style('font-size', 12)
.style('font-weight', 100)
const maxPopulation = d3.max(data, d => d.population) * outerRadius / ( outerRadius - 10 )
for (let i = 1; i <= 6; i++) {
const n = maxPopulation / 6 * i >>> 0
gridWrapper.append('circle')
.attr('r', outerRadius / 6 * i)
gridWrapper.append('text')
.text(n)
.attr('transform', `translate(0, ${ -outerRadius / 6 * i })`)
}
const bandWidth = 2 * Math.PI / data.length
for (let i = 0; i < data.length; i++) {
const x = outerRadius * Math.sin( bandWidth * i + bandWidth / 2)
const y = outerRadius * Math.cos( bandWidth * i + bandWidth / 2)
gridWrapper.append('line')
.attr('x2', x)
.attr('y2', y)
}
}
function renderToolTip() {
// 设置tooltip
const toolTip = d3.select('#radial-bar-chart')
.append('div')
.attr('class', 'tooltip')
toolTip.html(
`
<div class="province">
</div>
<div class="population">
<span>population</span>
<span class="value"></span>
</div>
`
)
// 设置mousemove事件
d3.selectAll('.arc')
.on('mousemove', e => {
const svg = document.querySelector('svg')
const [x, y] = d3.mouse(svg)
toolTip.style('opacity', 1)
toolTip.style('left', `${ x + 50 }px`)
toolTip.style('top', `${ y + 50 }px`)
toolTip.select('.province').text('四川省')
toolTip.select('.population .value').text('10000')
})
svg.on('mouseout', () => {
toolTip.style('opacity', 0)
})
}
// 转换弧度值为角度
function tsRadian2angle(radian) {
return radian * 180 / Math.PI
}
// gen random data
function randomData() {
const data = []
for (const p of provinces) {
data.push({
province: p,
population: Math.random() * 40000000 + 30000000 >>> 0
})
}
data.sort((a, b) => b.population - a.population)
return data
}
// TODO: 内半径颜色
</script>
</body>
</html>