Index: app/Http/Controllers/DashboardController.php
===================================================================
--- app/Http/Controllers/DashboardController.php	(revision b7b2297c6f46586b8a37439269e971ba95d3ee13)
+++ app/Http/Controllers/DashboardController.php	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -3,63 +3,40 @@
 namespace App\Http\Controllers;
 
-use Illuminate\Http\Request;
+use App\Models\Payment;
+use Illuminate\Support\Facades\DB;
 use Inertia\Inertia;
 
 class DashboardController extends Controller
 {
-    /**
-     * Display a listing of the resource.
-     */
     public function index()
     {
-        return Inertia::render('Dashboard');
-    }
+        $currentRevenue = Payment::whereMonth('payment_date', now()->month)
+            ->whereYear('payment_date', now()->year)
+            ->where('payment_status', 'paid')
+            ->sum('amount');
 
-    /**
-     * Show the form for creating a new resource.
-     */
-    public function create()
-    {
-        //
-    }
+        $previousRevenue = Payment::whereMonth('payment_date', now()->subMonth()->month)
+            ->whereYear('payment_date', now()->subMonth()->year)
+            ->where('payment_status', 'paid')
+            ->sum('amount');
 
-    /**
-     * Store a newly created resource in storage.
-     */
-    public function store(Request $request)
-    {
-        //
-    }
+        $monthlyRevenue = Payment::select(
+            DB::raw("to_char(payment_date, 'YYYY-MM') as month"),
+            DB::raw('SUM(amount) as revenue')
+        )
+            ->where('payment_status', 'paid')
+            ->groupBy('month')
+            ->orderBy('month')
+            ->get()
+            ->map(fn($row) => [
+                'month' => $row->month,
+                'revenue' => (float) $row->revenue, // ✅ Ensure numeric type for TS
+            ]);
 
-    /**
-     * Display the specified resource.
-     */
-    public function show(string $id)
-    {
-        //
-    }
-
-    /**
-     * Show the form for editing the specified resource.
-     */
-    public function edit(string $id)
-    {
-        //
-    }
-
-    /**
-     * Update the specified resource in storage.
-     */
-    public function update(Request $request, string $id)
-    {
-        //
-    }
-
-    /**
-     * Remove the specified resource from storage.
-     */
-    public function destroy(string $id)
-    {
-        //
+        return Inertia::render('Dashboard', [
+            'currentRevenue' => (float) $currentRevenue,
+            'previousRevenue' => (float) $previousRevenue,
+            'monthlyRevenue' => $monthlyRevenue,
+        ]);
     }
 }
Index: package.json
===================================================================
--- package.json	(revision b7b2297c6f46586b8a37439269e971ba95d3ee13)
+++ package.json	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -42,6 +42,9 @@
         "@tanstack/vue-table": "^8.21.3",
         "@types/lodash-es": "^4.17.12",
+        "@unovis/ts": "^1.6.1",
+        "@unovis/vue": "^1.6.1",
         "@vueuse/core": "^13.9.0",
         "bunx": "^0.1.0",
+        "chart.js": "^4.5.0",
         "class-variance-authority": "^0.7.1",
         "clsx": "^2.1.1",
@@ -51,8 +54,10 @@
         "lodash-es": "^4.17.21",
         "lucide-vue-next": "^0.536.0",
-        "reka-ui": "^2.5.0",
+        "recharts": "^3.2.1",
+        "reka-ui": "^2.5.1",
         "shadcn-vue": "^2.2.0",
         "tailwind-merge": "^3.3.1",
         "tailwindcss-animate": "^1.0.7",
+        "vue-chartjs": "^5.3.2",
         "vue-loader": "^17.4.2",
         "vue-sonner": "^2.0.2"
Index: resources/js/Pages/Dashboard.vue
===================================================================
--- resources/js/Pages/Dashboard.vue	(revision b7b2297c6f46586b8a37439269e971ba95d3ee13)
+++ resources/js/Pages/Dashboard.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -1,10 +1,100 @@
-<script setup>
-import {Button} from '@/components/ui/button/index.js'
-import App from '@/Layout/App.vue'
+<script setup lang="ts">
+import { computed } from 'vue';
+import { Head } from '@inertiajs/vue3';
+import App from '@/Layout/App.vue';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import LineChart from '@/components/ui/chart/LineChart.vue';
+
+/* ---------- types ---------- */
+interface MonthlyRevenue {
+    month: string;
+    revenue: number;
+}
+
+const props = defineProps<{
+    currentRevenue: number;
+    previousRevenue: number;
+    monthlyRevenue: MonthlyRevenue[];
+}>();
+
+const revenueDelta = computed(() => {
+    if (!props.previousRevenue || props.previousRevenue === 0) return null;
+    const diff = props.currentRevenue - props.previousRevenue;
+    const pct = (diff / props.previousRevenue) * 100;
+    const sign = pct >= 0 ? '+' : '';
+    return sign + pct.toFixed(1) + '%';
+});
 </script>
 
 <template>
