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',
|
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: [],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
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: {
|
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',
|
||||||
|
|
|
||||||
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: {
|
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',
|
||||||
|
|
|
||||||
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', []],
|
['@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.
|
||||||
|
|
|
||||||
|
|
@ -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": [
|
||||||
|
|
|
||||||
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