D3
是一個 js library
,讓開發者在向量繪圖時,能快速算出圖表物件在繪圖區域的路徑座標、座標軸、動態顏色等許多好用的 function
。
本系列文章採用 vue
搭配 D3
是因為使用 data
驅動畫面的方式,會比直接操作 DOM
的效能更好。 D3
內建的繪製功能是類似於 jquery
的方式直接操作 DOM
,當資料變多時可能會拖垮效能出現延遲的畫面,網路上許多教程初期是用此方式,想看的話 google
一下就有囉~
此系列文章 D3
專注於使用他本身的計算函數和座標軸等優勢的功能,至於畫面更新等其他雜事就交給 vue
處理。當然,未來還有更多優化效能的方式,例如:SVG 換成 CANVAS 等,但都有其優缺點,等之後有空再來做比較。
準備 SVG
畫布
1 2 3 4 5 6
| <svg class="chart" :viewBox="viewBox" preserveAspectRatio="xMidYMin slice"> <g class="chartWrap" :transform="startPoint"> ... </g> </svg>
|
1 2 3 4 5 6 7 8
| .chart { width: 100%; padding-bottom: 100%; height: 1px; overflow: visible; ... }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import * as d3 from "d3";
export default { data() { return { data: [], chart: { width: 500, height: 500, paddingTop: 30, paddingRight: 30, paddingBottom: 100, paddingLeft: 60 }, hideTooltip: true }; }, computed: { viewBox() { let viewW = this.chart.width + this.chart.paddingRight + this.chart.paddingLeft; let viewH = this.chart.height + this.chart.paddingTop + this.chart.paddingBottom;
return `0 0 ${viewW} ${viewH}`; }, startPoint() { return `translate(${this.chart.paddingLeft}, ${this.chart.paddingTop})`; }, ... } }
|
準備隨機資料
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| methods: { randomData() { let min = 0; let max = 500; let random = [ { name: "鼓山區", value: [ { month: "6月", number: "233" }, { month: "7月", number: "412" }, { month: "8月", number: "533" }, { month: "9月", number: "267" }, { month: "10月", number: "321" } ] }, ... ];
for (let i = 0; i < 3; i++) { random[i].value.forEach((e) => { e.number = Math.floor(Math.random() * (max - min + 1)) + min; }); }
this.data = random; ... }, ... }
|
插入座標軸
1 2 3 4 5 6 7 8
| <g :transform="`translate(0, ${chart.height})`" class="axis axisX" fill="none" font-size="10" font-family="sans-serif" text-anchor="middle"> </g>
<g class="axis axisY" fill="none" font-size="10" font-family="sans-serif" text-anchor="end"> </g>
<text class="axisYText" :x="axisYText[0]" :y="axisYText[1]" dy="1em" transform="rotate(-90)" text-anchor="middle">件數</text>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| .chart { ... .chartWrap { .axis.axisY { .tick { line { stroke: #efefef; transform: translateX(1px); } &:nth-child(2) { line { stroke: black; } } } } .line { stroke-dasharray: 3000; } } }
|
因程式碼過多,這邊只講用到的 D3 函數部分,文章最後會有完整程式碼連結。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| xScale() { return d3 .scaleLinear() .domain([0, this.data[0].value.length]) .range([0, this.chart.width]); },
xAxis() { return d3 .axisBottom(this.xScale) .ticks(5) .tickFormat((d, i) => { return this.xLabel[i]; }); },
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| yScale() { let Ymin = 0; let Ymax; let newArray = [];
this.data.forEach(function(e, i) { e.value.forEach(function(ev) { newArray.push(ev.number); }); }); Ymax = d3.max(newArray);
return d3 .scaleLinear() .domain([Ymin, Ymax]) .range([this.chart.height, 0]); },
yAxis() { return d3 .axisLeft(this.yScale) .tickSizeInner(-this.chart.height); },
|
内侧刻度 (tickSizeInner) 和外侧刻度 (tickSizeOuter) 不同,内侧刻度是一个个单独的line元素,而外侧刻度则实际上是坐标轴线path的一部分。
在每次隨機資料後,動態插入座標軸。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| methods: { randomData() { ... document.querySelector('.chart .axisX').innerHTML = ''; document.querySelector('.chart .axisY').innerHTML = '';
d3 .select(".chart .axisX") .call(this.xAxis); d3 .select(".chart .axisY") .call(this.yAxis); }, ... }
|
繪製折線
注意: transition-group
標籤是拿來做折線 CSS
的動畫效果,這邊以效能考量皆採用 CSS
來做動畫,搭配 vue
transition
動態加上 class
,並不建議用 JS
的方法去計算動畫。
1 2 3 4
| <transition-group tag="g" name="growLine"> <path class="line" v-for="(path, key) in line" :key="`${key}${path.d}`" fill="none" :stroke="path.color" :d="path.d"></path> </transition-group>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| .growLine-enter-active { transition: all 2s; stroke-dashoffset: 0; } .growLine-enter { stroke-dashoffset: 3000; }
.chartContain { ... .chart { ... .chartWrap { ... .line { stroke-dasharray: 3000; } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| computed: { ... color() { return d3.scaleOrdinal(d3.schemeCategory10); }, line() { let line = d3 .line() .x((d, i) => { return this.xScale(i + 1); }) .y(d => { return this.yScale(d); }); let pathArray = [];
this.dataArray.forEach((e, i) => { pathArray.push({d: line(e), color: this.color(i)}); });
return pathArray; }, }
|
繪製點
注意: showTooltip
, hiddenTooltip
是拿來做滑過物件顯示數據資料的 tooltip
功能,在下一段會提到,這邊先純粹解釋繪製折點的部分。
1 2 3 4 5 6
| <g class="dot" v-for="(group, key) in dot" :key="key"> <transition-group tag="g" name="growDot"> <circle v-for="(c, k) in group.circle" :key="`${k}${c.cx}${c.cy}`" :cx="c.cx" :cy="c.cy" r="5" :fill="group.color" stroke="white" v-on:mouseover="showTooltip(key, k, $event)" v-on:mouseout="hiddenTooltip"></circle> </transition-group> </g>
|
1 2 3 4 5 6 7 8 9
| .growDot-enter-active { transition: all 1s; } .growDot-enter { opacity: 0; transform: scale(0); transform-origin: 50%; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| computed: { dot() { let circleArray = [];
this.dataArray.forEach((group, i) => { let circleGroup = [];
group.forEach((number, index) => { circleGroup.push({ cx: this.xScale(index + 1), cy: this.yScale(number), }); });
circleArray.push({ circle: circleGroup, color: this.color(i) }); });
return circleArray; }, }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ... .tooltip { min-width: 100px; background-color: rgba(0, 0, 0, 0.9); color: white; padding: 10px; border-radius: 6px; position: absolute; text-align: left; line-height: 1.5em; z-index: 1; &.hidden { visibility: hidden; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| methods: { showTooltip(index1, index2, event) { let mouseX = event.clientX + 20; let mouseY = event.clientY;
document.querySelector('.tooltip').setAttribute('style', `left: ${mouseX}px; top: ${mouseY}px;`); document.querySelector('.tooltip .name').innerHTML = `${this.data[index1].name} / ${this.data[index1].value[index2].month}`; document.querySelector('.tooltip .value').innerHTML = `${this.data[index1].value[index2].number} 件`;
this.hideTooltip = false; }, hiddenTooltip() { this.hideTooltip = true; } }
|
全部畫好的樣子如下圖,可以去 Github 查看完整程式碼喔!也可以在這裡查看折線圖的 Demo,我們下回見~
參考資料