使用 D3.js 創建柱狀堆積圖

柱狀堆積圖

項目地址
使用 D3.js 創建的圖表:

8.1 效果圖

完成效果

可以看到每組數據都進行了疊加。

現在來看一下具體實現:

8.2 實現

堆疊圖雖然和柱狀圖在展示上相差不是很多,但是在實現上差距還是有的。簡單的柱狀圖是使用 svg 中的 rect 元素,根據數據,賦予 rect 相應的寬高來展示數據的差異。但是堆疊圖是使用多個 rect 堆疊起,其中的 rect 關係我們是需要計算的。還好在 D3.js 中提供了相關的 API: d3.shape 中的 d3.stack ,對數據進行處理。

本次示例中也是使用的官方示例數據。

8.2.1 座標軸

座標軸的生成在之前的文章中也提到不少,這裏爲了嘗試更多的 scale ,使用了在本例中不太合適的 scaleTime 比例尺。

注意 比例尺的選擇要根據展示的圖的 nature 來選擇合適的比例尺,不能只是淡淡因爲 x 軸 lable 的數據是什麼類型就選擇什麼類型的比例尺。

看到這裏,應該發現我們的展示圖上有一些問題:柱狀圖並不是居中在座標的 ticks 中,而是有一個 1/2 bandWidth 的偏移。這也是 scaleTime 的原因。

上代碼

/** 數據格式
     * [
        {month: new Date(2015, 0, 1), apples: 3840, bananas: 1920, cherries: 960, dates: 400},
        {month: new Date(2015, 1, 1), apples: 1600, bananas: 1440, cherries: 960, dates: 400},
        {month: new Date(2015, 2, 1), apples:  640, bananas:  960, cherries: 640, dates: 400},
        {month: new Date(2015, 3, 1), apples:  320, bananas:  480, cherries: 640, dates: 400}
      ]
     */
// ...
const timeDate = data.map(datum => datum.month)
// y 軸 scale
const yScale = scaleLinear()
	.domain([0, max(series, d => max(d, d => d[1]))])
	.range([this.height - padding.pt - padding.pb, 0]);

const yAxis = axisLeft(yScale)

svg.append('g')
    .classed("y-axis", true)
    .call(yAxis);

// y軸寬度
const yAxisWidth: number = getYAxisWidth(svg.select('.y-axis'))
svg.select(".y-axis")
    .attr("transform", `translate(${padding.pl + yAxisWidth},${padding.pt})`);

// 爲了給兩端留出空白區域
const phMinDate = timeMonth.offset(min(timeDate),-1);
const phMaxDate = timeMonth.offset(max(timeDate),1);

// x軸scale
const xScale = scaleTime()
.domain([min(timeDate),max(timeDate)])
.range([padding.pl, this.width - padding.pr - yAxisWidth]);

// x軸
const xAxis = axisBottom(xScale)
.ticks(timeMonth.every(1))
.tickFormat(timeFormat('%y-%m'))

svg.append('g')
    .classed("x-axis", true)
    .attr("transform", `translate(${yAxisWidth},${this.height - padding.pb})`)
    .call(xAxis)
// ...

其中使用的 utils 函數 - getYAxisWidth項目 可查看。

這個示例展示了 scaleTime 的用法以及對時間的進一步格式化:

// x軸
const xAxis = axisBottom(xScale)
.ticks(timeMonth.every(1))
.tickFormat(timeFormat('%y-%m'))

timeMonth.every 的參數值爲 3 的時候,我們就可以得到按季度分隔的時間軸了。

這時我們可以看到:

座標軸
兩個座標軸已然準備就緒。

8.2.2 數據展示

要展示 stack 數據,需要對原始數據進行預處理。D3.js 提供 d3.stack 處理函數:

const stackIns = stack()
    .keys(Object.keys(data[0]).slice(1))
    .order(stackOrderNone)
    .offset(stackOffsetNone);

const series = stackIns(data);

現在,我們得到的 series 即時我們要繪製的堆疊圖的能夠識別的數據:

svg.selectAll('g.stack')
    .data(series)
    .join(
        enter => enter.append('g'),
        update => update,
        exit => exit.remove()
	)
    .classed('stack', true)
    .attr('fill', (d:any,i:number)=>schemePaired[i])
    .attr('transform',`translate(${yAxisWidth},${padding.pt})`)
    .selectAll('rect')
    .data(d => d)
    .join(
    enter => enter.append('rect'),
    update => update,
    exit => exit.remove()
)
    .attr('x', (d: any) => xScale(d.data.month))
    .attr('y', (d: any) => yScale(d[1]))
    .attr('height', (d: any) => yScale(d[0]) - yScale(d[1]))
    .attr('width', 14)

