<template>
<div class="pie-chart" @mousemove="updateTooltipPosition">
<svg :width="size" :height="size" viewBox="-1 -1 2 2">
<g>
<path
v-for="(slice, index) in slices"
:key="index"
:d="
generateArc(
slice.startAngle,
slice.endAngle,
highlightedIndex === index
)
"
:fill="effectiveColors[index % effectiveColors.length]"
@mouseover="highlightSlice(index)"
@mouseout="unhighlightSlice()"
@click="selectSlice(index)"
@mousemove="updateTooltipPosition"
/>
</g>
</svg>
<!-- Tooltip -->
<div v-if="tooltipVisible" class="tooltip" :style="tooltipStyle">
{{ hoveredLabel }}: {{ hoveredValue }}
</div>
</div>
</template>
<script setup>
import { computed, ref } from "vue";
const props = defineProps({
data: {
type: Array,
required: true,
// data是一个对象数组,每个对象有'value'和'label'键
},
size: {
type: Number,
default: 200,
},
colors: {
type: Array,
default: () => ["#ff6384", "#36a2eb", "#cc65fe", "#ffce56", "#4bc0c0"], // 默认颜色数组
},
normalRadius: {
type: Number,
default: 0.85, // 默认半径 (0 to 1)
},
hoverRadius: {
type: Number,
default: 0.9, // hover半径 (0 to 1)
},
});
const totalValue = computed(() =>
props.data.reduce((acc, cur) => acc + cur.value, 0)
);
const slices = computed(() => {
let accumulatedAngle = 0;
return props.data.map(({ value }, index, array) => {
const startAngle = accumulatedAngle;
const endAngle = accumulatedAngle + (value / totalValue.value) * 360;
const sliceAngles = { startAngle, endAngle };
if (index === array.length - 1 && Math.abs(endAngle - 360) > 1e-6) {
sliceAngles.endAngle = 360;
}
accumulatedAngle = endAngle;
return sliceAngles;
});
});
const effectiveColors = computed(() => props.colors);
const highlightedIndex = ref(null);
const tooltipVisible = ref(false);
const hoveredLabel = ref("");
const hoveredValue = ref("");
const tooltipPosition = ref({ x: 0, y: 0 });
function generateArc(startAngle, endAngle, isHighlighted = false) {
const r = isHighlighted ? props.hoverRadius : props.normalRadius;
const startRadians = ((startAngle - 90) * Math.PI) / 180.0;
const endRadians = ((endAngle - 90) * Math.PI) / 180.0;
const start = polarToCartesian(r, startRadians);
const end = polarToCartesian(r, endRadians);
const largeArcFlag = Math.abs(endAngle - startAngle) <= 180 ? "0" : "1";
return [
`M ${start.x},${start.y}`,
`A ${r},${r},0,${largeArcFlag},1,${end.x},${end.y}`,
`L 0,0`,
`Z`,
].join(" ");
}
function polarToCartesian(radius, angleInRadians) {
return {
x: Math.cos(angleInRadians) * radius,
y: Math.sin(angleInRadians) * radius,
};
}
function highlightSlice(index) {
highlightedIndex.value = index;
const { label, value } = props.data[index];
hoveredLabel.value = label;
hoveredValue.value = value;
tooltipVisible.value = true;
}
function unhighlightSlice() {
highlightedIndex.value = null;
tooltipVisible.value = false;
}
function selectSlice(index) {
console.log(`Selected slice: ${props.data[index].label}`);
}
function updateTooltipPosition(event) {
if (!tooltipVisible.value) return;
const svgRect = event.currentTarget.getBoundingClientRect();
tooltipPosition.value = {
x: event.clientX - svgRect.left - window.scrollX,
y: event.clientY - svgRect.top - window.scrollY,
};
}
const tooltipStyle = computed(() => ({
left: `${tooltipPosition.value.x}px`,
top: `${tooltipPosition.value.y}px`,
}));
</script>
<style scoped>
.pie-chart {
display: inline-block;
position: relative;
}
.tooltip {
position: absolute;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 5px;
border-radius: 3px;
pointer-events: none;
transform: translate(-50%, -50%);
}
path {
transition: fill 0.2s ease-out;
}
</style>
<PieChart :data="chartData" :size="300" />
import PieChart from './pieChart.vue';
const chartData = [
{ label: 'Red', value: 20 },
{ label: 'Blue', value: 300 },
{ label: 'Purple', value: 100 },
{ label: 'Yellow', value: 400 }
];