feat: introduce area, line, bar, donut chart

This commit is contained in:
zernonia 2023-11-13 15:10:45 +08:00
parent 60d22c8206
commit bfccb78a4f
46 changed files with 1576 additions and 1 deletions

View File

@ -128,6 +128,21 @@ export const docsConfig: DocsConfig = {
href: '/docs/charts/area', href: '/docs/charts/area',
items: [], items: [],
}, },
{
title: 'Line',
href: '/docs/charts/line',
items: [],
},
{
title: 'Bar',
href: '/docs/charts/bar',
items: [],
},
{
title: 'Donut',
href: '/docs/charts/donut',
items: [],
},
], ],
}, },
{ {

View File

@ -121,7 +121,7 @@ function constructFiles(componentName: string, style: Style, sources: Record<str
'package.json': { 'package.json': {
content: { content: {
name: `shadcn-vue-${componentName.toLowerCase().replace(/ /g, '-')}`, name: `shadcn-vue-${componentName.toLowerCase().replace(/ /g, '-')}`,
scripts: { start: `shadcn-vue add ${registryDependencies.join(' ')} -y && vite` }, scripts: { start: registryDependencies ? `shadcn-vue add ${registryDependencies.join(' ')} -y && vite` : 'vite' },
dependencies, dependencies,
devDependencies, devDependencies,
}, },

View File

@ -0,0 +1,16 @@
---
title: Area
description: Displays a callout for user attention.
---
<ComponentPreview name="AreaChartDemo" />
## Installation
```bash
npx shadcn-vue@latest add chart-area
```

View File

@ -0,0 +1,16 @@
---
title: Bar
description: Displays a callout for user attention.
---
<ComponentPreview name="BarChartDemo" />
## Installation
```bash
npx shadcn-vue@latest add chart-bar
```

View File

@ -0,0 +1,16 @@
---
title: Donut
description: Displays a callout for user attention.
---
<ComponentPreview name="DonutChartDemo" />
## Installation
```bash
npx shadcn-vue@latest add chart-donut
```

View File

@ -0,0 +1,16 @@
---
title: Line
description: Displays a callout for user attention.
---
<ComponentPreview name="LineChartDemo" />
## Installation
```bash
npx shadcn-vue@latest add chart-line
```

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import { AreaChart } from '@/lib/registry/default/ui/chart-area'
const data = [
{ name: 'Jan', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Feb', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Mar', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Apr', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'May', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jun', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jul', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
]
const categories = ['total', 'predicted']
</script>
<template>
<AreaChart :data="data" index="name" :categories="categories" />
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { BarChart } from '@/lib/registry/default/ui/chart-bar'
const data = [
{ name: 'Jan', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Feb', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Mar', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Apr', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'May', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jun', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jul', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
]
const categories = ['total', 'predicted']
</script>
<template>
<BarChart
:data="data"
index="name"
:categories="categories"
:y-formatter="(tick, i) => {
return typeof tick === 'number'
? `$ ${new Intl.NumberFormat('us').format(tick).toString()}`
: ''
}"
/>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { DonutChart } from '@/lib/registry/default/ui/chart-donut'
const data = [
{ name: 'Jan', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Feb', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Mar', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Apr', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'May', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jun', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jul', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
]
</script>
<template>
<DonutChart
index="name"
:category="'total'"
:data="data"
/>
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { LineChart } from '@/lib/registry/default/ui/chart-line'
const data = [
{ name: 'Jan', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Feb', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Mar', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Apr', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'May', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jun', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jul', total: Math.floor(Math.random() * 2000) + 500 },
]
const categories = ['total']
</script>
<template>
<LineChart
:data="data"
index="name"
:categories="categories"
:y-formatter="(tick, i) => {
return typeof tick === 'number'
? `$ ${new Intl.NumberFormat('us').format(tick).toString()}`
: ''
}"
/>
</template>

View File

@ -19,6 +19,7 @@ export const buttonVariants = cva(
}, },
size: { size: {
default: 'h-10 px-4 py-2', default: 'h-10 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-9 rounded-md px-3', sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8', lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10', icon: 'h-10 w-10',

View File

@ -0,0 +1,133 @@
<script setup lang="ts">
import type { BulletLegendItemInterface } from '@unovis/ts'
import { VisArea, VisAxis, VisLine, VisXYContainer } from '@unovis/vue'
import { Area, Axis, Line } from '@unovis/ts'
import { ref } from 'vue'
import { useMounted } from '@vueuse/core'
import { ChartCrosshair, ChartLegend, defaultColors } from '@/lib/registry/new-york/ui/chart'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<{
data: any[]
categories: string[]
index: string
colors?: string[]
filterOpacity?: number
xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
showXAxis?: boolean
showYAxis?: boolean
showTooltip?: boolean
showLegend?: boolean
showGridLine?: boolean
showGradiant?: boolean
}>(), {
colors: () => defaultColors,
filterOpacity: 0.2,
showXAxis: true,
showYAxis: true,
showTooltip: true,
showLegend: true,
showGridLine: true,
showGradiant: true,
})
type Data = typeof props.data[number]
const legendItems = ref<BulletLegendItemInterface[]>(props.categories.map((category, i) => ({
name: category,
color: props.colors[i],
inactive: false,
})))
const isMounted = useMounted()
function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
// do something when clicked on legend
}
</script>
<template>
<div :class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')">
<ChartLegend v-if="showLegend" v-model:items="legendItems" @legend-item-click="handleLegendItemClick" />
<VisXYContainer :style="{ height: isMounted ? '100%' : 'auto' }" :margin="{ left: 20, right: 20 }" :data="data">
<svg width="0" height="0">
<defs>
<linearGradient v-for="(color, i) in colors" :id="`color-${i}`" :key="i" x1="0" y1="0" x2="0" y2="1">
<template v-if="showGradiant">
<stop offset="5%" :stop-color="color" stop-opacity="0.4" />
<stop offset="95%" :stop-color="color" stop-opacity="0" />
</template>
<template v-else>
<stop offset="0%" :stop-color="color" />
</template>
</linearGradient>
</defs>
</svg>
<ChartCrosshair v-if="showTooltip" :colors="colors" :items="legendItems" :index="index" />
<template v-for="(category, i) in categories" :key="category">
<VisArea
:x="(d: Data, i: number) => i"
:y="(d: Data) => d[category]"
color="auto"
:attributes="{
[Area.selectors.area]: {
fill: `url(#color-${i})`,
},
}"
:opacity="legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1"
/>
<VisLine
:x="(d: Data, i: number) => i"
:y="(d: Data) => d[category]"
:color="colors[i]"
:attributes="{
[Line.selectors.line]: {
opacity: legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1,
},
}"
/>
</template>
<VisAxis
v-if="showXAxis"
type="x"
:num-ticks="data.length"
:tick-format="xFormatter ?? ((v: number) => data[v]?.[index])"
:grid-line="false"
:tick-line="false"
tick-text-color="hsl(var(--muted-foreground))"
/>
<VisAxis
v-if="showYAxis"
type="y"
:num-ticks="data.length"
:tick-line="false"
:tick-format="yFormatter"
:domain-line="false"
:grid-line="showGridLine"
:attributes="{
[Axis.selectors.grid]: {
class: 'text-muted',
},
}"
tick-text-color="hsl(var(--muted-foreground))"
/>
</VisXYContainer>
</div>
</template>
<style>
:root {
--vis-tooltip-background-color: none;
--vis-tooltip-border-color: none;
--vis-tooltip-text-color: none;
--vis-tooltip-shadow-color: none;
--vis-tooltip-backdrop-filter: none;
--vis-tooltip-padding: none;
}
</style>

View File

@ -0,0 +1 @@
export { default as AreaChart } from './AreaChart.vue'

View File

@ -0,0 +1,113 @@
<script setup lang="ts">
import type { BulletLegendItemInterface } from '@unovis/ts'
import { VisAxis, VisGroupedBar, VisStackedBar, VisXYContainer } from '@unovis/vue'
import { Axis, GroupedBar, StackedBar } from '@unovis/ts'
import { computed, ref } from 'vue'
import { useMounted } from '@vueuse/core'
import { ChartCrosshair, ChartLegend, defaultColors } from '@/lib/registry/new-york/ui/chart'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<{
data: any[]
categories: string[]
index: string
colors?: string[]
filterOpacity?: number
type?: 'stacked' | 'grouped'
xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
showXAxis?: boolean
showYAxis?: boolean
showTooltip?: boolean
showLegend?: boolean
showGridLine?: boolean
}>(), {
colors: () => defaultColors,
type: 'grouped',
filterOpacity: 0.2,
showXAxis: true,
showYAxis: true,
showTooltip: true,
showLegend: true,
showGridLine: true,
})
type Data = typeof props.data[number]
const legendItems = ref<BulletLegendItemInterface[]>(props.categories.map((category, i) => ({
name: category,
color: props.colors[i],
inactive: false,
})))
const isMounted = useMounted()
function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
// do something when clicked on legend
}
const VisBarComponent = computed(() => props.type === 'grouped' ? VisGroupedBar : VisStackedBar)
const selectorsBar = computed(() => props.type === 'grouped' ? GroupedBar.selectors.bar : StackedBar.selectors.bar)
</script>
<template>
<div :class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')">
<ChartLegend v-if="showLegend" v-model:items="legendItems" @legend-item-click="handleLegendItemClick" />
<VisXYContainer :style="{ height: isMounted ? '100%' : 'auto' }" :margin="{ left: 20, right: 20 }" :data="data">
<ChartCrosshair v-if="showTooltip" :colors="colors" :items="legendItems" :index="index" />
<VisBarComponent
:x="(d: Data, i: number) => i"
:y="categories.map(category => (d: Data) => d[category]) "
:color="colors"
:rounded-corners="4"
:bar-padding="0.1"
:attributes="{
[selectorsBar]: {
opacity: (d: Data, i:number) => {
const pos = i % categories.length
return legendItems[pos]?.inactive ? filterOpacity : 1
},
},
}"
/>
<VisAxis
v-if="showXAxis"
type="x"
:num-ticks="data.length"
:tick-format="xFormatter ?? ((v: number) => data[v]?.[index])"
:grid-line="false"
:tick-line="false"
tick-text-color="hsl(var(--muted-foreground))"
/>
<VisAxis
v-if="showYAxis"
type="y"
:num-ticks="data.length"
:tick-line="false"
:tick-format="yFormatter"
:domain-line="false"
:grid-line="showGridLine"
:attributes="{
[Axis.selectors.grid]: {
class: 'text-muted',
},
}"
tick-text-color="hsl(var(--muted-foreground))"
/>
</VisXYContainer>
</div>
</template>
<style>
:root {
--vis-tooltip-background-color: none;
--vis-tooltip-border-color: none;
--vis-tooltip-text-color: none;
--vis-tooltip-shadow-color: none;
--vis-tooltip-backdrop-filter: none;
--vis-tooltip-padding: none;
}
</style>