這樣像圖 8.1 的效果就出來了。現在來看一下完整的代碼:

// bp-stack.ts

import Component from '@glimmer/component';
import { action } from '@ember/object';
import Layout from 'ember-d3-demo/utils/d3/layout';
import { scaleTime, scaleLinear } from 'd3-scale';
import { axisBottom, axisLeft } from 'd3-axis';
import { min, max } from 'd3-array';
import { timeMonth } from 'd3-time';
import { timeFormat } from 'd3-time-format';
import { stack, stackOrderNone, stackOffsetNone } from 'd3-shape';
import { getYAxisWidth } from 'ember-d3-demo/utils/d3/yAxisWidth';
import { schemePaired} from 'd3-scale-chromatic';

interface D3BpStackArgs {
    data: any[];
    /** 數據格式
     * [
        {month: new Date(2015, 0, 1), apples: 3840, bananas: 1920, cherries: 960, dates: 400},
        {month: new Date(2015, 1, 1), apples: 1600, bananas: 1440, cherries: 960, dates: 400},
        {month: new Date(2015, 2, 1), apples:  640, bananas:  960, cherries: 640, dates: 400},
        {month: new Date(2015, 3, 1), apples:  320, bananas:  480, cherries: 640, dates: 400}
      ]
     */
    width: number;
    height: number;
}

export default class D3BpStack extends Component<D3BpStackArgs> {
    constainer: any = null
    width: number = this.args.width
    height: number = this.args.height

    @action
    initChart() {
        const data = this.args.data
        let layout = new Layout('.bp-stack')

        let { width, height } = this

        if (width) {
            layout.setWidth(width)
        } else {
            width = layout.getWidth()
        }
        if (height) {
            layout.setHeight(height)
        } else {
            height = layout.getHeight()
        }
        const container = layout.getContainer()
        this.width = layout.getWidth()
        this.height = layout.getHeight()
        this.constainer = container
        const padding = layout.getPadding()

        // 生成 svg
        let svg = container.append('svg')
            .attr("width", width)
            .attr("height", height);

        const stackIns = stack()
            .keys(Object.keys(data[0]).slice(1))
            .order(stackOrderNone)
            .offset(stackOffsetNone);

        const series = stackIns(data);

        const timeDate = data.map(datum => datum.month)

        // y 軸 scale
        const yScale = scaleLinear()
            .domain([0, max(series, d => max(d, d => d[1]))])
            .range([this.height - padding.pt - padding.pb, 0]);

        const yAxis = axisLeft(yScale)

        svg.append('g')
            .classed("y-axis", true)
            .call(yAxis);

        // y軸寬度
        const yAxisWidth: number = getYAxisWidth(svg.select('.y-axis'))
        svg.select(".y-axis")
            .attr("transform", `translate(${padding.pl + yAxisWidth},${padding.pt})`);
        
        // 爲了給兩端留出空白區域
        const phMinDate = timeMonth.offset(min(timeDate),-1);
        const phMaxDate = timeMonth.offset(max(timeDate),1);

        // x軸scale
        const xScale = scaleTime()
            .domain([min(timeDate),max(timeDate)])
            .range([padding.pl, this.width - padding.pr - yAxisWidth]);

        // x軸
        const xAxis = axisBottom(xScale)
            .ticks(timeMonth.every(1))
            .tickFormat(timeFormat('%y-%m'))

        svg.append('g')
            .classed("x-axis", true)
            .attr("transform", `translate(${yAxisWidth},${this.height - padding.pb})`)
            .call(xAxis)

        svg.selectAll('g.stack')
            .data(series)
            .join(
                enter => enter.append('g'),
                update => update,
                exit => exit.remove()
            )
            .classed('stack', true)
            .attr('fill', (d:any,i:number)=>schemePaired[i])
            .attr('transform',`translate(${yAxisWidth},${padding.pt})`)
            .selectAll('rect')
            .data(d => d)
            .join(
                enter => enter.append('rect'),
                update => update,
                exit => exit.remove()
            )
            .attr('x', (d: any) => xScale(d.data.month))
            .attr('y', (d: any) => yScale(d[1]))
            .attr('height', (d: any) => yScale(d[0]) - yScale(d[1]))
            .attr('width', 14)

    }
}

{{!-- bp-stack.hbs --}}
<div class="bp-stack" {{did-insert this.initChart}}></div>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章