+    <Head title="Dashboard" />
     <App>
-        <h1>Dashboard</h1>
+        <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
+            <Card>
+                <CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
+                    <CardTitle class="text-sm font-medium">
+                        Total Revenue
+                    </CardTitle>
+                    <svg
+                        xmlns="http://www.w3.org/2000/svg"
+                        viewBox="0 0 24 24"
+                        fill="none"
+                        stroke="currentColor"
+                        stroke-linecap="round"
+                        stroke-linejoin="round"
+                        stroke-width="2"
+                        class="h-4 w-4 text-muted-foreground"
+                    >
+                        <path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
+                    </svg>
+                </CardHeader>
+
+                <CardContent>
+                    <div class="text-2xl font-bold">
+                        ${{ props.currentRevenue.toLocaleString() }}
+                    </div>
+                    <p class="text-xs text-muted-foreground">
+                        {{ revenueDelta }} from last month
+                    </p>
+
+                    <!-- Line chart here -->
+                    <LineChart
+                        :labels="props.monthlyRevenue.map(r => r.month)"
+                        :data="props.monthlyRevenue.map(r => r.revenue)"
+                    />
+                </CardContent>
+            </Card>
+            <Card>
+                <CardHeader
+                    class="flex flex-row items-center justify-between space-y-0 pb-2"
+                >
+                    <CardTitle class="text-sm font-medium">
+                        Total Revenue
+                    </CardTitle>
+                    <svg
+                        xmlns="http://www.w3.org/2000/svg"
+                        viewBox="0 0 24 24"
+                        fill="none"
+                        stroke="currentColor"
+                        strokeLinecap="round"
+                        strokeLinejoin="round"
+                        strokeWidth="2"
+                        class="text-muted-foreground h-4 w-4"
+                    >
+                        <path
+                            d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"
+                        />
+                    </svg>
+                </CardHeader>
+                <CardContent>
+                    <div class="text-2xl font-bold">$45,231.89</div>
+                    <p class="text-muted-foreground text-xs">
+                        +20.1% from last month
+                    </p>
+                </CardContent>
+            </Card>
+        </div>
     </App>
 </template>