View File

@ -0,0 +1 @@
export { default as BarChart } from './BarChart.vue'

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import { VisDonut, VisSingleContainer } from '@unovis/vue'
import { Donut } from '@unovis/ts'
import { computed, ref } from 'vue'
import { useMounted } from '@vueuse/core'
import { ChartSingleTooltip, defaultColors } from '@/lib/registry/new-york/ui/chart'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<{
data: any[]
index: string
category: string
colors?: string[]
type?: 'donut' | 'pie'
filterOpacity?: number
valueFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
showTooltip?: boolean
showLegend?: boolean
}>(), {
colors: () => defaultColors,
type: 'donut',
filterOpacity: 0.2,
showTooltip: true,
showLegend: true,
})
type Data = typeof props.data[number]
const isMounted = useMounted()
const activeSegmentKey = ref<string>()
const legendItems = computed(() => props.data.map((item, i) => ({
name: item[props.index],
color: props.colors[i],
inactive: false,
})))
</script>
<template>
<div :class="cn('w-full h-48 flex flex-col items-end', $attrs.class ?? '')">
<VisSingleContainer :style="{ height: isMounted ? '100%' : 'auto' }" :margin="{ left: 20, right: 20 }" :data="data">
<ChartSingleTooltip :selector="Donut.selectors.segment" :index="category" :items="legendItems" />
<VisDonut
:value="(d: Data) => d[category]"
:sort-function="(a: Data, b: Data) => (a[category] - b[category])"
:color="colors"
:arc-width="type === 'donut' ? 20 : 0"
:show-background="false"
:events="{
[Donut.selectors.segment]: {
click: (d: any, ev: PointerEvent, i: number, elements: HTMLElement[]) => {
if (d?.data?.[index] === activeSegmentKey) {
activeSegmentKey = undefined
elements.forEach(el => el.style.opacity = '1')
}
else {
activeSegmentKey = d?.data?.[index]
elements.forEach(el => el.style.opacity = `${filterOpacity}`)
elements[i].style.opacity = '1'
}
},
},
}"
/>
</VisSingleContainer>
</div>
</template>
<style>
:root {
--vis-tooltip-background-color: none;
--vis-tooltip-border-color: none;
--vis-tooltip-text-color: none;
--vis-tooltip-shadow-color: none;
--vis-tooltip-backdrop-filter: none;
--vis-tooltip-padding: none;
}
</style>

