Menu

Change language

GitHub repo

Line Chart

line-chartui

Line chart wrapper around Apache ECharts. Smooth/stepped/dashed lines, multi-series, point markers. Theme-aware via registry tokens.

Installation

$npx uipkge-ng@latest add line-chart
Manifest:https://uipkge.dev/r/angular/line-chart.jsonInstalls to:src/app/components/ui/charts/line-chart/

Examples

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
dataRecord<string, unknown>[][]optional
xFieldstringxoptional
yFieldstring | string[]yoptional
curve

Line interpolation. Default 'smooth'.

'smooth''linear''step''stepStart''stepEnd'
smoothoptional
stacked

Stack series cumulatively. Default false.

booleanfalseoptional
markers

Show point markers. Default true.

booleantrueoptional
dashed

Dashed stroke on all series (forecast look). Default false.

booleanfalseoptional
heightnumber | string300optional
optionRecord<string, unknown>—optional
ariaLabel

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

string—optional
classstring—optional

npm dependencies

Used by

Files installed (3)

  • src/app/components/ui/charts/line-chart/line-chart.component.ts6.9 kB
    import {
      AfterViewInit,
      Component,
      ElementRef,
      Input,
      OnChanges,
      OnDestroy,
      SimpleChanges,
      ViewChild,
      ChangeDetectionStrategy,
    } from '@angular/core'
    import type * as echarts from 'echarts/core'
    import { cn } from '@/lib/utils'
    import {
      getChartAxisColor,
      getChartColors,
      getChartSplitLineColor,
      getChartTextColor,
      getChartTooltipBg,
      getChartTooltipBorder,
      getChartTooltipText,
      mergeOptionBlock,
    } from '../use-chart-theme'
    
    export type LineCurve = 'smooth' | 'linear' | 'step' | 'stepStart' | 'stepEnd'
    
    /**
     * Angular port of the UIPKGE LineChart — ECharts wrapper with smooth/stepped/
     * dashed lines, multi-series, and point markers. Standalone, theme-aware via
     * registry tokens. Same inputs as the Vue `LineChart` (`data`, `xField`,
     * `yField`, `curve`, `stacked`, `markers`, `dashed`, `height`, `option`).
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-line-chart, [ui-line-chart]',
      standalone: true,
      host: {
        '[attr.data-slot]': '"line-chart"',
        '[attr.data-uipkge]': '""',
        '[class]': 'hostClass',
        '[attr.role]': '"img"',
        '[attr.aria-label]': 'ariaLabel || "Chart"',
        '[attr.tabindex]': '0',
        '[style.height]': 'heightStyle',
      },
      template: `<div #chartEl class="size-full"></div>`,
    })
    export class UiLineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
      @Input() data: Record<string, unknown>[] = []
      @Input() xField = 'x'
      @Input() yField: string | string[] = 'y'
      /** Line interpolation. Default 'smooth'. */
      @Input() curve: LineCurve = 'smooth'
      /** Stack series cumulatively. Default false. */
      @Input() stacked = false
      /** Show point markers. Default true. */
      @Input() markers = true
      /** Dashed stroke on all series (forecast look). Default false. */
      @Input() dashed = false
      @Input() height: number | string = 300
      @Input() option?: Record<string, unknown>
      /** Accessible name announced for the chart image. Defaults to "Chart". */
      @Input() ariaLabel?: string
      @Input('class') className?: string
    
      @ViewChild('chartEl', { static: false }) chartEl?: ElementRef<HTMLDivElement>
    
      private chart: echarts.ECharts | null = null
      private unsubscribeTheme: (() => void) | null = null
    
      get hostClass(): string {
        return cn('block focus-visible:ring-ring w-full focus-visible:ring-2 focus-visible:outline-none', this.className)
      }
    
      get heightStyle(): string {
        return /^\d+$/.test(String(this.height)) ? `${this.height}px` : String(this.height)
      }
    
      fields(): string[] {
        return Array.isArray(this.yField) ? this.yField : [this.yField]
      }
    
      /** Build the merged ECharts option (pure — no DOM needed, unit-testable). */
      getOption(): Record<string, unknown> {
        const fields = this.fields()
        const colors = getChartColors()
        const xData = this.data.map((d) => d[this.xField])
    
        const series = fields.map((field, i) => ({
          name: field,
          type: 'line',
          smooth: this.curve === 'smooth',
          step:
            this.curve === 'step'
              ? 'middle'
              : this.curve === 'stepStart'
                ? 'start'
                : this.curve === 'stepEnd'
                  ? 'end'
                  : false,
          stack: this.stacked ? 'lines' : undefined,
          symbol: this.markers ? 'circle' : 'none',
          symbolSize: 6,
          lineStyle: { width: 2, type: this.dashed ? 'dashed' : 'solid' },
          itemStyle: { color: colors[i % colors.length] },
          data: this.data.map((d) => d[field]),
        }))
    
        const userOption: Record<string, any> = this.option ?? {}
        const {
          series: userSeries,
          xAxis: userXAxis,
          yAxis: userYAxis,
          grid: userGrid,
          tooltip: userTooltip,
          legend: userLegend,
          ...userRest
        } = userOption
        const mergedSeries = Array.isArray(userSeries) ? series.map((s, i) => ({ ...s, ...(userSeries[i] ?? {}) })) : series
    
        const baseLegend =
          fields.length > 1
            ? {
                bottom: 0,
                icon: 'circle',
                itemWidth: 8,
                itemHeight: 8,
                textStyle: { fontSize: 11, color: getChartTextColor() },
              }
            : undefined
    
        return {
          color: colors,
          grid: mergeOptionBlock(
            {
              left: 16,
              right: 16,
              top: 24,
              bottom: fields.length > 1 ? 32 : 24,
              outerBoundsMode: 'same',
              outerBoundsContain: 'axisLabel',
            },
            userGrid,
          ),
          tooltip: mergeOptionBlock(
            {
              trigger: 'axis',
              backgroundColor: getChartTooltipBg(),
              borderColor: getChartTooltipBorder(),
              textStyle: { color: getChartTooltipText(), fontSize: 12 },
            },
            userTooltip,
          ),
          legend: userLegend?.show === false ? undefined : mergeOptionBlock(baseLegend ?? {}, userLegend),
          xAxis: mergeOptionBlock(
            {
              type: 'category',
              data: xData,
              axisLine: { lineStyle: { color: getChartAxisColor() } },
              axisLabel: { color: getChartTextColor(), fontSize: 11 },
              axisTick: { show: false },
            },
            userXAxis,
          ),
          yAxis: mergeOptionBlock(
            {
              type: 'value',
              splitLine: { lineStyle: { color: getChartSplitLineColor() } },
              axisLabel: { color: getChartTextColor(), fontSize: 11 },
              axisLine: { show: false },
              axisTick: { show: false },
            },
            userYAxis,
          ),
          series: mergedSeries,
          ...userRest,
        }
      }
    
      async ngAfterViewInit(): Promise<void> {
        if (typeof window === 'undefined' || !this.chartEl?.nativeElement) return
        const [{ use }, { CanvasRenderer }, { LineChart }, comps] = await Promise.all([
          import('echarts/core'),
          import('echarts/renderers'),
          import('echarts/charts'),
          import('echarts/components'),
        ])
        use([
          CanvasRenderer,
          LineChart,
          comps.GridComponent,
          comps.TooltipComponent,
          comps.LegendComponent,
          comps.MarkPointComponent,
          comps.MarkLineComponent,
          comps.MarkAreaComponent,
        ])
        const { init } = await import('echarts/core')
        this.chart = init(this.chartEl.nativeElement)
        this.chart.setOption(this.getOption())
        const { onChartThemeChange } = await import('../use-chart-theme')
        this.unsubscribeTheme = onChartThemeChange(() => this.chart?.setOption(this.getOption()))
        if (typeof ResizeObserver !== 'undefined') {
          new ResizeObserver(() => this.chart?.resize()).observe(this.chartEl.nativeElement)
        }
      }
    
      ngOnChanges(changes: SimpleChanges): void {
        if (
          this.chart &&
          (changes['data'] ||
            changes['option'] ||
            changes['curve'] ||
            changes['stacked'] ||
            changes['markers'] ||
            changes['dashed'])
        ) {
          this.chart.setOption(this.getOption())
        }
      }
    
      ngOnDestroy(): void {
        this.unsubscribeTheme?.()
        this.unsubscribeTheme = null
        try {
          this.chart?.dispose()
        } catch {
          /* already disposed */
        }
        this.chart = null
      }
    }
    
  • src/app/components/ui/charts/line-chart/index.ts0.1 kB
  • src/app/components/ui/charts/use-chart-theme.ts7.4 kB

Raw manifest:https://uipkge.dev/r/angular/line-chart.json