+
+<style scoped></style>
Index: resources/js/components/ui/chart-bar/BarChart.vue
===================================================================
--- resources/js/components/ui/chart-bar/BarChart.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart-bar/BarChart.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,116 @@
+<script setup lang="ts" generic="T extends Record<string, any>">
+import type { BulletLegendItemInterface } from "@unovis/ts"
+import type { Component } from "vue"
+import type { BaseChartProps } from "."
+import { Axis, GroupedBar, StackedBar } from "@unovis/ts"
+import { VisAxis, VisGroupedBar, VisStackedBar, VisXYContainer } from "@unovis/vue"
+import { useMounted } from "@vueuse/core"
+import { computed, ref } from "vue"
+import { cn } from "@/lib/utils"
+import { ChartCrosshair, ChartLegend, defaultColors } from '@/components/ui/chart'
+
+const props = withDefaults(defineProps<BaseChartProps<T> & {
+  /**
+   * Render custom tooltip component.
+   */
+  customTooltip?: Component
+  /**
+   * Change the type of the chart
+   * @default "grouped"
+   */
+  type?: "stacked" | "grouped"
+  /**
+   * Rounded bar corners
+   * @default 0
+   */
+  roundedCorners?: number
+}>(), {
+  type: "grouped",
+  margin: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
+  filterOpacity: 0.2,
+  roundedCorners: 0,
+  showXAxis: true,
+  showYAxis: true,
+  showTooltip: true,
+  showLegend: true,
+  showGridLine: true,
+})
+const emits = defineEmits<{
+  legendItemClick: [d: BulletLegendItemInterface, i: number]
+}>()
+
+type KeyOfT = Extract<keyof T, string>
+type Data = typeof props.data[number]
+
+const index = computed(() => props.index as KeyOfT)
+const colors = computed(() => props.colors?.length ? props.colors : defaultColors(props.categories.length))
+const legendItems = ref<BulletLegendItemInterface[]>(props.categories.map((category, i) => ({
+  name: category,
+  color: colors.value[i],
+  inactive: false,
+})))
+
+const isMounted = useMounted()
+
+function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
+  emits("legendItemClick", d, i)
+}
+
+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
+      :data="data"
+      :style="{ height: isMounted ? '100%' : 'auto' }"
+      :margin="margin"
+    >
+      <ChartCrosshair v-if="showTooltip" :colors="colors" :items="legendItems" :custom-tooltip="customTooltip" :index="index" />
+
+      <VisBarComponent
+        :x="(d: Data, i: number) => i"
+        :y="categories.map(category => (d: Data) => d[category]) "
+        :color="colors"
+        :rounded-corners="roundedCorners"
+        :bar-padding="0.05"
+        :attributes="{
+          [selectorsBar]: {
+            opacity: (d: Data, i:number) => {
+              const pos = i % categories.length
+              return legendItems[pos]?.inactive ? filterOpacity : 1
+            },
+          },
+        }"
+      />
+
+      <VisAxis
+        v-if="showXAxis"
+        type="x"
+        :tick-format="xFormatter ?? ((v: number) => data[v]?.[index])"
+        :grid-line="false"
+        :tick-line="false"
+        tick-text-color="hsl(var(--vis-text-color))"
+      />
+      <VisAxis
+        v-if="showYAxis"
+        type="y"
+        :tick-line="false"
+        :tick-format="yFormatter"
+        :domain-line="false"
+        :grid-line="showGridLine"
+        :attributes="{
+          [Axis.selectors.grid]: {
+            class: 'text-muted',
+          },
+        }"
+        tick-text-color="hsl(var(--vis-text-color))"
+      />
+
+      <slot />
+    </VisXYContainer>
+  </div>
+</template>
Index: resources/js/components/ui/chart-bar/index.ts
===================================================================
--- resources/js/components/ui/chart-bar/index.ts	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart-bar/index.ts	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,66 @@
+export { default as BarChart } from "./BarChart.vue"
+
+import type { Spacing } from "@unovis/ts"
+
+type KeyOf<T extends Record<string, any>> = Extract<keyof T, string>
+
+export interface BaseChartProps<T extends Record<string, any>> {
+  /**
+   * The source data, in which each entry is a dictionary.
+   */
+  data: T[]
+  /**
+   * Select the categories from your data. Used to populate the legend and tooltip.
+   */
+  categories: KeyOf<T>[]
+  /**
+   * Sets the key to map the data to the axis.
+   */
+  index: KeyOf<T>
+  /**
+   * Change the default colors.
+   */
+  colors?: string[]
+  /**
+   * Margin of each the container
+   */
+  margin?: Spacing
+  /**
+   * Change the opacity of the non-selected field
+   * @default 0.2
+   */
+  filterOpacity?: number
+  /**
+   * Function to format X label
+   */
+  xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
+  /**
+   * Function to format Y label
+   */
+  yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
+  /**
+   * Controls the visibility of the X axis.
+   * @default true
+   */
+  showXAxis?: boolean
+  /**
+   * Controls the visibility of the Y axis.
+   * @default true
+   */
+  showYAxis?: boolean
+  /**
+   * Controls the visibility of tooltip.
+   * @default true
+   */
+  showTooltip?: boolean
+  /**
+   * Controls the visibility of legend.
+   * @default true
+   */
+  showLegend?: boolean
+  /**
+   * Controls the visibility of gridline.
+   * @default true
+   */
+  showGridLine?: boolean
+}
Index: resources/js/components/ui/chart-donut/DonutChart.vue
===================================================================
--- resources/js/components/ui/chart-donut/DonutChart.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart-donut/DonutChart.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,101 @@
+<script setup lang="ts" generic="T extends Record<string, any>">
+import type { Component } from "vue"
+import type { BaseChartProps } from "."
+import { Donut } from "@unovis/ts"
+import { VisDonut, VisSingleContainer } from "@unovis/vue"
+import { useMounted } from "@vueuse/core"
+import { computed, ref } from "vue"
+import { cn } from "@/lib/utils"
+import { ChartSingleTooltip, defaultColors } from '@/components/ui/chart'
+
+const props = withDefaults(defineProps<Pick<BaseChartProps<T>, "data" | "colors" | "index" | "margin" | "showLegend" | "showTooltip" | "filterOpacity"> & {
+  /**
+   * Sets the name of the key containing the quantitative chart values.
+   */
+  category: KeyOfT
+  /**
+   * Change the type of the chart
+   * @default "donut"
+   */
+  type?: "donut" | "pie"
+  /**
+   * Function to sort the segment
+   */
+  sortFunction?: (a: any, b: any) => number | undefined
+  /**
+   * Controls the formatting for the label.
+   */
+  valueFormatter?: (tick: number, i?: number, ticks?: number[]) => string
+  /**
+   * Render custom tooltip component.
+   */
+  customTooltip?: Component
+}>(), {
+  margin: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
+  sortFunction: () => undefined,
+  type: "donut",
+  filterOpacity: 0.2,
+  showTooltip: true,
+  showLegend: true,
+})
+
+type KeyOfT = Extract<keyof T, string>
+type Data = typeof props.data[number]
+
+const valueFormatter = props.valueFormatter ?? ((tick: number) => `${tick}`)
+const category = computed(() => props.category as KeyOfT)
+const index = computed(() => props.index as KeyOfT)
+
+const isMounted = useMounted()
+const activeSegmentKey = ref<string>()
+const colors = computed(() => props.colors?.length ? props.colors : defaultColors(props.data.filter(d => d[props.category]).filter(Boolean).length))
+const legendItems = computed(() => props.data.map((item, i) => ({
+  name: item[props.index],
+  color: colors.value[i],
+  inactive: false,
+})))
+
+const totalValue = computed(() => props.data.reduce((prev, curr) => {
+  return prev + curr[props.category]
+}, 0))
+</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"
+        :value-formatter="valueFormatter"
+        :custom-tooltip="customTooltip"
+      />
+
+      <VisDonut
+        :value="(d: Data) => d[category]"
+        :sort-function="sortFunction"
+        :color="colors"
+        :arc-width="type === 'donut' ? 20 : 0"
+        :show-background="false"
+        :central-label="type === 'donut' ? valueFormatter(totalValue) : ''"
+        :events="{
+          [Donut.selectors.segment]: {
+            click: (d: Data, 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'
+              }
+            },
+          },
+        }"
+      />
+
+      <slot />
+    </VisSingleContainer>
+  </div>
+</template>
Index: resources/js/components/ui/chart-donut/index.ts
===================================================================
--- resources/js/components/ui/chart-donut/index.ts	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart-donut/index.ts	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,39 @@
+export { default as DonutChart } from "./DonutChart.vue"
+
+import type { Spacing } from "@unovis/ts"
+
+type KeyOf<T extends Record<string, any>> = Extract<keyof T, string>
+
+export interface BaseChartProps<T extends Record<string, any>> {
+  /**
+   * The source data, in which each entry is a dictionary.
+   */
+  data: T[]
+  /**
+   * Sets the key to map the data to the axis.
+   */
+  index: KeyOf<T>
+  /**
+   * Change the default colors.
+   */
+  colors?: string[]
+  /**
+   * Margin of each the container
+   */
+  margin?: Spacing
+  /**
+   * Change the opacity of the non-selected field
+   * @default 0.2
+   */
+  filterOpacity?: number
+  /**
+   * Controls the visibility of tooltip.
+   * @default true
+   */
+  showTooltip?: boolean
+  /**
+   * Controls the visibility of legend.
+   * @default true
+   */
+  showLegend?: boolean
+}
Index: resources/js/components/ui/chart-line/LineChart.vue
===================================================================
--- resources/js/components/ui/chart-line/LineChart.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart-line/LineChart.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,107 @@
+<script setup lang="ts" generic="T extends Record<string, any>">
+import type { BulletLegendItemInterface } from "@unovis/ts"
+import type { Component } from "vue"
+import type { BaseChartProps } from "."
+import { Axis, CurveType, Line } from "@unovis/ts"
+
+import { VisAxis, VisLine, VisXYContainer } from "@unovis/vue"
+import { useMounted } from "@vueuse/core"
+import { computed, ref } from "vue"
+import { cn } from "@/lib/utils"
+import { ChartCrosshair, ChartLegend, defaultColors } from '@/components/ui/chart'
+
+const props = withDefaults(defineProps<BaseChartProps<T> & {
+  /**
+   * Render custom tooltip component.
+   */
+  customTooltip?: Component
+  /**
+   * Type of curve
+   */
+  curveType?: CurveType
+}>(), {
+  curveType: CurveType.MonotoneX,
+  filterOpacity: 0.2,
+  margin: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
+  showXAxis: true,
+  showYAxis: true,
+  showTooltip: true,
+  showLegend: true,
+  showGridLine: true,
+})
+
+const emits = defineEmits<{
+  legendItemClick: [d: BulletLegendItemInterface, i: number]
+}>()
+
+type KeyOfT = Extract<keyof T, string>
+type Data = typeof props.data[number]
+
+const index = computed(() => props.index as KeyOfT)
+const colors = computed(() => props.colors?.length ? props.colors : defaultColors(props.categories.length))
+
+const legendItems = ref<BulletLegendItemInterface[]>(props.categories.map((category, i) => ({
+  name: category,
+  color: colors.value[i],
+  inactive: false,
+})))
+
+const isMounted = useMounted()
+
+function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
+  emits("legendItemClick", d, i)
+}
+</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
+      :margin="{ left: 20, right: 20 }"
+      :data="data"
+      :style="{ height: isMounted ? '100%' : 'auto' }"
+    >
+      <ChartCrosshair v-if="showTooltip" :colors="colors" :items="legendItems" :index="index" :custom-tooltip="customTooltip" />
+
+      <template v-for="(category, i) in categories" :key="category">
+        <VisLine
+          :x="(d: Data, i: number) => i"
+          :y="(d: Data) => d[category]"
+          :curve-type="curveType"
+          :color="colors[i]"
+          :attributes="{
+            [Line.selectors.line]: {
+              opacity: legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1,
+            },
+          }"
+        />
+      </template>
+
+      <VisAxis
+        v-if="showXAxis"
+        type="x"
+        :tick-format="xFormatter ?? ((v: number) => data[v]?.[index])"
+        :grid-line="false"
+        :tick-line="false"
+        tick-text-color="hsl(var(--vis-text-color))"
+      />
+      <VisAxis
+        v-if="showYAxis"
+        type="y"
+        :tick-line="false"
+        :tick-format="yFormatter"
+        :domain-line="false"
+        :grid-line="showGridLine"
+        :attributes="{
+          [Axis.selectors.grid]: {
+            class: 'text-muted',
+          },
+        }"
+        tick-text-color="hsl(var(--vis-text-color))"
+      />
+
+      <slot />
+    </VisXYContainer>
+  </div>
+</template>
Index: resources/js/components/ui/chart-line/index.ts
===================================================================
--- resources/js/components/ui/chart-line/index.ts	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart-line/index.ts	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,66 @@
+export { default as LineChart } from "./LineChart.vue"
+
+import type { Spacing } from "@unovis/ts"
+
+type KeyOf<T extends Record<string, any>> = Extract<keyof T, string>
+
+export interface BaseChartProps<T extends Record<string, any>> {
+  /**
+   * The source data, in which each entry is a dictionary.
+   */
+  data: T[]
+  /**
+   * Select the categories from your data. Used to populate the legend and tooltip.
+   */
+  categories: KeyOf<T>[]
+  /**
+   * Sets the key to map the data to the axis.
+   */
+  index: KeyOf<T>
+  /**
+   * Change the default colors.
+   */
+  colors?: string[]
+  /**
+   * Margin of each the container
+   */
+  margin?: Spacing
+  /**
+   * Change the opacity of the non-selected field
+   * @default 0.2
+   */
+  filterOpacity?: number
+  /**
+   * Function to format X label
+   */
+  xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
+  /**
+   * Function to format Y label
+   */
+  yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
+  /**
+   * Controls the visibility of the X axis.
+   * @default true
+   */
+  showXAxis?: boolean
+  /**
+   * Controls the visibility of the Y axis.
+   * @default true
+   */
+  showYAxis?: boolean
+  /**
+   * Controls the visibility of tooltip.
+   * @default true
+   */
+  showTooltip?: boolean
+  /**
+   * Controls the visibility of legend.
+   * @default true
+   */
+  showLegend?: boolean
+  /**
+   * Controls the visibility of gridline.
+   * @default true
+   */
+  showGridLine?: boolean
+}
Index: resources/js/components/ui/chart/ChartCrosshair.vue
===================================================================
--- resources/js/components/ui/chart/ChartCrosshair.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart/ChartCrosshair.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,45 @@
+<script setup lang="ts">
+import type { BulletLegendItemInterface } from "@unovis/ts"
+import type { Component } from "vue"
+import { omit } from "@unovis/ts"
+import { VisCrosshair, VisTooltip } from "@unovis/vue"
+import { createApp } from "vue"
+import { ChartTooltip } from "."
+
+const props = withDefaults(defineProps<{
+  colors: string[]
+  index: string
+  items: BulletLegendItemInterface[]
+  customTooltip?: Component
+}>(), {
+  colors: () => [],
+})
+
+// Use weakmap to store reference to each datapoint for Tooltip
+const wm = new WeakMap()
+function template(d: 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 }
+    })
+    const TooltipComponent = props.customTooltip ?? ChartTooltip
+    createApp(TooltipComponent, { title: d[props.index].toString(), data: omittedData }).mount(componentDiv)
+    wm.set(d, componentDiv.innerHTML)
+    return componentDiv.innerHTML
+  }
+}
+
+function color(d: unknown, i: number) {
+  return props.colors[i] ?? "transparent"
+}
+</script>
+
+<template>
+  <VisTooltip :horizontal-shift="20" :vertical-shift="20" />
+  <VisCrosshair :template="template" :color="color" />
+</template>
Index: resources/js/components/ui/chart/ChartLegend.vue
===================================================================
--- resources/js/components/ui/chart/ChartLegend.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart/ChartLegend.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,60 @@
+<script setup lang="ts">
+import type { BulletLegendItemInterface } from "@unovis/ts"
+import { BulletLegend } from "@unovis/ts"
+import { VisBulletLegend } from "@unovis/vue"
+import { nextTick, onMounted, ref } from "vue"
+import { buttonVariants } from '@/components/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>()
+
+function keepStyling() {
+  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, "!inline-flex", "!mr-2"))
+  })
+}
+
+onMounted(() => {
+  keepStyling()
+})
+
+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 }))
+  }
+  keepStyling()
+}
+</script>
+
+<template>
+  <div
+    ref="elRef" class="w-max" :style="{
+      '--vis-legend-bullet-size': '16px',
+    }"
+  >
+    <VisBulletLegend
+      :items="items"
+      :on-legend-item-click="onLegendItemClick"
+    />
+  </div>
+</template>
Index: resources/js/components/ui/chart/ChartSingleTooltip.vue
===================================================================
--- resources/js/components/ui/chart/ChartSingleTooltip.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart/ChartSingleTooltip.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,63 @@
+<script setup lang="ts">
+import type { BulletLegendItemInterface } from "@unovis/ts"
+import type { Component } from "vue"
+import { omit } from "@unovis/ts"
+import { VisTooltip } from "@unovis/vue"
+import { createApp } from "vue"
+import { ChartTooltip } from "."
+
+const props = defineProps<{
+  selector: string
+  index: string
+  items?: BulletLegendItemInterface[]
+  valueFormatter?: (tick: number, i?: number, ticks?: number[]) => string
+  customTooltip?: Component
+}>()
+
+// Use weakmap to store reference to each datapoint for Tooltip
+const wm = new WeakMap()
+function template(d: any, i: number, elements: (HTMLElement | SVGElement)[]) {
+  const valueFormatter = props.valueFormatter ?? ((tick: number) => `${tick}`)
+  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: valueFormatter(value) }
+      })
+      const TooltipComponent = props.customTooltip ?? ChartTooltip
+      createApp(TooltipComponent, { 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: valueFormatter(data[props.index]), color: style.fill }]
+      const componentDiv = document.createElement("div")
+      const TooltipComponent = props.customTooltip ?? ChartTooltip
+      createApp(TooltipComponent, { 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>
Index: resources/js/components/ui/chart/ChartTooltip.vue
===================================================================
--- resources/js/components/ui/chart/ChartTooltip.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart/ChartTooltip.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,40 @@
+<script setup lang="ts">
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/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 ml-4">{{ item.value }}</span>
+      </div>
+    </CardContent>
+  </Card>
+</template>
Index: resources/js/components/ui/chart/LineChart.vue
===================================================================
--- resources/js/components/ui/chart/LineChart.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart/LineChart.vue	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,72 @@
+<script setup lang="ts">
+import {
+    CategoryScale,
+    Chart as ChartJS,
+    Filler,
+    LinearScale,
+    LineElement,
+    PointElement,
+    Tooltip,
+} from 'chart.js';
+import { Line } from 'vue-chartjs';
+
+ChartJS.register(
+    LineElement,
+    PointElement,
+    LinearScale,
+    CategoryScale,
+    Tooltip,
+    Filler,
+);
+
+const props = defineProps<{
+    labels: string[];
+    data: number[];
+}>();
+
+const chartData = {
+    labels: props.labels,
+    datasets: [
+        {
+            label: 'Revenue',
+            data: props.data,
+            borderColor: '#000',
+            borderWidth: 2,
+            tension: 0.3,
+            pointBackgroundColor: '#fff',
+            pointBorderColor: '#000',
+            pointBorderWidth: 1.5,
+            fill: false,
+        },
+    ],
+};
+
+const chartOptions = {
+    responsive: true,
+    maintainAspectRatio: false,
+    plugins: {
+        legend: { display: false },
+        tooltip: {
+            enabled: true,
+            backgroundColor: '#111',
+            titleColor: '#fff',
+            bodyColor: '#fff',
+        },
+    },
+    elements: {
+        line: {
+            borderDash: [3, 3], // dotted line
+        },
+    },
+    scales: {
+        x: { display: false },
+        y: { display: false },
+    },
+};
+</script>
+
+<template>
+    <div class="mt-4 h-20">
+        <Line :data="chartData" :options="chartOptions" />
+    </div>
+</template>
Index: resources/js/components/ui/chart/index.ts
===================================================================
--- resources/js/components/ui/chart/index.ts	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart/index.ts	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,18 @@
+export { default as ChartCrosshair } from "./ChartCrosshair.vue"
+export { default as ChartLegend } from "./ChartLegend.vue"
+export { default as ChartSingleTooltip } from "./ChartSingleTooltip.vue"
+export { default as ChartTooltip } from "./ChartTooltip.vue"
+
+export function defaultColors(count: number = 3) {
+  const quotient = Math.floor(count / 2)
+  const remainder = count % 2
+
+  const primaryCount = quotient + remainder
+  const secondaryCount = quotient
+  return [
+    ...Array.from(new Array(primaryCount).keys()).map(i => `hsl(var(--vis-primary-color) / ${1 - (1 / primaryCount) * i})`),
+    ...Array.from(new Array(secondaryCount).keys()).map(i => `hsl(var(--vis-secondary-color) / ${1 - (1 / secondaryCount) * i})`),
+  ]
+}
+
+export * from "./interface"
Index: resources/js/components/ui/chart/interface.ts
===================================================================
--- resources/js/components/ui/chart/interface.ts	(revision 847485c09a11468c23071337f9f158a06a231285)
+++ resources/js/components/ui/chart/interface.ts	(revision 847485c09a11468c23071337f9f158a06a231285)
@@ -0,0 +1,64 @@
+import type { Spacing } from "@unovis/ts"
+
+type KeyOf<T extends Record<string, any>> = Extract<keyof T, string>
+
+export interface BaseChartProps<T extends Record<string, any>> {
+  /**
+   * The source data, in which each entry is a dictionary.
+   */
+  data: T[]
+  /**
+   * Select the categories from your data. Used to populate the legend and tooltip.
+   */
+  categories: KeyOf<T>[]
+  /**
+   * Sets the key to map the data to the axis.
+   */
+  index: KeyOf<T>
+  /**
+   * Change the default colors.
+   */
+  colors?: string[]
+  /**
+   * Margin of each the container
+   */
+  margin?: Spacing
+  /**
+   * Change the opacity of the non-selected field
+   * @default 0.2
+   */
+  filterOpacity?: number
+  /**
+   * Function to format X label
+   */
+  xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
+  /**
+   * Function to format Y label
+   */
+  yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
+  /**
+   * Controls the visibility of the X axis.
+   * @default true
+   */
+  showXAxis?: boolean
+  /**
+   * Controls the visibility of the Y axis.
+   * @default true
+   */
+  showYAxis?: boolean
+  /**
+   * Controls the visibility of tooltip.
+   * @default true
+   */
+  showTooltip?: boolean
+  /**
+   * Controls the visibility of legend.
+   * @default true
+   */
+  showLegend?: boolean
+  /**
+   * Controls the visibility of gridline.
+   * @default true
+   */
+  showGridLine?: boolean
+}