View File

@ -0,0 +1 @@
export { default as DonutChart } from './DonutChart.vue'

View File

@ -0,0 +1,105 @@
<script setup lang="ts">
import type { BulletLegendItemInterface } from '@unovis/ts'
import { VisAxis, VisLine, VisXYContainer } from '@unovis/vue'
import { Axis, Line } from '@unovis/ts'
import { ref } from 'vue'
import { useMounted } from '@vueuse/core'
import { ChartCrosshair, ChartLegend, defaultColors } from '@/lib/registry/new-york/ui/chart'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<{
data: any[]
categories: string[]
index: string
colors?: string[]
filterOpacity?: number
xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
showXAxis?: boolean
showYAxis?: boolean
showTooltip?: boolean
showLegend?: boolean
showGridLine?: boolean
}>(), {
colors: () => defaultColors,
filterOpacity: 0.2,
showXAxis: true,
showYAxis: true,
showTooltip: true,
showLegend: true,
showGridLine: true,
})
type Data = typeof props.data[number]
const legendItems = ref<BulletLegendItemInterface[]>(props.categories.map((category, i) => ({
name: category,
color: props.colors[i],
inactive: false,
})))
const isMounted = useMounted()
function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
// do something when clicked on legend
}
</script>
<template>
<div :class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')">
<ChartLegend v-if="showLegend" v-model:items="legendItems" @legend-item-click="handleLegendItemClick" />
<VisXYContainer :style="{ height: isMounted ? '100%' : 'auto' }" :margin="{ left: 20, right: 20 }" :data="data">
<ChartCrosshair v-if="showTooltip" :colors="colors" :items="legendItems" :index="index" />
<template v-for="(category, i) in categories" :key="category">
<VisLine
:x="(d: Data, i: number) => i"
:y="(d: Data) => d[category]"
:color="colors[i]"
:attributes="{
[Line.selectors.line]: {
opacity: legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1,
},
}"
/>
</template>
<VisAxis
v-if="showXAxis"
type="x"
:num-ticks="data.length"
:tick-format="xFormatter ?? ((v: number) => data[v]?.[index])"
:grid-line="false"
:tick-line="false"
tick-text-color="hsl(var(--muted-foreground))"
/>
<VisAxis
v-if="showYAxis"
type="y"
:num-ticks="data.length"
:tick-line="false"
:tick-format="yFormatter"
:domain-line="false"
:grid-line="showGridLine"
:attributes="{
[Axis.selectors.grid]: {
class: 'text-muted',
},
}"
tick-text-color="hsl(var(--muted-foreground))"
/>
</VisXYContainer>
</div>
</template>
<style>
:root {
--vis-tooltip-background-color: none;
--vis-tooltip-border-color: none;
--vis-tooltip-text-color: none;
--vis-tooltip-shadow-color: none;
--vis-tooltip-backdrop-filter: none;
--vis-tooltip-padding: none;
}
</style>

View File

