柱狀堆積圖
項目地址
使用 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>