chart
Gitea Actions Demo / Deploy (push) Successful in 24s

This commit is contained in:
vadim committed 2026-10-06 23:26:38 +03:00
1 parent fcc438e5fe
commit 72b9d0d407
2 files changed
+152 -4

No files matched your search

+143
View File
@@ -0,0 +1,143 @@
<script setup lang="ts">
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useTheme } from 'vuetify'
import { ArcElement, Chart as ChartJS, type ChartData, type ChartOptions, Tooltip } from 'chart.js'
import { Doughnut } from 'vue-chartjs'
import { useStatusStore } from '@/stores/status.ts'
import type { IssueStat } from '@/api/issue.api.ts'
ChartJS.register(ArcElement, Tooltip)
// Statuses whose color is not a theme color (e.g. "default") fall back to a neutral gray
const FALLBACK_COLOR = '#9E9E9E'
interface Props {
stats?: IssueStat | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
select: [status: string]
}>()
const theme = useTheme()
const statusStore = useStatusStore()
function themeColor(name: string, fallback = FALLBACK_COLOR): string {
const color = theme.current.value.colors[name]
return typeof color === 'string' ? color : fallback
}
const { statuses } = storeToRefs(statusStore)
// Keep the status order from the API so a status always lands on the same segment
const items = computed(() =>
(statuses.value ?? []).map((status) => ({
alias: status.alias,
label: status.name,
count: props.stats?.byStatus.find((el) => el.status === status.alias)?.count ?? 0,
color: themeColor(status.color),
})),
)
const total = computed(() => props.stats?.total ?? 0)
const chartData = computed<ChartData<'doughnut'>>(() => ({
labels: items.value.map((el) => el.label),
datasets: [
{
data: items.value.map((el) => el.count),
backgroundColor: items.value.map((el) => el.color),
// 2px surface-colored gap between segments
borderColor: themeColor('surface', '#FFFFFF'),
borderWidth: 2,
hoverOffset: 4,
},
],
}))
const chartOptions = computed<ChartOptions<'doughnut'>>(() => ({
responsive: true,
maintainAspectRatio: false,
cutout: '72%',
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (ctx) => ` ${ctx.parsed} (${Math.round((ctx.parsed / (total.value || 1)) * 100)}%)`,
},
},
},
onHover: (event, elements) => {
const target = event.native?.target as HTMLElement | undefined
if (target) target.style.cursor = elements.length ? 'pointer' : 'default'
},
onClick: (_event, elements) => {
const item = elements[0] && items.value[elements[0].index]
if (item) emit('select', item.alias)
},
}))
</script>
<template>
<div class="status-chart">
<div class="status-chart__canvas">
<doughnut :data="chartData" :options="chartOptions" />
<div class="status-chart__total">
<div class="text-headline-medium">{{ total }}</div>
<div class="text-label-medium">issues</div>
</div>
</div>
<ul class="d-flex flex-column ga-2">
<li v-for="item in items" :key="item.alias" class="status-chart__legend-item" @click="emit('select', item.alias)">
<span class="status-chart__swatch" :style="{ background: item.color }" />
<span>{{ item.label }}</span>
<span class="text-label-medium">{{ item.count }}</span>
</li>
</ul>
</div>
</template>
<style scoped lang="scss">
.status-chart {
display: flex;
align-items: center;
gap: 1.5rem;
&__canvas {
position: relative;
width: 180px;
height: 180px;
flex-shrink: 0;
}
// Overlay sits on top of the canvas but must not swallow hover/click on the segments
&__total {
position: absolute;
z-index: -1;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
pointer-events: none;
line-height: 1.1;
}
&__legend-item {
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
column-gap: 0.5rem;
cursor: pointer;
}
&__swatch {
width: 10px;
height: 10px;
border-radius: 2px;
}
}
</style>
+9 -4
View File
@@ -11,6 +11,7 @@ import type { Project } from '@/api/project.api.ts'
import DashboardCountCard from '@/components/DashboardCountCard.vue' import DashboardCountCard from '@/components/DashboardCountCard.vue'
import LabelText from '@/components/LabelText.vue' import LabelText from '@/components/LabelText.vue'
import ProjectMenu from '@/components/ProjectMenu.vue' import ProjectMenu from '@/components/ProjectMenu.vue'
import IssueStatusChart from '@/components/IssueStatusChart.vue'
const ACTIVE_STATUSES: Issue['status'][] = ['OPEN', 'IN_PROGRESS'] const ACTIVE_STATUSES: Issue['status'][] = ['OPEN', 'IN_PROGRESS']
@@ -210,13 +211,17 @@ function onClickCard(status?: string) {
<v-row density="compact"> <v-row density="compact">
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-card border> <v-card border>
<v-card-text>text</v-card-text> <v-card-title>Issues by status</v-card-title>
<v-divider />
<v-card-text>
<issue-status-chart :stats="stats" @select="onClickCard" />
</v-card-text>
</v-card> </v-card>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-card border> <!-- <v-card border>-->
<v-card-text>text</v-card-text> <!-- <v-card-text>text</v-card-text>-->
</v-card> <!-- </v-card>-->
</v-col> </v-col>
</v-row> </v-row>
</div> </div>