@ -0,0 +1 @@
export { default as LineChart } from './LineChart.vue'

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import { VisCrosshair, VisTooltip } from '@unovis/vue'
import type { BulletLegendItemInterface } from '@unovis/ts'
import { omit } from '@unovis/ts'
import { createApp } from 'vue'
import { ChartTooltip } from '@/lib/registry/default/ui/chart'
const props = withDefaults(defineProps<{
colors: string[]
index: string
items: BulletLegendItemInterface[]
}>(), {
colors: () => [],
})
// Use weakmap to store reference to each datapoint for Tooltip
const wm = new WeakMap()
function template(d: any, ...a: any) {
if (wm.has(d)) {
return wm.get(d)
}
else {
const componentDiv = document.createElement('div')
const omittedData = Object.entries(omit(d, [props.index])).map(([key, value]) => {
const legendReference = props.items.find(i => i.name === key)
return { ...legendReference, value }
})
createApp(ChartTooltip, { title: d[props.index], data: omittedData }).mount(componentDiv)
wm.set(d, componentDiv.innerHTML)
return componentDiv.innerHTML
}
}
</script>
<template>
<VisTooltip :horizontal-shift="20" :vertical-shift="20" />
<VisCrosshair :color="colors" :template="template" />
</template>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { VisBulletLegend } from '@unovis/vue'
import type { BulletLegendItemInterface } from '@unovis/ts'
import { BulletLegend } from '@unovis/ts'
import { nextTick, onMounted, ref } from 'vue'
import { buttonVariants } from '@/lib/registry/default/ui/button'
const props = withDefaults(defineProps<{ items: BulletLegendItemInterface[] }>(), {
items: () => [],
})
const emits = defineEmits<{
legendItemClick: [d: BulletLegendItemInterface, i: number]
'update:items': [payload: BulletLegendItemInterface[]]
}>()
const elRef = ref<HTMLElement>()
onMounted(() => {
const selector = `.${BulletLegend.selectors.item}`
nextTick(() => {
const elements = elRef.value?.querySelectorAll(selector)
const classes = buttonVariants({ variant: 'ghost', size: 'xs' }).split(' ')
elements?.forEach(el => el.classList.add(...classes, '!mr-2'))
})
})
function onLegendItemClick(d: BulletLegendItemInterface, i: number) {
emits('legendItemClick', d, i)
const isBulletActive = !props.items[i].inactive
const isFilterApplied = props.items.some(i => i.inactive)
if (isFilterApplied && isBulletActive) {
// reset filter
emits('update:items', props.items.map(item => ({ ...item, inactive: false })))
}
else {
// apply selection, set other item as inactive
emits('update:items', props.items.map(item => item.name === d.name ? ({ ...d, inactive: false }) : { ...item, inactive: true }))
}
}
</script>
<template>
<div ref="elRef" class="w-max">
<VisBulletLegend
:items="items"
:on-legend-item-click="onLegendItemClick"
/>
</div>
</template>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import { VisTooltip } from '@unovis/vue'
import type { BulletLegendItemInterface } from '@unovis/ts'
import { omit } from '@unovis/ts'
import { createApp } from 'vue'
import { ChartTooltip } from '@/lib/registry/default/ui/chart'
const props = withDefaults(defineProps<{
selector: string
index: string
items?: BulletLegendItemInterface[]
}>(), {})
// Use weakmap to store reference to each datapoint for Tooltip
const wm = new WeakMap()
function template(d: any, i: number, elements: (HTMLElement | SVGElement)[]) {
if (props.index in d) {
if (wm.has(d)) {
return wm.get(d)
}
else {
const componentDiv = document.createElement('div')
const omittedData = Object.entries(omit(d, [props.index])).map(([key, value]) => {
const legendReference = props.items?.find(i => i.name === key)
return { ...legendReference, value }
})
createApp(ChartTooltip, { title: d[props.index], data: omittedData }).mount(componentDiv)
wm.set(d, componentDiv.innerHTML)
return componentDiv.innerHTML
}
}
else {
const data = d.data
if (wm.has(data)) {
return wm.get(data)
}
else {
const style = getComputedStyle(elements[i])
const omittedData = [{ name: data.name, value: data[props.index], color: style.fill }]
const componentDiv = document.createElement('div')
createApp(ChartTooltip, { title: d[props.index], data: omittedData }).mount(componentDiv)
wm.set(d, componentDiv.innerHTML)
return componentDiv.innerHTML
}
}
}
</script>
<template>
<VisTooltip
:horizontal-shift="20" :vertical-shift="20" :triggers="{
[selector]: template,
}"
/>
</template>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { Card, CardContent, CardHeader, CardTitle } from '@/lib/registry/default/ui/card'
defineProps<{
title?: string
data: {
name: string
color: string
value: any
}[]
}>()
</script>
<template>
<Card class="text-sm">
<CardHeader v-if="title" class="p-3 border-b">
<CardTitle>
{{ title }}
</CardTitle>
</CardHeader>
<CardContent class="p-3 min-w-[180px] flex flex-col gap-1">
<div v-for="(item, key) in data" :key="key" class="flex justify-between">
<div class="flex items-center">
<span class="w-2.5 h-2.5 mr-2">
<svg width="100%" height="100%" viewBox="0 0 30 30">
<path
d=" M 15 15 m -14, 0 a 14,14 0 1,1 28,0 a 14,14 0 1,1 -28,0"
:stroke="item.color"
:fill="item.color"
stroke-width="1"
/>
</svg>
</span>
<span>{{ item.name }}</span>
</div>
<span class="font-semibold">{{ item.value }}</span>
</div>
</CardContent>
</Card>
</template>

View File

