[技术分享]D3.js 极坐标系下的柱状图

shenyiling发布于3 个月前 • 238 次阅读

极坐标系的柱状图

虽然标题是柱状图,但其实使用arc饼图生成器画得,所以感觉叫饼图也可以~~

预览

在这里插入图片描述 LIVE DEMO

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)返回一个包含xy坐标的数组,坐标相对于指定的container元素,可以是html元素或svg元素。

d3.arc

常配合d3.pieapi使用 用于在饼图或环形图中生成圆形、圆环、扇形。一个扇形需要指定起始角度、终止角度、内半径、外半径等。还有一些不常用的属性,例如扇形的拐角半径,间隔半径,间隔角度等。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>
共收到 0 条回复