Menu

Change language

GitHub repo

Bar Chart

bar-chartui

Bar chart wrapper around Apache ECharts. Vertical, horizontal, grouped, stacked, and negative-value variants. Theme-aware via registry tokens.

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/bar-chart.json
Named registry:npx shadcn-vue@latest add @uipkge/bar-chartInstalls to:app/components/ui/charts/bar-chart/

Examples

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
dataRecord<string, any>[]—required
xFieldstring'x'optional
yFieldstring | string[]'y'optional
stacked

Stack series on one baseline. Default false.

booleanfalseoptional
stackGap

Gap in px between stacked bar segments. Default 1. Set to 0 to disable.

number1optional
stackGapColor

Color of the gap between stacked bar segments. Defaults to card background.

string—optional
valueLabels

Show value labels on top of each bar. Default false.

booleanfalseoptional
radius

Top corner rounding in px. Default 6.

number6optional
heightnumber | string300optional
optionany—optional
classstring—optional
ariaLabel

Accessible name announced for the chart image. Defaults to "Chart".

string—optional

npm dependencies

Used by

Files installed (3)

  • app/components/ui/charts/bar-chart/BarChart.vue6.2 kB
    <script setup lang="ts">
    import { computed } from 'vue'
    import { use } from 'echarts/core'
    import { CanvasRenderer } from 'echarts/renderers'
    import { BarChart as EChartsBarChart } from 'echarts/charts'
    import { GridComponent, TooltipComponent, LegendComponent, MarkAreaComponent } from 'echarts/components'
    import VChart from 'vue-echarts'
    import { cn } from '@/lib/utils'
    import {
      chartColors,
      chartTextColor,
      chartAxisColor,
      chartSplitLineColor,
      chartTooltipBg,
      chartTooltipBorder,
      chartTooltipText,
      chartBgColor,
      mergeOptionBlock,
    } from '../useChartTheme'
    
    use([CanvasRenderer, EChartsBarChart, GridComponent, TooltipComponent, LegendComponent, MarkAreaComponent])
    
    interface Props {
      data: Record<string, any>[]
      xField?: string
      yField?: string | string[]
      /** Stack series on one baseline. Default false. */
      stacked?: boolean
      /** Gap in px between stacked bar segments. Default 1. Set to 0 to disable. */
      stackGap?: number
      /** Color of the gap between stacked bar segments. Defaults to card background. */
      stackGapColor?: string
      /** Show value labels on top of each bar. Default false. */
      valueLabels?: boolean
      /** Top corner rounding in px. Default 6. */
      radius?: number
      height?: number | string
      option?: any
      class?: string
      /** Accessible name announced for the chart image. Defaults to "Chart". */
      ariaLabel?: string
    }
    
    const props = withDefaults(defineProps<Props>(), {
      xField: 'x',
      yField: 'y',
      stacked: false,
      stackGap: 1,
      valueLabels: false,
      radius: 6,
      height: 300,
    })
    
    const mergedOption = computed(() => {
      const fields = Array.isArray(props.yField) ? props.yField : [props.yField]
      const xData = props.data.map((d) => d[props.xField!])
    
      const userOption: any = props.option ?? {}
      const {
        series: userSeries,
        xAxis: userXAxis,
        yAxis: userYAxis,
        grid: userGrid,
        tooltip: userTooltip,
        legend: userLegend,
        ...userRest
      } = userOption
    
      const isHorizontal = userXAxis?.type === 'value' || userYAxis?.type === 'category'
      const hasUserStack = Array.isArray(userSeries) && userSeries.some((s: any) => Boolean(s?.stack))
      const stackGap = props.stackGap
      const stackGapColor = props.stackGapColor ?? chartBgColor.value
    
      const defaultRadius = [props.radius, props.radius, props.radius, props.radius]
    
      const series = fields.map((field, i) => {
        const u = Array.isArray(userSeries) ? (userSeries[i] ?? {}) : {}
        const isStacked = Boolean(props.stacked || u?.stack || hasUserStack)
    
        return {
          // Single-series charts hide the legend, so a blank name only drops the raw field key from the tooltip row.
          name: fields.length > 1 ? field : '',
          type: 'bar',
          stack: props.stacked ? 'bars' : undefined,
          barMaxWidth: 32,
          itemStyle: {
            color: chartColors.value[i % chartColors.value.length],
            borderRadius: defaultRadius,
            ...(isStacked && stackGap > 0
              ? {
                  borderColor: stackGapColor,
                  borderWidth: stackGap,
                }
              : {}),
          },
          label: props.valueLabels ? { show: true, position: 'top', color: chartTextColor.value, fontSize: 11 } : undefined,
          data: props.data.map((d) => d[field]),
        }
      })
    
      // Per-index series merge + 2-level deep merge for axis/grid/tooltip
      // blocks (see AreaChart for the rule). Lets consumers tweak axisLabel
      // font size without losing the wrapper's `data` / colors.
      const count = Math.max(fields.length, Array.isArray(userSeries) ? userSeries.length : 0)
      const mergedSeries = Array.isArray(userSeries)
        ? Array.from({ length: count }, (_, i) => {
            const s = series[i] ?? {
              type: 'bar',
              barMaxWidth: 32,
              itemStyle: {
                color: chartColors.value[i % chartColors.value.length],
              },
            }
            const u = userSeries[i] ?? {}
            const isStacked = Boolean(props.stacked || s.stack || u?.stack || hasUserStack)
    
            return {
              ...s,
              ...u,
              itemStyle: {
                ...s.itemStyle,
                borderRadius: defaultRadius,
                ...(isStacked && stackGap > 0
                  ? {
                      borderColor: stackGapColor,
                      borderWidth: stackGap,
                    }
                  : {}),
                ...(u.itemStyle ?? {}),
              },
            }
          })
        : series
    
      // Single-series charts hide the legend explicitly so ECharts' default
      // doesn't dump the y-field name onto the chart canvas.
      const baseLegend: any =
        fields.length > 1
          ? {
              bottom: 0,
              icon: 'circle',
              itemWidth: 8,
              itemHeight: 8,
              textStyle: { fontSize: 11, color: chartTextColor.value },
            }
          : { show: false }
    
      return {
        color: chartColors.value,
        grid: mergeOptionBlock(
          { left: 16, right: 16, top: 24, bottom: fields.length > 1 ? 32 : 24, containLabel: true },
          userGrid,
        ),
        tooltip: mergeOptionBlock(
          {
            trigger: 'axis',
            backgroundColor: chartTooltipBg.value,
            borderColor: chartTooltipBorder.value,
            textStyle: { color: chartTooltipText.value, fontSize: 12 },
          },
          userTooltip,
        ),
        legend: userLegend?.show === false ? undefined : mergeOptionBlock(baseLegend, userLegend),
        xAxis: mergeOptionBlock(
          {
            type: 'category',
            data: xData,
            axisLine: { lineStyle: { color: chartAxisColor.value } },
            axisLabel: { color: chartTextColor.value, fontSize: 11 },
            axisTick: { show: false },
          },
          userXAxis,
        ),
        yAxis: mergeOptionBlock(
          {
            type: 'value',
            splitLine: { lineStyle: { color: chartSplitLineColor.value } },
            axisLabel: { color: chartTextColor.value, fontSize: 11 },
            axisLine: { show: false },
            axisTick: { show: false },
          },
          userYAxis,
        ),
        series: mergedSeries,
        ...userRest,
      }
    })
    </script>
    
    <template>
      <div
        role="img"
        tabindex="0"
        :aria-label="ariaLabel || 'Chart'"
        :style="{ height: /^\d+$/.test(String(height)) ? `${height}px` : String(height) }"
        :class="cn('focus-visible:ring-ring w-full focus-visible:ring-2 focus-visible:outline-none', props.class)"
      >
        <VChart :option="mergedOption" :autoresize="true" class="size-full" />
      </div>
    </template>
    
  • app/components/ui/charts/bar-chart/index.ts0.1 kB
  • app/components/ui/charts/useChartTheme.ts8.2 kB

Raw manifest:https://uipkge.dev/r/vue/bar-chart.json