@ -0,0 +1,6 @@
export { default as ChartTooltip } from './ChartTooltip.vue'
export { default as ChartSingleTooltip } from './ChartSingleTooltip.vue'
export { default as ChartLegend } from './ChartLegend.vue'
export { default as ChartCrosshair } from './ChartCrosshair.vue'
export const defaultColors = ['hsl(var(--primary))', 'hsl(var(--muted))']

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import { AreaChart } from '@/lib/registry/new-york/ui/chart-area'
const data = [
{ name: 'Jan', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Feb', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Mar', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Apr', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'May', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jun', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jul', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
]
const categories = ['total', 'predicted']
</script>
<template>
<AreaChart :data="data" index="name" :categories="categories" />
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { BarChart } from '@/lib/registry/new-york/ui/chart-bar'
const data = [
{ name: 'Jan', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Feb', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Mar', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Apr', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'May', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jun', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jul', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
]
const categories = ['total', 'predicted']
</script>
<template>
<BarChart
:data="data"
index="name"
:categories="categories"
:y-formatter="(tick, i) => {
return typeof tick === 'number'
? `$ ${new Intl.NumberFormat('us').format(tick).toString()}`
: ''
}"
/>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { DonutChart } from '@/lib/registry/new-york/ui/chart-donut'
const data = [
{ name: 'Jan', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Feb', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Mar', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Apr', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'May', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jun', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jul', total: Math.floor(Math.random() * 2000) + 500, predicted: Math.floor(Math.random() * 2000) + 500 },
]
</script>
<template>
<DonutChart
index="name"
:category="'total'"
:data="data"
/>
</template>

View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import { LineChart } from '@/lib/registry/new-york/ui/chart-line'
const data = [
{ name: 'Jan', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Feb', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Mar', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Apr', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'May', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jun', total: Math.floor(Math.random() * 2000) + 500 },
{ name: 'Jul', total: Math.floor(Math.random() * 2000) + 500 },
]
const categories = ['total']
</script>
<template>
<LineChart
:data="data"
index="name"
:categories="categories"
:y-formatter="(tick, i) => {
return typeof tick === 'number'
? `$ ${new Intl.NumberFormat('us').format(tick).toString()}`
: ''
}"
/>
</template>

View File

@ -20,6 +20,7 @@ export const buttonVariants = cva(
}, },
size: { size: {
default: 'h-9 px-4 py-2', default: 'h-9 px-4 py-2',
xs: 'h-7 rounded px-2',
sm: 'h-8 rounded-md px-3 text-xs', sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8', lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9', icon: 'h-9 w-9',

View File

@ -0,0 +1,133 @@
<script setup lang="ts">
import type { BulletLegendItemInterface } from '@unovis/ts'
import { VisArea, VisAxis, VisLine, VisXYContainer } from '@unovis/vue'
import { Area, Axis, Line } from '@unovis/ts'
import { ref } from 'vue'
import { useMounted } from '@vueuse/core'
import { ChartCrosshair, ChartLegend, defaultColors } from '@/lib/registry/new-york/ui/chart'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<{
data: any[]
categories: string[]
index: string
colors?: string[]
filterOpacity?: number
xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
showXAxis?: boolean
showYAxis?: boolean
showTooltip?: boolean
showLegend?: boolean
showGridLine?: boolean
showGradiant?: boolean
}>(), {
colors: () => defaultColors,
filterOpacity: 0.2,
showXAxis: true,
showYAxis: true,
showTooltip: true,
showLegend: true,
showGridLine: true,
showGradiant: true,
})
type Data = typeof props.data[number]
const legendItems = ref<BulletLegendItemInterface[]>(props.categories.map((category, i) => ({
name: category,
color: props.colors[i],
inactive: false,
})))
const isMounted = useMounted()
function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
// do something when clicked on legend
}
</script>
<template>
<div :class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')">
<ChartLegend v-if="showLegend" v-model:items="legendItems" @legend-item-click="handleLegendItemClick" />
<VisXYContainer :style="{ height: isMounted ? '100%' : 'auto' }" :margin="{ left: 20, right: 20 }" :data="data">
<svg width="0" height="0">
<defs>
<linearGradient v-for="(color, i) in colors" :id="`color-${i}`" :key="i" x1="0" y1="0" x2="0" y2="1">
<template v-if="showGradiant">
<stop offset="5%" :stop-color="color" stop-opacity="0.4" />
<stop offset="95%" :stop-color="color" stop-opacity="0" />
</template>
<template v-else>
<stop offset="0%" :stop-color="color" />
</template>
</linearGradient>
</defs>
</svg>
<ChartCrosshair v-if="showTooltip" :colors="colors" :items="legendItems" :index="index" />
<template v-for="(category, i) in categories" :key="category">
<VisArea
:x="(d: Data, i: number) => i"
:y="(d: Data) => d[category]"
color="auto"
:attributes="{
[Area.selectors.area]: {
fill: `url(#color-${i})`,
},
}"
:opacity="legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1"
/>
<VisLine
:x="(d: Data, i: number) => i"
:y="(d: Data) => d[category]"
:color="colors[i]"
:attributes="{
[Line.selectors.line]: {
opacity: legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1,
},
}"
/>
</template>
<VisAxis
v-if="showXAxis"
type="x"
:num-ticks="data.length"
:tick-format="xFormatter ?? ((v: number) => data[v]?.[index])"
:grid-line="false"
:tick-line="false"
tick-text-color="hsl(var(--muted-foreground))"
/>
<VisAxis
v-if="showYAxis"
type="y"
:num-ticks="data.length"
:tick-line="false"
:tick-format="yFormatter"
:domain-line="false"
:grid-line="showGridLine"
:attributes="{
[Axis.selectors.grid]: {
class: 'text-muted',
},
}"
tick-text-color="hsl(var(--muted-foreground))"
/>
</VisXYContainer>
</div>
</template>
<style>
:root {
--vis-tooltip-background-color: none;
--vis-tooltip-border-color: none;
--vis-tooltip-text-color: none;
--vis-tooltip-shadow-color: none;
--vis-tooltip-backdrop-filter: none;
--vis-tooltip-padding: none;
}
</style>

View File

@ -0,0 +1 @@
export { default as AreaChart } from './AreaChart.vue'

View File

@ -0,0 +1,113 @@
<script setup lang="ts">
import type { BulletLegendItemInterface } from '@unovis/ts'
import { VisAxis, VisGroupedBar, VisStackedBar, VisXYContainer } from '@unovis/vue'
import { Axis, GroupedBar, StackedBar } from '@unovis/ts'
import { computed, ref } from 'vue'
import { useMounted } from '@vueuse/core'
import { ChartCrosshair, ChartLegend, defaultColors } from '@/lib/registry/new-york/ui/chart'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<{
data: any[]
categories: string[]
index: string
colors?: string[]
filterOpacity?: number
type?: 'stacked' | 'grouped'
xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
showXAxis?: boolean
showYAxis?: boolean
showTooltip?: boolean
showLegend?: boolean
showGridLine?: boolean
}>(), {
colors: () => defaultColors,
type: 'grouped',
filterOpacity: 0.2,
showXAxis: true,
showYAxis: true,
showTooltip: true,
showLegend: true,
showGridLine: true,
})
type Data = typeof props.data[number]
const legendItems = ref<BulletLegendItemInterface[]>(props.categories.map((category, i) => ({
name: category,
color: props.colors[i],
inactive: false,
})))
const isMounted = useMounted()
function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
// do something when clicked on legend
}
const VisBarComponent = computed(() => props.type === 'grouped' ? VisGroupedBar : VisStackedBar)
const selectorsBar = computed(() => props.type === 'grouped' ? GroupedBar.selectors.bar : StackedBar.selectors.bar)
</script>
<template>
<div :class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')">
<ChartLegend v-if="showLegend" v-model:items="legendItems" @legend-item-click="handleLegendItemClick" />
<VisXYContainer :style="{ height: isMounted ? '100%' : 'auto' }" :margin="{ left: 20, right: 20 }" :data="data">
<ChartCrosshair v-if="showTooltip" :colors="colors" :items="legendItems" :index="index" />
<VisBarComponent
:x="(d: Data, i: number) => i"
:y="categories.map(category => (d: Data) => d[category]) "
:color="colors"
:rounded-corners="4"
:bar-padding="0.1"
:attributes="{
[selectorsBar]: {
opacity: (d: Data, i:number) => {
const pos = i % categories.length
return legendItems[pos]?.inactive ? filterOpacity : 1
},
},
}"
/>
<VisAxis
v-if="showXAxis"
type="x"
:num-ticks="data.length"
:tick-format="xFormatter ?? ((v: number) => data[v]?.[index])"
:grid-line="false"
:tick-line="false"
tick-text-color="hsl(var(--muted-foreground))"
/>
<VisAxis
v-if="showYAxis"
type="y"
:num-ticks="data.length"
:tick-line="false"
:tick-format="yFormatter"
:domain-line="false"
:grid-line="showGridLine"
:attributes="{
[Axis.selectors.grid]: {
class: 'text-muted',
},
}"
tick-text-color="hsl(var(--muted-foreground))"
/>
</VisXYContainer>
</div>
</template>
<style>
:root {
--vis-tooltip-background-color: none;
--vis-tooltip-border-color: none;
--vis-tooltip-text-color: none;
--vis-tooltip-shadow-color: none;
--vis-tooltip-backdrop-filter: none;
--vis-tooltip-padding: none;
}
</style>

