效果图:
image.png
<template>
<div class="db-root">
<div class="db-body">
<div
v-for="point of dataSource"
:key="point.id"
:style="{ top: `${point.y}px`, left: `${point.x}px`, background: point.color, ...getStyle }"
:class="['circle', { 'noisy': point.noisy }]"
/>
</div>
<label>
扫描半径:
<input type="number" v-model="params.eps" />
</label>
<label>
最小包含点数:
<input type="number" v-model="params.minPts" />
</label>
<button @click="handleStartCalculate" :disabled="disabled">开始计算</button>
<button @click="resetColor">重置颜色</button>
<button @click="resetDataSource">重置点分布</button>
</div>
</template>
<script>
import { random, map, isEmpty, filter, forEach, isEqual, some, find } from 'lodash'
import { v4 as uuid } from 'uuid'
const COLORS = [
'#297aff',
'#ff9800',
'#30af28',
'#ffcc0d',
'#00cccc',
'#66ccff',
'#01a5ed',
'#009966',
'#A0D911',
]
const DEFAULT_COLOR = '#000000'
export default {
name: 'DBSCAN',
data() {
return {
dataSource: [],
params: {
minPts: 2, // 最小包含点数
eps: 30, // 半径
},
disabled: false,
}
},
created() {
this.resetDataSource()
},
computed: {
getStyle() {
const { params: { eps } } = this
const unit = `${eps}px`
return {
borderTopLeftRadius: unit,
borderTopRightRadius: unit,
borderBottomLeftRadius: unit,
borderBottomRightRadius: unit,
width: `${eps * 2}px`,
height: `${eps * 2}px`,
position: 'absolute',
opacity: 0.3
}
},
},
methods: {
// 重置点的颜色和状态
resetColor() {
map(this.dataSource, point => {
point.color = DEFAULT_COLOR
point.visited = false
point.noisy = false
})
this.disabled = false
},
// 重新生成随机样本
resetDataSource() {
const eps = parseInt(this.params.eps)
this.dataSource = map(new Array(400), () => (
{
id: uuid(),
x: random(0, 1600 - eps * 2, false),
y: random(0, 600 - eps * 2, false),
color: DEFAULT_COLOR,
visited: false,
noisy: false,
}
))
this.disabled = false
},
/**
* 判断第二个点是否在该点半径范围内
* @param center
* @param x
* @param y
*/
isInEps(center, { x, y }) {
// 利用勾股定理判断点是否在点的半径范围中
const eps = parseInt(this.params.eps)
const xOffset = Math.abs(x - center.x)
const yOffset = Math.abs(y - center.y)
const distance = Math.sqrt(xOffset ** 2 + yOffset ** 2)
return distance <= eps
},
async handleStartCalculate() {
const { dataSource, isInEps } = this
const minPts = parseInt(this.params.minPts)
let colorIndex = 0
// 重复查找没有访问过的点
while (some(dataSource, point => !point.visited)) {
const visitedPoints = new Map()
// 存放点的栈(广度优先遍历)
const pointsStack = []
const core = find(dataSource, point => !point.visited)
if (core) {
const pointsInEps = filter(dataSource, p => !isEqual(core.id, p.id) && isInEps(core, p))
// 如果是核心点,将其放入栈中
if (pointsInEps.length >= minPts) {
pointsStack.push(core)
while (!isEmpty(pointsStack)) {
// 弹出最后一个元素,将其被标记为已访问过
const point = pointsStack.pop()
point.visited = true
visitedPoints.set(point.id, true)
const stackMap = new Map()
forEach(pointsStack, p => stackMap.set(p.id, true))
// 寻找半径范围内的其他点,将其染色
const pointsInEps = filter(dataSource, p => !isEqual(point.id, p.id) && isInEps(point, p) && !visitedPoints.has(p.id) && !stackMap.has(p.id))
// 如果是核心点,说明该点颜色还是默认的
if (isEqual(point.color, DEFAULT_COLOR)) {
point.color = COLORS[colorIndex % COLORS.length]
forEach(pointsInEps, p => p.color = point.color)
colorIndex++
} else {
// 如果不是核心点,直接将该点所有密度相连的点染色
forEach(pointsInEps, p => p.color = point.color)
}
// 染色过后放进栈中
pointsStack.push(...pointsInEps)
}
} else {
// 该点可能是边界点或者噪点
core.visited = true
}
}
}
this.disabled = true
},
},
}
</script>
<style scoped>
.db-root {
width: 100%;
height: 100%;
}
.db-body {
margin: 0 auto;
width: 1600px;
height: 600px;
position: relative;
border: 1px solid grey;
}
</style>
网友评论