|
| 1 | +<script setup lang="ts" generic="T extends Record<string, any>"> |
| 2 | +import type { BaseChartProps } from '.' |
| 3 | +import { ChartCrosshair, ChartLegend, defaultColors } from '~/components/ui/chart' |
| 4 | +import { cn } from '~/lib/utils' |
| 5 | +import { type BulletLegendItemInterface, CurveType } from '@unovis/ts' |
| 6 | +import { Area, Axis, Line } from '@unovis/ts' |
| 7 | +import { VisArea, VisAxis, VisLine, VisXYContainer } from '@unovis/vue' |
| 8 | +import { useMounted } from '@vueuse/core' |
| 9 | +import { useId } from 'radix-vue' |
| 10 | +import { type Component, computed, ref } from 'vue' |
| 11 | +
|
| 12 | +const props = withDefaults(defineProps<BaseChartProps<T> & { |
| 13 | + /** |
| 14 | + * Render custom tooltip component. |
| 15 | + */ |
| 16 | + customTooltip?: Component |
| 17 | + /** |
| 18 | + * Type of curve |
| 19 | + */ |
| 20 | + curveType?: CurveType |
| 21 | + /** |
| 22 | + * Controls the visibility of gradient. |
| 23 | + * @default true |
| 24 | + */ |
| 25 | + showGradiant?: boolean |
| 26 | +}>(), { |
| 27 | + curveType: CurveType.MonotoneX, |
| 28 | + filterOpacity: 0.2, |
| 29 | + margin: () => ({ top: 0, bottom: 0, left: 0, right: 0 }), |
| 30 | + showXAxis: true, |
| 31 | + showYAxis: true, |
| 32 | + showTooltip: true, |
| 33 | + showLegend: true, |
| 34 | + showGridLine: true, |
| 35 | + showGradiant: true, |
| 36 | +}) |
| 37 | +
|
| 38 | +const emits = defineEmits<{ |
| 39 | + legendItemClick: [d: BulletLegendItemInterface, i: number] |
| 40 | +}>() |
| 41 | +
|
| 42 | +type KeyOfT = Extract<keyof T, string> |
| 43 | +type Data = typeof props.data[number] |
| 44 | +
|
| 45 | +const chartRef = useId() |
| 46 | +
|
| 47 | +const index = computed(() => props.index as KeyOfT) |
| 48 | +const colors = computed(() => props.colors?.length ? props.colors : defaultColors(props.categories.length)) |
| 49 | +
|
| 50 | +const legendItems = ref<BulletLegendItemInterface[]>(props.categories.map((category, i) => ({ |
| 51 | + name: category, |
| 52 | + color: colors.value[i], |
| 53 | + inactive: false, |
| 54 | +}))) |
| 55 | +
|
| 56 | +const isMounted = useMounted() |
| 57 | +
|
| 58 | +function handleLegendItemClick(d: BulletLegendItemInterface, i: number) { |
| 59 | + emits('legendItemClick', d, i) |
| 60 | +} |
| 61 | +</script> |
| 62 | + |
| 63 | +<template> |
| 64 | + <div :class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')"> |
| 65 | + <ChartLegend v-if="showLegend" v-model:items="legendItems" @legend-item-click="handleLegendItemClick" /> |
| 66 | + |
| 67 | + <VisXYContainer :style="{ height: isMounted ? '100%' : 'auto' }" :margin="{ left: 20, right: 20 }" :data="data"> |
| 68 | + <svg width="0" height="0"> |
| 69 | + <defs> |
| 70 | + <linearGradient v-for="(color, i) in colors" :id="`${chartRef}-color-${i}`" :key="i" x1="0" y1="0" x2="0" y2="1"> |
| 71 | + <template v-if="showGradiant"> |
| 72 | + <stop offset="5%" :stop-color="color" stop-opacity="0.4" /> |
| 73 | + <stop offset="95%" :stop-color="color" stop-opacity="0" /> |
| 74 | + </template> |
| 75 | + <template v-else> |
| 76 | + <stop offset="0%" :stop-color="color" /> |
| 77 | + </template> |
| 78 | + </linearGradient> |
| 79 | + </defs> |
| 80 | + </svg> |
| 81 | + |
| 82 | + <ChartCrosshair v-if="showTooltip" :colors="colors" :items="legendItems" :index="index" :custom-tooltip="customTooltip" /> |
| 83 | + |
| 84 | + <template v-for="(category, i) in categories" :key="category"> |
| 85 | + <VisArea |
| 86 | + :x="(d: Data, i: number) => i" |
| 87 | + :y="(d: Data) => d[category]" |
| 88 | + color="auto" |
| 89 | + :curve-type="curveType" |
| 90 | + :attributes="{ |
| 91 | + [Area.selectors.area]: { |
| 92 | + fill: `url(#${chartRef}-color-${i})`, |
| 93 | + }, |
| 94 | + }" |
| 95 | + :opacity="legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1" |
| 96 | + /> |
| 97 | + </template> |
| 98 | + |
| 99 | + <template v-for="(category, i) in categories" :key="category"> |
| 100 | + <VisLine |
| 101 | + :x="(d: Data, i: number) => i" |
| 102 | + :y="(d: Data) => d[category]" |
| 103 | + :color="colors[i]" |
| 104 | + :curve-type="curveType" |
| 105 | + :attributes="{ |
| 106 | + [Line.selectors.line]: { |
| 107 | + opacity: legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1, |
| 108 | + }, |
| 109 | + }" |
| 110 | + /> |
| 111 | + </template> |
| 112 | + |
| 113 | + <VisAxis |
| 114 | + v-if="showXAxis" |
| 115 | + type="x" |
| 116 | + :tick-format="xFormatter ?? ((v: number) => data[v]?.[index])" |
| 117 | + :grid-line="false" |
| 118 | + :tick-line="false" |
| 119 | + tick-text-color="hsl(var(--vis-text-color))" |
| 120 | + /> |
| 121 | + <VisAxis |
| 122 | + v-if="showYAxis" |
| 123 | + type="y" |
| 124 | + :tick-line="false" |
| 125 | + :tick-format="yFormatter" |
| 126 | + :domain-line="false" |
| 127 | + :grid-line="showGridLine" |
| 128 | + :attributes="{ |
| 129 | + [Axis.selectors.grid]: { |
| 130 | + class: 'text-muted', |
| 131 | + }, |
| 132 | + }" |
| 133 | + tick-text-color="hsl(var(--vis-text-color))" |
| 134 | + /> |
| 135 | + |
| 136 | + <slot /> |
| 137 | + </VisXYContainer> |
| 138 | + </div> |
| 139 | +</template> |
0 commit comments