View File

@ -0,0 +1 @@
export { default as BarChart } from './BarChart.vue'

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import { VisDonut, VisSingleContainer } from '@unovis/vue'
import { Donut } from '@unovis/ts'
import { computed, ref } from 'vue'
import { useMounted } from '@vueuse/core'
import { ChartSingleTooltip, defaultColors } from '@/lib/registry/new-york/ui/chart'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<{
data: any[]
index: string
category: string
colors?: string[]
type?: 'donut' | 'pie'
filterOpacity?: number
valueFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
showTooltip?: boolean
showLegend?: boolean
}>(), {
colors: () => defaultColors,
type: 'donut',
filterOpacity: 0.2,
showTooltip: true,
showLegend: true,
})
type Data = typeof props.data[number]
const isMounted = useMounted()
const activeSegmentKey = ref<string>()
const legendItems = computed(() => props.data.map((item, i) => ({
name: item[props.index],
color: props.colors[i],
inactive: false,
})))
</script>
<template>
<div :class="cn('w-full h-48 flex flex-col items-end', $attrs.class ?? '')">
<VisSingleContainer :style="{ height: isMounted ? '100%' : 'auto' }" :margin="{ left: 20, right: 20 }" :data="data">
<ChartSingleTooltip :selector="Donut.selectors.segment" :index="category" :items="legendItems" />
<VisDonut
:value="(d: Data) => d[category]"
:sort-function="(a: Data, b: Data) => (a[category] - b[category])"
:color="colors"
:arc-width="type === 'donut' ? 20 : 0"
:show-background="false"
:events="{
[Donut.selectors.segment]: {
click: (d: any, ev: PointerEvent, i: number, elements: HTMLElement[]) => {
if (d?.data?.[index] === activeSegmentKey) {
activeSegmentKey = undefined
elements.forEach(el => el.style.opacity = '1')
}
else {
activeSegmentKey = d?.data?.[index]
elements.forEach(el => el.style.opacity = `${filterOpacity}`)
elements[i].style.opacity = '1'
}
},
},
}"
/>
</VisSingleContainer>
</div>
</template>
<style>
:root {
--vis-tooltip-background-color: none;
--vis-tooltip-border-color: none;
--vis-tooltip-text-color: none;
--vis-tooltip-shadow-color: none;
--vis-tooltip-backdrop-filter: none;
--vis-tooltip-padding: none;
}
</style>

View File

@ -0,0 +1 @@
export { default as DonutChart } from './DonutChart.vue'

View File

@ -0,0 +1,105 @@
<script setup lang="ts">
import type { BulletLegendItemInterface } from '@unovis/ts'
import { VisAxis, VisLine, VisXYContainer } from '@unovis/vue'
import { Axis, Line } from '@unovis/ts'
import { ref } from 'vue'
import { useMounted } from '@vueuse/core'
import { ChartCrosshair, ChartLegend, defaultColors } from '@/lib/registry/new-york/ui/chart'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<{
data: any[]
categories: string[]
index: string
colors?: string[]
filterOpacity?: number
xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
showXAxis?: boolean
showYAxis?: boolean
showTooltip?: boolean
showLegend?: boolean
showGridLine?: boolean
}>(), {
colors: () => defaultColors,
filterOpacity: 0.2,
showXAxis: true,
showYAxis: true,
showTooltip: true,
showLegend: true,
showGridLine: true,
})
type Data = typeof props.data[number]
const legendItems = ref<BulletLegendItemInterface[]>(props.categories.map((category, i) => ({
name: category,
color: props.colors[i],
inactive: false,
})))
const isMounted = useMounted()
function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
// do something when clicked on legend
}
</script>
<template>
<div :class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')">
<ChartLegend v-if="showLegend" v-model:items="legendItems" @legend-item-click="handleLegendItemClick" />
<VisXYContainer :style="{ height: isMounted ? '100%' : 'auto' }" :margin="{ left: 20, right: 20 }" :data="data">
<ChartCrosshair v-if="showTooltip" :colors="colors" :items="legendItems" :index="index" />
<template v-for="(category, i) in categories" :key="category">
<VisLine
:x="(d: Data, i: number) => i"
:y="(d: Data) => d[category]"
:color="colors[i]"
:attributes="{
[Line.selectors.line]: {
opacity: legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1,
},
}"
/>
</template>
<VisAxis
v-if="showXAxis"
type="x"
:num-ticks="data.length"
:tick-format="xFormatter ?? ((v: number) => data[v]?.[index])"
:grid-line="false"
:tick-line="false"
tick-text-color="hsl(var(--muted-foreground))"
/>
<VisAxis
v-if="showYAxis"
type="y"
:num-ticks="data.length"
:tick-line="false"
:tick-format="yFormatter"
:domain-line="false"
:grid-line="showGridLine"
:attributes="{
[Axis.selectors.grid]: {
class: 'text-muted',
},
}"
tick-text-color="hsl(var(--muted-foreground))"
/>
</VisXYContainer>
</div>
</template>
<style>
:root {
--vis-tooltip-background-color: none;
--vis-tooltip-border-color: none;
--vis-tooltip-text-color: none;
--vis-tooltip-shadow-color: none;
--vis-tooltip-backdrop-filter: none;
--vis-tooltip-padding: none;
}
</style>

View File

