mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-25 15:15:47 +00:00
Compare commits
1 Commits
cloud/1.43
...
fix/codera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7deb337683 |
57
src/components/ui/chart/ChartBar.stories.ts
Normal file
57
src/components/ui/chart/ChartBar.stories.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
import ChartBar from './ChartBar.vue'
|
||||||
|
|
||||||
|
const meta: Meta<typeof ChartBar> = {
|
||||||
|
title: 'Components/Chart/ChartBar',
|
||||||
|
component: ChartBar,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: { layout: 'centered' },
|
||||||
|
decorators: [
|
||||||
|
(story) => ({
|
||||||
|
components: { story },
|
||||||
|
template:
|
||||||
|
'<div class="w-[413px] bg-neutral-900 p-4 rounded-lg"><story /></div>'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
ariaLabel: 'Bar chart example',
|
||||||
|
data: {
|
||||||
|
labels: ['A', 'B', 'C', 'D'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'BarName1',
|
||||||
|
data: [10, 50, 35, 75],
|
||||||
|
backgroundColor: '#ff8000'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultipleDatasets: Story = {
|
||||||
|
args: {
|
||||||
|
ariaLabel: 'Bar chart with multiple datasets',
|
||||||
|
data: {
|
||||||
|
labels: ['A', 'B', 'C', 'D'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Series 1',
|
||||||
|
data: [30, 60, 45, 80],
|
||||||
|
backgroundColor: '#ff8000'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Series 2',
|
||||||
|
data: [50, 40, 70, 20],
|
||||||
|
backgroundColor: '#4ade80'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/components/ui/chart/ChartBar.vue
Normal file
34
src/components/ui/chart/ChartBar.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn('rounded-lg bg-component-node-widget-background p-6', props.class)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<canvas ref="canvasRef" :aria-label="ariaLabel" role="img" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChartData, ChartOptions } from 'chart.js'
|
||||||
|
import { computed, ref, toRef } from 'vue'
|
||||||
|
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
|
import { useChart } from './useChart'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: ChartData<'bar'>
|
||||||
|
options?: ChartOptions<'bar'>
|
||||||
|
ariaLabel: string
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
|
|
||||||
|
useChart(
|
||||||
|
canvasRef,
|
||||||
|
ref('bar'),
|
||||||
|
toRef(() => props.data),
|
||||||
|
computed(() => props.options as ChartOptions | undefined)
|
||||||
|
)
|
||||||
|
</script>
|
||||||
76
src/components/ui/chart/ChartLine.stories.ts
Normal file
76
src/components/ui/chart/ChartLine.stories.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
import ChartLine from './ChartLine.vue'
|
||||||
|
|
||||||
|
const meta: Meta<typeof ChartLine> = {
|
||||||
|
title: 'Components/Chart/ChartLine',
|
||||||
|
component: ChartLine,
|
||||||
|
tags: ['autodocs'],
|
||||||
|
parameters: { layout: 'centered' },
|
||||||
|
decorators: [
|
||||||
|
(story) => ({
|
||||||
|
components: { story },
|
||||||
|
template:
|
||||||
|
'<div class="w-[413px] bg-neutral-900 p-4 rounded-lg"><story /></div>'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
ariaLabel: 'Line chart example',
|
||||||
|
data: {
|
||||||
|
labels: ['A', 'B', 'C', 'D'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'LineName1',
|
||||||
|
data: [10, 45, 25, 80],
|
||||||
|
borderColor: '#4ade80',
|
||||||
|
borderDash: [5, 5],
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: '#4ade8033',
|
||||||
|
tension: 0.4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultipleLines: Story = {
|
||||||
|
args: {
|
||||||
|
ariaLabel: 'Line chart with multiple lines',
|
||||||
|
data: {
|
||||||
|
labels: ['A', 'B', 'C', 'D'],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'LineName1',
|
||||||
|
data: [10, 45, 25, 80],
|
||||||
|
borderColor: '#4ade80',
|
||||||
|
borderDash: [5, 5],
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: '#4ade8033',
|
||||||
|
tension: 0.4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'LineName2',
|
||||||
|
data: [80, 60, 40, 10],
|
||||||
|
borderColor: '#ff8000',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: '#ff800033',
|
||||||
|
tension: 0.4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'LineName3',
|
||||||
|
data: [60, 70, 35, 40],
|
||||||
|
borderColor: '#ef4444',
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: '#ef444433',
|
||||||
|
tension: 0.4
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/components/ui/chart/ChartLine.vue
Normal file
34
src/components/ui/chart/ChartLine.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn('rounded-lg bg-component-node-widget-background p-6', props.class)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<canvas ref="canvasRef" :aria-label="ariaLabel" role="img" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ChartData, ChartOptions } from 'chart.js'
|
||||||
|
import { computed, ref, toRef } from 'vue'
|
||||||
|
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
|
import { useChart } from './useChart'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: ChartData<'line'>
|
||||||
|
options?: ChartOptions<'line'>
|
||||||
|
ariaLabel: string
|
||||||
|
class?: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const canvasRef = ref<HTMLCanvasElement | null>(null)
|
||||||
|
|
||||||
|
useChart(
|
||||||
|
canvasRef,
|
||||||
|
ref('line'),
|
||||||
|
toRef(() => props.data),
|
||||||
|
computed(() => props.options as ChartOptions | undefined)
|
||||||
|
)
|
||||||
|
</script>
|
||||||
196
src/components/ui/chart/useChart.ts
Normal file
196
src/components/ui/chart/useChart.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import type { ChartData, ChartOptions, ChartType } from 'chart.js'
|
||||||
|
import {
|
||||||
|
BarController,
|
||||||
|
BarElement,
|
||||||
|
CategoryScale,
|
||||||
|
Chart,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
LinearScale,
|
||||||
|
LineController,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Tooltip
|
||||||
|
} from 'chart.js'
|
||||||
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
|
Chart.register(
|
||||||
|
BarController,
|
||||||
|
BarElement,
|
||||||
|
CategoryScale,
|
||||||
|
Filler,
|
||||||
|
Legend,
|
||||||
|
LinearScale,
|
||||||
|
LineController,
|
||||||
|
LineElement,
|
||||||
|
PointElement,
|
||||||
|
Tooltip
|
||||||
|
)
|
||||||
|
|
||||||
|
function getCssVar(name: string): string {
|
||||||
|
return getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue(name)
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultOptions(type: ChartType): ChartOptions {
|
||||||
|
const foreground = getCssVar('--color-base-foreground') || '#ffffff'
|
||||||
|
const muted = getCssVar('--color-muted-foreground') || '#8a8a8a'
|
||||||
|
|
||||||
|
return {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
align: 'start',
|
||||||
|
labels: {
|
||||||
|
color: foreground,
|
||||||
|
usePointStyle: true,
|
||||||
|
pointStyle: 'circle',
|
||||||
|
boxWidth: 8,
|
||||||
|
boxHeight: 8,
|
||||||
|
padding: 16,
|
||||||
|
font: { family: 'Inter', size: 11 },
|
||||||
|
generateLabels(chart) {
|
||||||
|
const datasets = chart.data.datasets
|
||||||
|
return datasets.map((dataset, i) => {
|
||||||
|
const color =
|
||||||
|
(dataset as { borderColor?: string }).borderColor ??
|
||||||
|
(dataset as { backgroundColor?: string }).backgroundColor ??
|
||||||
|
'#888'
|
||||||
|
return {
|
||||||
|
text: dataset.label ?? '',
|
||||||
|
fillStyle: color as string,
|
||||||
|
strokeStyle: color as string,
|
||||||
|
lineWidth: 0,
|
||||||
|
pointStyle: 'circle' as const,
|
||||||
|
hidden: !chart.isDatasetVisible(i),
|
||||||
|
datasetIndex: i
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
elements: {
|
||||||
|
point: {
|
||||||
|
radius: 0,
|
||||||
|
hoverRadius: 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
color: muted,
|
||||||
|
font: { family: 'Inter', size: 11 },
|
||||||
|
padding: 8
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: true,
|
||||||
|
color: muted + '33',
|
||||||
|
drawTicks: false
|
||||||
|
},
|
||||||
|
border: { display: true, color: muted }
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
color: muted,
|
||||||
|
font: { family: 'Inter', size: 11 },
|
||||||
|
padding: 4
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
display: false,
|
||||||
|
drawTicks: false
|
||||||
|
},
|
||||||
|
border: { display: true, color: muted }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...(type === 'bar' && {
|
||||||
|
datasets: {
|
||||||
|
bar: {
|
||||||
|
borderRadius: { topLeft: 4, topRight: 4 },
|
||||||
|
borderSkipped: false,
|
||||||
|
barPercentage: 0.6,
|
||||||
|
categoryPercentage: 0.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useChart(
|
||||||
|
canvasRef: Ref<HTMLCanvasElement | null>,
|
||||||
|
type: Ref<ChartType>,
|
||||||
|
data: Ref<ChartData>,
|
||||||
|
options?: Ref<ChartOptions | undefined>
|
||||||
|
) {
|
||||||
|
const chartInstance = ref<Chart | null>(null)
|
||||||
|
|
||||||
|
function createChart() {
|
||||||
|
if (!canvasRef.value) return
|
||||||
|
|
||||||
|
chartInstance.value?.destroy()
|
||||||
|
|
||||||
|
const defaults = getDefaultOptions(type.value)
|
||||||
|
const merged = options?.value
|
||||||
|
? deepMerge(defaults, options.value)
|
||||||
|
: defaults
|
||||||
|
|
||||||
|
chartInstance.value = new Chart(canvasRef.value, {
|
||||||
|
type: type.value,
|
||||||
|
data: data.value,
|
||||||
|
options: merged
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(createChart)
|
||||||
|
|
||||||
|
watch([type, data, options ?? ref(undefined)], () => {
|
||||||
|
if (chartInstance.value) {
|
||||||
|
chartInstance.value.data = data.value
|
||||||
|
chartInstance.value.options = options?.value
|
||||||
|
? deepMerge(getDefaultOptions(type.value), options.value)
|
||||||
|
: getDefaultOptions(type.value)
|
||||||
|
chartInstance.value.update()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
chartInstance.value?.destroy()
|
||||||
|
chartInstance.value = null
|
||||||
|
})
|
||||||
|
|
||||||
|
return { chartInstance }
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepMerge<T extends Record<string, unknown>>(
|
||||||
|
target: T,
|
||||||
|
source: Record<string, unknown>
|
||||||
|
): T {
|
||||||
|
const result = { ...target } as Record<string, unknown>
|
||||||
|
for (const key of Object.keys(source)) {
|
||||||
|
const srcVal = source[key]
|
||||||
|
const tgtVal = result[key]
|
||||||
|
if (
|
||||||
|
srcVal &&
|
||||||
|
typeof srcVal === 'object' &&
|
||||||
|
!Array.isArray(srcVal) &&
|
||||||
|
tgtVal &&
|
||||||
|
typeof tgtVal === 'object' &&
|
||||||
|
!Array.isArray(tgtVal)
|
||||||
|
) {
|
||||||
|
result[key] = deepMerge(
|
||||||
|
tgtVal as Record<string, unknown>,
|
||||||
|
srcVal as Record<string, unknown>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
result[key] = srcVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result as T
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user