哇!開始用SVG畫圖了~越來越實用了~
設計一些實用的可縮放向量圖形~
應該可以做一些簡單的動畫吧!
這次有四個檔案,App.vue、AxisLabel.vue、PolyGraph.vue、util.js
App.vue
<!--
An SVG graph
-->
<script setup>
import PolyGraph from './PolyGraph.vue'
import { ref, reactive } from 'vue'
const newLabel = ref('')
const stats = reactive([
{ label: 'A', value: 100 },
{ label: 'B', value: 100 },
{ label: 'C', value: 100 },
{ label: 'D', value: 100 },
{ label: 'E', value: 100 },
{ label: 'F', value: 100 }
])
function add(e) {
e.preventDefault()
if (!newLabel.value) return
stats.push({
label: newLabel.value,
value: 100
})
newLabel.value = ''
}
function remove(stat) {
if (stats.length > 3) {
stats.splice(stats.indexOf(stat), 1)
} else {
alert("Can't delete more!")
}
}
</script>
<template>
<svg width="200" height="200">
<PolyGraph :stats="stats"></PolyGraph>
</svg>
<!-- controls -->
<div v-for="stat in stats">
<label>{{stat.label}}</label>
<input type="range" v-model="stat.value" min="0" max="100">
<span>{{stat.value}}</span>
<button @click="remove(stat)" class="remove">X</button>
</div>
<form id="add">
<input name="newlabel" v-model="newLabel">
<button @click="add">Add a Stat</button>
</form>
<pre id="raw">{{ stats }}</pre>
</template>
<style>
polygon {
fill: #42b983;
opacity: 0.75;
}
circle {
fill: transparent;
stroke: #999;
}
text {
font-size: 10px;
fill: #666;
}
label {
display: inline-block;
margin-left: 10px;
width: 20px;
}
#raw {
position: absolute;
top: 0;
left: 300px;
}
</style>
<script setup>
import PolyGraph from './PolyGraph.vue'
import { ref, reactive } from 'vue'
PolyGraph
是我們將用於繪製多邊形圖的子組件。ref
和reactive
是 Vue 3 的響應性 API,用來創建響應性數據。
const newLabel = ref('')
const stats = reactive([
{ label: 'A', value: 100 },
{ label: 'B', value: 100 },
{ label: 'C', value: 100 },
{ label: 'D', value: 100 },
{ label: 'E', value: 100 },
{ label: 'F', value: 100 }
])
newLabel
是一個響應性引用,用來儲存新添加的標籤。stats
是一個響應性物件,用來儲存每個標籤及其對應的值。
function add(e) {
e.preventDefault()
if (!newLabel.value) return
stats.push({
label: newLabel.value,
value: 100
})
newLabel.value = ''
}
add
函數用來添加新的標籤到stats
中。e.preventDefault()
用來防止表單的默認提交行為。- 如果
newLabel
不是空的,就將其添加到stats
中,並重置newLabel
。
function remove(stat) {
if (stats.length > 3) {
stats.splice(stats.indexOf(stat), 1)
} else {
alert("Can't delete more!")
}
}
remove
函數用來從stats
中移除標籤。- 當
stats
長度超過 3 時,允許移除;否則,顯示警告信息。
<template>
<svg width="200" height="200">
<PolyGraph :stats="stats"></PolyGraph>
</svg>
- 使用
svg
元素來顯示多邊形圖。 PolyGraph
組件接收stats
作為道具,繪製圖表。
<div v-for="stat in stats">
<label>{{stat.label}}</label>
<input type="range" v-model="stat.value" min="0" max="100">
<span>{{stat.value}}</span>
<button @click="remove(stat)" class="remove">X</button>
</div>
- 使用
v-for
遍歷stats
,顯示每個標籤及其對應的控制項。 input
元素用來調整標籤的值。button
用來移除標籤。
<form id="add">
<input name="newlabel" v-model="newLabel">
<button @click="add">Add a Stat</button>
</form>
- 表單用來添加新標籤。
input
用來輸入新標籤的名稱,綁定到newLabel
。button
用來觸發add
函數,添加新標籤。
<pre id="raw">{{ stats }}</pre>
- 使用
<pre>
元素顯示stats
的原始數據,以便於調試。
<style>
<style scoped>
polygon {
fill: #42b983;
opacity: 0.75;
}
circle {
fill: transparent;
stroke: #999;
}
text {
font-size: 10px;
fill: #666;
}
label {
display: inline-block;
margin-left: 10px;
width: 20px;
}
#raw {
position: absolute;
top: 0;
left: 300px;
}
</style>
polygon
樣式用來設定多邊形的顏色和透明度。circle
樣式用來設定圓形的填充和邊框。text
樣式用來設定文本的字體大小和顏色。label
樣式用來設定標籤的顯示方式。#raw
樣式用來設定顯示原始數據的位置和樣式。
AxisLabel.vue
<script setup>
import { computed } from 'vue'
import { valueToPoint } from './util.js'
const props = defineProps({
stat: Object,
index: Number,
total: Number
})
const point = computed(() =>
valueToPoint(+props.stat.value + 10, props.index, props.total)
)
</script>
<template>
<text :x="point.x" :y="point.y">{{stat.label}}</text>
</template>
<script setup>
import { computed } from 'vue'
import { valueToPoint } from './util.js'
- 從 Vue 中引入
computed
函數,用來創建計算屬性。 - 從
util.js
文件中引入valueToPoint
函數,用來將值轉換為座標點。
const props = defineProps({
stat: Object,
index: Number,
total: Number
})
使用 defineProps
定義組件的 props
,包括:
stat
:一個物件,表示統計數據。index
:一個數字,表示當前統計數據在列表中的索引。total
:一個數字,表示統計數據的總數。
const point = computed(() =>
valueToPoint(+props.stat.value + 10, props.index, props.total)
)
- 使用
computed
函數創建計算屬性point
。 valueToPoint
函數接收三個參數:+props.stat.value + 10
:將統計數據的值轉換為數字並加上 10。props.index
:當前統計數據的索引。props.total
:統計數據的總數。
point
是一個響應性物件,包含計算出來的x
和y
座標。
<template>
<template>
<text :x="point.x" :y="point.y">{{props.stat.label}}</text>
</template>
- 使用
<text>
元素在 SVG 中顯示文本。 :x="point.x"
:綁定x
屬性到計算屬性point
的x
值。:y="point.y"
:綁定y
屬性到計算屬性point
的y
值。{{props.stat.label}}
:顯示props.stat
中的標籤文本。
PolyGraph.vue
<script setup>
import AxisLabel from './AxisLabel.vue'
import { computed } from 'vue'
import { valueToPoint } from './util.js'
const props = defineProps({
stats: Array
})
const points = computed(() => {
const total = props.stats.length
return props.stats
.map((stat, i) => {
const { x, y } = valueToPoint(stat.value, i, total)
return `${x},${y}`
})
.join(' ')
})
</script>
<template>
<g>
<polygon :points="points"></polygon>
<circle cx="100" cy="100" r="80"></circle>
<axis-label
v-for="(stat, index) in stats"
:stat="stat"
:index="index"
:total="stats.length"
>
</axis-label>
</g>
</template>
<style>
polygon {
fill: #42b983;
opacity: 0.75;
}
circle {
fill: transparent;
stroke: #999;
}
</style>
<script setup>
import AxisLabel from './AxisLabel.vue'
import { computed } from 'vue'
import { valueToPoint } from './util.js'
- 從當前目錄中的
AxisLabel.vue
文件中引入AxisLabel
組件。 - 從 Vue 中引入
computed
函數,用來創建計算屬性。 - 從當前目錄中的
util.js
文件中引入valueToPoint
函數,用來將值轉換為座標點。
const props = defineProps({使用
stats: Array
})
defineProps
定義組件的 props
,包括:stats
:一個陣列,表示統計數據。
const points = computed(() => {
const total = props.stats.length
return props.stats
.map((stat, i) => {
const { x, y } = valueToPoint(stat.value, i, total)
return `${x},${y}`
})
.join(' ')
})
- 使用
computed
函數創建計算屬性points
。 total
變數計算stats
陣列的長度,表示統計數據的總數。props.stats.map
遍歷stats
陣列,對每個stat
和其索引i
進行以下操作:- 使用
valueToPoint
函數計算出stat.value
對應的x
和y
座標。 - 返回格式為
${x},${y}
的字串。
- 使用
join(' ')
方法將所有座標字串用空格連接起來,形成 SVGpolygon
元素需要的points
屬性值。
<template>
<template>
<g>
<polygon :points="points"></polygon>
<circle cx="100" cy="100" r="80"></circle>
<axis-label
v-for="(stat, index) in stats"
:stat="stat"
:index="index"
:total="stats.length"
>
</axis-label>
</g>
</template>
SVG 元素群組 <g>
- 使用
<g>
標籤將 SVG 元素分組,這是一個容器元素,用來組織其他 SVG 元素。
多邊形 <polygon>
- 使用
<polygon>
元素繪製多邊形。 :points="points"
綁定points
計算屬性作為多邊形的頂點座標。
圓形 <circle>
- 使用
<circle>
元素繪製一個圓。 cx="100"
和cy="100"
設置圓心的 x 和 y 座標。r="80"
設置圓的半徑。
自定義標籤組件 <axis-label>
- 使用
<axis-label>
元素來顯示每個統計數據的標籤。 - 使用
v-for
指令遍歷stats
陣列,為每個stat
創建一個axis-label
元素。 :stat="stat"
綁定當前的stat
。:index="index"
綁定當前的索引。:total="stats.length"
綁定統計數據的總數。
<style>
polygon {
fill: #42b983;
opacity: 0.75;
}
circle {
fill: transparent;
stroke: #999;
}
</style>
style部分,我從App.vue搬到子組件裡頭,因為我App.vue使用style scoped的關係唷!為什麼會使用呢?就是因為全域的style又會影響到其他祖先組件的內容了
util.js
export function valueToPoint(value, index, total) {
const x = 0
const y = -value * 0.8
const angle = ((Math.PI * 2) / total) * index
const cos = Math.cos(angle)
const sin = Math.sin(angle)
const tx = x * cos - y * sin + 100
const ty = x * sin + y * cos + 100
return {
x: tx,
y: ty
}
}
這個函數 valueToPoint
用來將某個值轉換成在圓形坐標系中的點,常用於繪製雷達圖或其他基於極坐標的圖形。下面是逐行解釋:
export function valueToPoint
:定義並導出名為valueToPoint
的函數。value, index, total
:這個函數接受三個參數:value
:要轉換的值。index
:當前點在多邊形中的索引。total
:多邊形的頂點總數。
const x = 0
:定義一個x
變量並初始化為 0。這表示在極坐標系中,所有點的初始 x 坐標都在 y 軸上。const y = -value * 0.8
:定義一個y
變量並將其初始化為-value * 0.8
。這裡的value
是要轉換的值,乘以 -0.8 是為了縮放並翻轉 y 坐標,使得值越大,點越遠離圓心。const angle = ((Math.PI * 2) / total) * index
:計算當前點的角度。Math.PI * 2
:表示完整的圓周(360度)。/ total
:將圓周分成等份,每份的角度。* index
:將角度乘以當前點的索引,確定當前點在圓周上的位置。
const cos = Math.cos(angle)
:計算該角度的餘弦值,用於旋轉變換。const sin = Math.sin(angle)
:計算該角度的正弦值,用於旋轉變換。const tx = x * cos - y * sin + 100
:計算變換後的 x 坐標,並將其平移 100 個單位,使得點在畫布中心(100, 100)周圍旋轉。x * cos
:x 坐標旋轉後的新 x 值。- y * sin
:y 坐標旋轉後的新 x 值。+ 100
:將 x 坐標平移 100 個單位,使其位於畫布中心。
const ty = x * sin + y * cos + 100
:計算變換後的 y 坐標,並將其平移 100 個單位。x * sin
:x 坐標旋轉後的新 y 值。+ y * cos
:y 坐標旋轉後的新 y 值。+ 100
:將 y 坐標平移 100 個單位,使其位於畫布中心。
return { x: tx, y: ty }
:返回一個包含變換後 x 和 y 坐標的物件,表示在圓形坐標系中的點。

這題的CSS,我有改過一些~有興趣可以再看一下github摟
讓我想到這個範例可以改一改拿來當作獵人的念能力分佈圖
2024連載再開!希望能活著看到結局~看到庫拉皮卡下船www