feat: introduce area, line, bar, donut chart
This commit is contained in:
parent
60d22c8206
commit
bfccb78a4f
|
|
@ -128,6 +128,21 @@ export const docsConfig: DocsConfig = {
|
|||
href: '/docs/charts/area',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Line',
|
||||
href: '/docs/charts/line',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Bar',
|
||||
href: '/docs/charts/bar',
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: 'Donut',
|
||||
href: '/docs/charts/donut',
|
||||
items: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ function constructFiles(componentName: string, style: Style, sources: Record<str
|
|||
'package.json': {
|
||||
content: {
|
||||
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,
|
||||
devDependencies,
|
||||
},
|
||||
|
|
|
|||
16
apps/www/src/content/docs/charts/area.md
Normal file
16
apps/www/src/content/docs/charts/area.md
Normal 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
|
||||
```
|
||||
|
||||
|
||||
16
apps/www/src/content/docs/charts/bar.md
Normal file
16
apps/www/src/content/docs/charts/bar.md
Normal 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
|
||||
```
|
||||
|
||||
|
||||
16
apps/www/src/content/docs/charts/donut.md
Normal file
16
apps/www/src/content/docs/charts/donut.md
Normal 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
|
||||
```
|
||||
|
||||
|
||||
16
apps/www/src/content/docs/charts/line.md
Normal file
16
apps/www/src/content/docs/charts/line.md
Normal 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
|
||||
```
|
||||
|
||||
|
||||
19
apps/www/src/lib/registry/default/example/AreaChartDemo.vue
Normal file
19
apps/www/src/lib/registry/default/example/AreaChartDemo.vue
Normal 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>
|
||||
28
apps/www/src/lib/registry/default/example/BarChartDemo.vue
Normal file
28
apps/www/src/lib/registry/default/example/BarChartDemo.vue
Normal 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>
|
||||
21
apps/www/src/lib/registry/default/example/DonutChartDemo.vue
Normal file
21
apps/www/src/lib/registry/default/example/DonutChartDemo.vue
Normal 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>
|
||||
28
apps/www/src/lib/registry/default/example/LineChartDemo.vue
Normal file
28
apps/www/src/lib/registry/default/example/LineChartDemo.vue
Normal 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>
|
||||
|
|
@ -19,6 +19,7 @@ export const buttonVariants = cva(
|
|||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
xs: 'h-7 rounded px-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
|
|
|
|||
133
apps/www/src/lib/registry/default/ui/chart-area/AreaChart.vue
Normal file
133
apps/www/src/lib/registry/default/ui/chart-area/AreaChart.vue
Normal 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>
|
||||
1
apps/www/src/lib/registry/default/ui/chart-area/index.ts
Normal file
1
apps/www/src/lib/registry/default/ui/chart-area/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as AreaChart } from './AreaChart.vue'
|
||||
113
apps/www/src/lib/registry/default/ui/chart-bar/BarChart.vue
Normal file
113
apps/www/src/lib/registry/default/ui/chart-bar/BarChart.vue
Normal 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>
|
||||
1
apps/www/src/lib/registry/default/ui/chart-bar/index.ts
Normal file
1
apps/www/src/lib/registry/default/ui/chart-bar/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as BarChart } from './BarChart.vue'
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as DonutChart } from './DonutChart.vue'
|
||||
105
apps/www/src/lib/registry/default/ui/chart-line/LineChart.vue
Normal file
105
apps/www/src/lib/registry/default/ui/chart-line/LineChart.vue
Normal 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>
|
||||
1
apps/www/src/lib/registry/default/ui/chart-line/index.ts
Normal file
1
apps/www/src/lib/registry/default/ui/chart-line/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as LineChart } from './LineChart.vue'
|
||||
|
|
@ -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>
|
||||
51
apps/www/src/lib/registry/default/ui/chart/ChartLegend.vue
Normal file
51
apps/www/src/lib/registry/default/ui/chart/ChartLegend.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
40
apps/www/src/lib/registry/default/ui/chart/ChartTooltip.vue
Normal file
40
apps/www/src/lib/registry/default/ui/chart/ChartTooltip.vue
Normal 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>
|
||||
6
apps/www/src/lib/registry/default/ui/chart/index.ts
Normal file
6
apps/www/src/lib/registry/default/ui/chart/index.ts
Normal 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))']
|
||||
19
apps/www/src/lib/registry/new-york/example/AreaChartDemo.vue
Normal file
19
apps/www/src/lib/registry/new-york/example/AreaChartDemo.vue
Normal 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>
|
||||
28
apps/www/src/lib/registry/new-york/example/BarChartDemo.vue
Normal file
28
apps/www/src/lib/registry/new-york/example/BarChartDemo.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
28
apps/www/src/lib/registry/new-york/example/LineChartDemo.vue
Normal file
28
apps/www/src/lib/registry/new-york/example/LineChartDemo.vue
Normal 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>
|
||||
|
|
@ -20,6 +20,7 @@ export const buttonVariants = cva(
|
|||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
xs: 'h-7 rounded px-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
|
|
|
|||
133
apps/www/src/lib/registry/new-york/ui/chart-area/AreaChart.vue
Normal file
133
apps/www/src/lib/registry/new-york/ui/chart-area/AreaChart.vue
Normal 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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as AreaChart } from './AreaChart.vue'
|
||||
113
apps/www/src/lib/registry/new-york/ui/chart-bar/BarChart.vue
Normal file
113
apps/www/src/lib/registry/new-york/ui/chart-bar/BarChart.vue
Normal 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>
|
||||
1
apps/www/src/lib/registry/new-york/ui/chart-bar/index.ts
Normal file
1
apps/www/src/lib/registry/new-york/ui/chart-bar/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as BarChart } from './BarChart.vue'
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as DonutChart } from './DonutChart.vue'
|
||||
105
apps/www/src/lib/registry/new-york/ui/chart-line/LineChart.vue
Normal file
105
apps/www/src/lib/registry/new-york/ui/chart-line/LineChart.vue
Normal 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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as LineChart } from './LineChart.vue'
|
||||
|
|
@ -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>
|
||||
51
apps/www/src/lib/registry/new-york/ui/chart/ChartLegend.vue
Normal file
51
apps/www/src/lib/registry/new-york/ui/chart/ChartLegend.vue
Normal 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>
|
||||
|
|
@ -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>
|
||||
40
apps/www/src/lib/registry/new-york/ui/chart/ChartTooltip.vue
Normal file
40
apps/www/src/lib/registry/new-york/ui/chart/ChartTooltip.vue
Normal 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>
|
||||
6
apps/www/src/lib/registry/new-york/ui/chart/index.ts
Normal file
6
apps/www/src/lib/registry/new-york/ui/chart/index.ts
Normal 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))']
|
||||
|
|
@ -9,6 +9,7 @@ const DEPENDENCIES = new Map<string, string[]>([
|
|||
['@vueuse/core', []],
|
||||
['v-calendar', []],
|
||||
['@tanstack/vue-table', []],
|
||||
['@unovis/vue', ['@unovis/ts']],
|
||||
['vee-validate', ['@vee-validate/zod', 'zod']],
|
||||
])
|
||||
// Some dependencies latest tag were not compatible with Vue3.
|
||||
|
|
|
|||
|
|
@ -53,6 +53,19 @@
|
|||
],
|
||||
"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",
|
||||
"dependencies": [
|
||||
|
|
|
|||
19
apps/www/src/public/registry/styles/default/area-chart.json
Normal file
19
apps/www/src/public/registry/styles/default/area-chart.json
Normal 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"
|
||||
}
|
||||
19
apps/www/src/public/registry/styles/new-york/area-chart.json
Normal file
19
apps/www/src/public/registry/styles/new-york/area-chart.json
Normal 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"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user