@ -0,0 +1 @@
export { default as LineChart } from './LineChart.vue'

View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import { VisCrosshair, VisTooltip } from '@unovis/vue'
import type { BulletLegendItemInterface } from '@unovis/ts'
import { omit } from '@unovis/ts'
import { createApp } from 'vue'
import { ChartTooltip } from '@/lib/registry/new-york/ui/chart'
const props = withDefaults(defineProps<{
colors: string[]
index: string
items: BulletLegendItemInterface[]
}>(), {
colors: () => [],
})
// Use weakmap to store reference to each datapoint for Tooltip
const wm = new WeakMap()
function template(d: any, ...a: any) {
if (wm.has(d)) {
return wm.get(d)
}
else {
const componentDiv = document.createElement('div')
const omittedData = Object.entries(omit(d, [props.index])).map(([key, value]) => {
const legendReference = props.items.find(i => i.name === key)
return { ...legendReference, value }
})
createApp(ChartTooltip, { title: d[props.index], data: omittedData }).mount(componentDiv)
wm.set(d, componentDiv.innerHTML)
return componentDiv.innerHTML
}
}
</script>
<template>
<VisTooltip :horizontal-shift="20" :vertical-shift="20" />
<VisCrosshair :color="colors" :template="template" />
</template>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { VisBulletLegend } from '@unovis/vue'
import type { BulletLegendItemInterface } from '@unovis/ts'
import { BulletLegend } from '@unovis/ts'
import { nextTick, onMounted, ref } from 'vue'
import { buttonVariants } from '@/lib/registry/new-york/ui/button'
const props = withDefaults(defineProps<{ items: BulletLegendItemInterface[] }>(), {
items: () => [],
})
const emits = defineEmits<{
legendItemClick: [d: BulletLegendItemInterface, i: number]
'update:items': [payload: BulletLegendItemInterface[]]
}>()
const elRef = ref<HTMLElement>()
onMounted(() => {
const selector = `.${BulletLegend.selectors.item}`
nextTick(() => {
const elements = elRef.value?.querySelectorAll(selector)
const classes = buttonVariants({ variant: 'ghost', size: 'xs' }).split(' ')
elements?.forEach(el => el.classList.add(...classes, '!mr-2'))
})
})
function onLegendItemClick(d: BulletLegendItemInterface, i: number) {
emits('legendItemClick', d, i)
const isBulletActive = !props.items[i].inactive
const isFilterApplied = props.items.some(i => i.inactive)
if (isFilterApplied && isBulletActive) {
// reset filter
emits('update:items', props.items.map(item => ({ ...item, inactive: false })))
}
else {
// apply selection, set other item as inactive
emits('update:items', props.items.map(item => item.name === d.name ? ({ ...d, inactive: false }) : { ...item, inactive: true }))
}
}
</script>
<template>
<div ref="elRef" class="w-max">
<VisBulletLegend
:items="items"
:on-legend-item-click="onLegendItemClick"
/>
</div>
</template>

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import { VisTooltip } from '@unovis/vue'
import type { BulletLegendItemInterface } from '@unovis/ts'
import { omit } from '@unovis/ts'
import { createApp } from 'vue'
import { ChartTooltip } from '@/lib/registry/new-york/ui/chart'
const props = withDefaults(defineProps<{
selector: string
index: string
items?: BulletLegendItemInterface[]
}>(), {})
// Use weakmap to store reference to each datapoint for Tooltip
const wm = new WeakMap()
function template(d: any, i: number, elements: (HTMLElement | SVGElement)[]) {
if (props.index in d) {
if (wm.has(d)) {
return wm.get(d)
}
else {
const componentDiv = document.createElement('div')
const omittedData = Object.entries(omit(d, [props.index])).map(([key, value]) => {
const legendReference = props.items?.find(i => i.name === key)
return { ...legendReference, value }
})
createApp(ChartTooltip, { title: d[props.index], data: omittedData }).mount(componentDiv)
wm.set(d, componentDiv.innerHTML)
return componentDiv.innerHTML
}
}
else {
const data = d.data
if (wm.has(data)) {
return wm.get(data)
}
else {
const style = getComputedStyle(elements[i])
const omittedData = [{ name: data.name, value: data[props.index], color: style.fill }]
const componentDiv = document.createElement('div')
createApp(ChartTooltip, { title: d[props.index], data: omittedData }).mount(componentDiv)
wm.set(d, componentDiv.innerHTML)
return componentDiv.innerHTML
}
}
}
</script>
<template>
<VisTooltip
:horizontal-shift="20" :vertical-shift="20" :triggers="{
[selector]: template,
}"
/>
</template>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { Card, CardContent, CardHeader, CardTitle } from '@/lib/registry/new-york/ui/card'
defineProps<{
title?: string
data: {
name: string
color: string
value: any
}[]
}>()
</script>
<template>
<Card class="text-sm">
<CardHeader v-if="title" class="p-3 border-b">
<CardTitle>
{{ title }}
</CardTitle>
</CardHeader>
<CardContent class="p-3 min-w-[180px] flex flex-col gap-1">
<div v-for="(item, key) in data" :key="key" class="flex justify-between">
<div class="flex items-center">
<span class="w-2.5 h-2.5 mr-2">
<svg width="100%" height="100%" viewBox="0 0 30 30">
<path
d=" M 15 15 m -14, 0 a 14,14 0 1,1 28,0 a 14,14 0 1,1 -28,0"
:stroke="item.color"
:fill="item.color"
stroke-width="1"
/>
</svg>
</span>
<span>{{ item.name }}</span>
</div>
<span class="font-semibold">{{ item.value }}</span>
</div>
</CardContent>
</Card>
</template>

View File

@ -0,0 +1,6 @@
export { default as ChartTooltip } from './ChartTooltip.vue'
export { default as ChartSingleTooltip } from './ChartSingleTooltip.vue'
export { default as ChartLegend } from './ChartLegend.vue'
export { default as ChartCrosshair } from './ChartCrosshair.vue'
export const defaultColors = ['hsl(var(--primary))', 'hsl(var(--muted))']

View File

@ -9,6 +9,7 @@ const DEPENDENCIES = new Map<string, string[]>([
['@vueuse/core', []], ['@vueuse/core', []],
['v-calendar', []], ['v-calendar', []],
['@tanstack/vue-table', []], ['@tanstack/vue-table', []],
['@unovis/vue', ['@unovis/ts']],
['vee-validate', ['@vee-validate/zod', 'zod']], ['vee-validate', ['@vee-validate/zod', 'zod']],
]) ])
// Some dependencies latest tag were not compatible with Vue3. // Some dependencies latest tag were not compatible with Vue3.

View File

@ -53,6 +53,19 @@
], ],
"type": "components:ui" "type": "components:ui"
}, },
{
"name": "area-chart",
"dependencies": [
"@unovis/vue",
"@unovis/ts"
],
"registryDependencies": [],
"files": [
"ui/area-chart/AreaChart.vue",
"ui/area-chart/index.ts"
],
"type": "components:ui"
},
{ {
"name": "aspect-ratio", "name": "aspect-ratio",
"dependencies": [ "dependencies": [

View File

@ -0,0 +1,19 @@
{
"name": "area-chart",
"dependencies": [
"@unovis/vue",
"@unovis/ts"
],
"registryDependencies": [],
"files": [
{
"name": "AreaChart.vue",
"content": "<script setup lang=\"ts\">\nimport { VisArea, VisAxis, VisLine, VisXYContainer } from '@unovis/vue'\nimport { Area } from '@unovis/ts'\n\ntype Data = typeof data[number]\nconst data = [\n { name: 'Jan', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Feb', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Mar', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Apr', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'May', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Jun', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Jul', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Aug', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Sep', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Oct', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Nov', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Dec', total: Math.floor(Math.random() * 5000) + 1000 },\n]\n</script>\n\n<template>\n <VisXYContainer height=\"350px\" :margin=\"{ left: 20, right: 20 }\" :data=\"data\">\n <svg width=\"0\" height=\"0\">\n <defs>\n <linearGradient id=\"colorUv\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"5%\" stop-color=\"hsl(var(--primary))\" stop-opacity=\"0.6\" />\n <stop offset=\"95%\" stop-color=\"hsl(var(--primary))\" stop-opacity=\"0\" />\n </linearGradient>\n </defs>\n </svg>\n\n <VisArea\n :x=\"(d: Data, i: number) => i\"\n :y=\"(d: Data) => d.total\"\n color=\"auto\"\n :attributes=\"{\n [Area.selectors.area]: {\n fill: 'url(#colorUv)',\n },\n }\"\n :rounded-corners=\"4\"\n :bar-padding=\"0.15\"\n />\n <VisLine\n :x=\"(d: Data, i: number) => i\"\n :y=\"(d: Data) => d.total\"\n color=\"hsl(var(--primary))\"\n />\n <VisAxis\n type=\"x\"\n :num-ticks=\"data.length\"\n :tick-format=\"(index: number) => data[index]?.name\"\n :grid-line=\"false\"\n :tick-line=\"false\"\n tick-text-color=\"hsl(var(--muted-foreground))\"\n />\n <VisAxis\n type=\"y\"\n :num-ticks=\"data.length\"\n :tick-format=\"(index: number) => data[index]?.name\"\n :grid-line=\"false\"\n :tick-line=\"false\"\n :domain-line=\"false\"\n tick-text-color=\"hsl(var(--muted-foreground))\"\n />\n </VisXYContainer>\n</template>\n"
},
{
"name": "index.ts",
"content": "export { default as AreaChart } from './AreaChart.vue'\n"
}
],
"type": "components:ui"
}

View File

@ -0,0 +1,19 @@
{
"name": "area-chart",
"dependencies": [
"@unovis/vue",
"@unovis/ts"
],
"registryDependencies": [],
"files": [
{
"name": "AreaChart.vue",
"content": "<script setup lang=\"ts\">\nimport { VisArea, VisAxis, VisLine, VisXYContainer } from '@unovis/vue'\nimport { Area } from '@unovis/ts'\n\ntype Data = typeof data[number]\nconst data = [\n { name: 'Jan', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Feb', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Mar', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Apr', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'May', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Jun', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Jul', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Aug', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Sep', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Oct', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Nov', total: Math.floor(Math.random() * 5000) + 1000 },\n { name: 'Dec', total: Math.floor(Math.random() * 5000) + 1000 },\n]\n</script>\n\n<template>\n <VisXYContainer height=\"350px\" :margin=\"{ left: 20, right: 20 }\" :data=\"data\">\n <svg width=\"0\" height=\"0\">\n <defs>\n <linearGradient id=\"colorUv\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"5%\" stop-color=\"hsl(var(--primary))\" stop-opacity=\"0.6\" />\n <stop offset=\"95%\" stop-color=\"hsl(var(--primary))\" stop-opacity=\"0\" />\n </linearGradient>\n </defs>\n </svg>\n\n <VisArea\n :x=\"(d: Data, i: number) => i\"\n :y=\"(d: Data) => d.total\"\n color=\"auto\"\n :attributes=\"{\n [Area.selectors.area]: {\n fill: 'url(#colorUv)',\n },\n }\"\n :rounded-corners=\"4\"\n :bar-padding=\"0.15\"\n />\n <VisLine\n :x=\"(d: Data, i: number) => i\"\n :y=\"(d: Data) => d.total\"\n color=\"hsl(var(--primary))\"\n />\n <VisAxis\n type=\"x\"\n :num-ticks=\"data.length\"\n :tick-format=\"(index: number) => data[index]?.name\"\n :grid-line=\"false\"\n :tick-line=\"false\"\n tick-text-color=\"hsl(var(--muted-foreground))\"\n />\n <VisAxis\n type=\"y\"\n :num-ticks=\"data.length\"\n :tick-format=\"(index: number) => data[index]?.name\"\n :grid-line=\"false\"\n :tick-line=\"false\"\n :domain-line=\"false\"\n tick-text-color=\"hsl(var(--muted-foreground))\"\n />\n </VisXYContainer>\n</template>\n"
},
{
"name": "index.ts",
"content": "export { default as AreaChart } from './AreaChart.vue'\n"
}
],
"type": "components:ui"
}