{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "analytics-overview",
  "title": "Analytics Overview",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/analytics-overview/analytics-overview.component.ts",
      "content": "// Two-pane analytics dashboard: a combo chart (stacked bars with a\n// dot overlay on a dual y-axis) sits above an interactive drill-down\n// line chart. Click a bar in the top pane to filter the bottom pane\n// to that route. The block bakes in a realistic dummy dataset; swap\n// `routes`, `partners`, and `drillSeries` with your own to repoint.\n//\n// Built on raw ECharts rather than `ui-bar-chart` because combo charts\n// (bar + scatter + dual y-axes) sit outside the opinionated wrapper's\n// single-series-type contract -- this is the documented escape hatch.\n// We register the chart types this block needs at module load (mirroring\n// the wrappers in `@/ui/charts`) and read palette tokens from\n// `use-chart-theme` so the colours stay in sync with the rest of the registry.\nimport {\n  Component,\n  DestroyRef,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  Input,\n  Output,\n  afterRenderEffect,\n  computed,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { init, use, type ECElementEvent, type EChartsCoreOption, type EChartsType } from 'echarts/core'\nimport { CanvasRenderer } from 'echarts/renderers'\nimport { BarChart as EBar, ScatterChart as EScatter, LineChart as ELine } from 'echarts/charts'\nimport { GridComponent, TooltipComponent, LegendComponent, MarkLineComponent } from 'echarts/components'\nimport {\n  getChartAxisColor,\n  getChartColors,\n  getChartSplitLineColor,\n  getChartTextColor,\n  getChartTooltipBg,\n  getChartTooltipBorder,\n  getChartTooltipText,\n  onChartThemeChange,\n} from '@/ui/charts/use-chart-theme'\n\nuse([CanvasRenderer, EBar, EScatter, ELine, GridComponent, TooltipComponent, LegendComponent, MarkLineComponent])\n\n// Each route declares the per-partner volume (stack segments) + the\n// avg overlay value (dot on the right y-axis).\nconst partners = ['ALPHA', 'BETA', 'GAMMA', 'DELTA', 'EPSILON'] as const\ntype Partner = (typeof partners)[number]\n\ninterface Route {\n  id: string\n  label: string\n  volumes: Record<Partner, number>\n  overlay: number\n}\n\nconst routes: Route[] = [\n  {\n    id: 'r1',\n    label: 'NODE A → NODE B',\n    volumes: { ALPHA: 90, BETA: 25, GAMMA: 18, DELTA: 12, EPSILON: 8 },\n    overlay: 12,\n  },\n  {\n    id: 'r2',\n    label: 'NODE C → NODE D',\n    volumes: { ALPHA: 4, BETA: 100, GAMMA: 14, DELTA: 12, EPSILON: 24 },\n    overlay: 14.7,\n  },\n  {\n    id: 'r3',\n    label: 'NODE E → NODE F',\n    volumes: { ALPHA: 14, BETA: 38, GAMMA: 18, DELTA: 32, EPSILON: 28 },\n    overlay: 8,\n  },\n  { id: 'r4', label: 'NODE G → NODE H', volumes: { ALPHA: 130, BETA: 0, GAMMA: 0, DELTA: 0, EPSILON: 0 }, overlay: 6 },\n  { id: 'r5', label: 'NODE I → NODE J', volumes: { ALPHA: 0, BETA: 0, GAMMA: 60, DELTA: 0, EPSILON: 70 }, overlay: 17 },\n  { id: 'r6', label: 'NODE K → NODE L', volumes: { ALPHA: 60, BETA: 50, GAMMA: 16, DELTA: 0, EPSILON: 0 }, overlay: 8 },\n  { id: 'r7', label: 'NODE M → NODE N', volumes: { ALPHA: 70, BETA: 0, GAMMA: 0, DELTA: 0, EPSILON: 0 }, overlay: 22 },\n  { id: 'r8', label: 'NODE O → NODE P', volumes: { ALPHA: 0, BETA: 0, GAMMA: 0, DELTA: 0, EPSILON: 110 }, overlay: 29 },\n  { id: 'r9', label: 'NODE Q → NODE R', volumes: { ALPHA: 40, BETA: 0, GAMMA: 60, DELTA: 0, EPSILON: 0 }, overlay: 19 },\n  {\n    id: 'r10',\n    label: 'NODE S → NODE T',\n    volumes: { ALPHA: 60, BETA: 30, GAMMA: 0, DELTA: 0, EPSILON: 0 },\n    overlay: 23,\n  },\n]\n\n// Drill-down monthly series for a few partners. In a real app you'd\n// fetch this per-route on selection; the dummy block shows the same\n// shape regardless of which route is active.\nconst drillMonths = ['Mar 2026', 'Apr 2026', 'May 2026']\nconst drillSeries: Record<string, number[]> = {\n  ALPHA: [22, 25, 20],\n  GAMMA: [19, 17, 18],\n  EPSILON: [-1, 29, -1], // sparse point — left out of line by NaN-like handling\n}\n\nexport type AnalyticsOverviewMetric = 'overlay' | 'volume'\nexport type AnalyticsOverviewDrillView = 'Month' | 'Quarter'\n\n/** The resolved chart theme tokens (canvas-safe colours), re-read on every theme flip. */\nfunction chartTheme() {\n  return {\n    colors: getChartColors(),\n    textColor: getChartTextColor(),\n    axisColor: getChartAxisColor(),\n    splitLineColor: getChartSplitLineColor(),\n    tooltipBg: getChartTooltipBg(),\n    tooltipBorder: getChartTooltipBorder(),\n    tooltipText: getChartTooltipText(),\n  }\n}\n\n/**\n * Mounts one ECharts instance on its host element (what `echarts-for-react` does for the\n * React block): applies the option with `notMerge`, resizes with the container, re-emits\n * ECharts `click` events and disposes the instance when the host leaves the DOM.\n */\n@Directive({ selector: '[uiAnalyticsOverviewChart]', standalone: true })\nexport class UiAnalyticsOverviewChartDirective {\n  /** The full ECharts option, applied with `notMerge` whenever it changes. */\n  @Input({ alias: 'uiAnalyticsOverviewChart', required: true })\n  set option(value: EChartsCoreOption) {\n    this.optionState.set(value)\n  }\n  /** ECharts `click` events (a bar, a dot, a line point). */\n  @Output() readonly chartClick = new EventEmitter<ECElementEvent>()\n\n  private readonly optionState = signal<EChartsCoreOption | null>(null)\n\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  private chart: EChartsType | null = null\n  private resizeObserver: ResizeObserver | null = null\n\n  constructor() {\n    afterRenderEffect(() => {\n      const option = this.optionState()\n      if (!option) return\n      if (!this.chart) {\n        this.chart = init(this.el)\n        this.chart.on('click', (params) => this.chartClick.emit(params as ECElementEvent))\n        if (typeof ResizeObserver !== 'undefined') {\n          this.resizeObserver = new ResizeObserver(() => this.chart?.resize())\n          this.resizeObserver.observe(this.el)\n        }\n      }\n      this.chart.setOption(option, { notMerge: true, lazyUpdate: true })\n    })\n    inject(DestroyRef).onDestroy(() => {\n      this.resizeObserver?.disconnect()\n      this.chart?.dispose()\n      this.chart = null\n    })\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-analytics-overview, [ui-analytics-overview]',\n  standalone: true,\n  host: { class: 'block' },\n  imports: [UiAnalyticsOverviewChartDirective],\n  template: `\n    <div data-slot=\"analytics-overview\" class=\"space-y-6\">\n      <!-- Overview pane -->\n      <section class=\"bg-card border-border rounded-lg border p-5\">\n        <div class=\"mb-3 flex items-start justify-between gap-4\">\n          <div>\n            <h2 class=\"text-sm font-semibold tracking-tight text-[var(--accent-tint)]\">Overview</h2>\n            <p class=\"text-muted-foreground mt-1 text-xs\">\n              <span class=\"text-foreground font-medium\">Metric</span> measures the avg performance signal aggregated per\n              route.\n            </p>\n          </div>\n          <div class=\"text-muted-foreground text-xs\">\n            <span>Performance Metric:</span>\n            <select\n              [value]=\"overviewMetric()\"\n              (change)=\"setOverviewMetric($event)\"\n              class=\"border-border text-foreground ml-2 rounded-md border bg-transparent px-2 py-1 font-medium\"\n            >\n              <option value=\"overlay\">Avg metric</option>\n              <option value=\"volume\">Total volume</option>\n            </select>\n          </div>\n        </div>\n        <div\n          role=\"img\"\n          tabindex=\"0\"\n          class=\"focus-visible:ring-ring h-[320px] w-full focus-visible:ring-2 focus-visible:outline-none\"\n        >\n          <div\n            class=\"size-full\"\n            [uiAnalyticsOverviewChart]=\"overviewOption()\"\n            (chartClick)=\"onOverviewClick($event)\"\n          ></div>\n        </div>\n        <p class=\"text-muted-foreground mt-2 text-xs\">Click a bar to drill into that route's monthly trend.</p>\n      </section>\n\n      <!-- Drill-down pane -->\n      <section class=\"bg-card border-border rounded-lg border p-5\">\n        <div class=\"mb-3 flex items-center justify-between gap-4\">\n          <div class=\"flex items-center gap-3\">\n            <h3 class=\"text-sm font-semibold tracking-tight\">\n              @if (activeLabel(); as label) {\n                <span class=\"text-muted-foreground font-normal\">Drill-down:</span>\n                <span class=\"text-foreground ml-1\">{{ label }}</span>\n              } @else {\n                <span class=\"text-muted-foreground font-normal\">Select a route above to drill in</span>\n              }\n            </h3>\n            @if (activeRoute()) {\n              <button\n                type=\"button\"\n                class=\"border-border text-muted-foreground hover:text-foreground rounded-md border px-2 py-1 font-mono text-xs transition hover:border-[var(--accent-tint-soft)]\"\n                (click)=\"resetDrill()\"\n              >\n                ← Back to Overview\n              </button>\n            }\n          </div>\n          <div class=\"text-muted-foreground text-xs\">\n            <span>View By:</span>\n            <select\n              [value]=\"drillView()\"\n              (change)=\"setDrillView($event)\"\n              class=\"border-border text-foreground ml-2 rounded-md border bg-transparent px-2 py-1 font-medium\"\n            >\n              <option value=\"Month\">Month</option>\n              <option value=\"Quarter\">Quarter</option>\n            </select>\n          </div>\n        </div>\n        @if (activeRoute()) {\n          <div\n            role=\"img\"\n            tabindex=\"0\"\n            class=\"focus-visible:ring-ring h-[280px] w-full focus-visible:ring-2 focus-visible:outline-none\"\n          >\n            <div class=\"size-full\" [uiAnalyticsOverviewChart]=\"drillOption()\"></div>\n          </div>\n        } @else {\n          <div\n            class=\"text-muted-foreground border-border grid h-[260px] place-items-center rounded-md border border-dashed text-xs\"\n          >\n            No route selected. Click a bar above.\n          </div>\n        }\n      </section>\n    </div>\n  `,\n})\nexport class UiAnalyticsOverviewComponent {\n  readonly overviewMetric = signal<AnalyticsOverviewMetric>('overlay')\n  readonly activeRoute = signal<string | null>(null)\n  readonly drillView = signal<AnalyticsOverviewDrillView>('Month')\n\n  /** Bumped whenever `<html>` class/style flips, so the options re-read the theme tokens. */\n  private readonly themeTick = signal(0)\n\n  readonly overviewOption = computed<EChartsCoreOption>(() => {\n    this.themeTick()\n    const theme = chartTheme()\n    const x = routes.map((r) => r.label)\n    const stacks = partners.map((p, i) => ({\n      name: p,\n      type: 'bar' as const,\n      stack: 'vol',\n      barMaxWidth: 36,\n      emphasis: { focus: 'series' as const },\n      itemStyle: { color: theme.colors[i % theme.colors.length] },\n      data: routes.map((r) => r.volumes[p]),\n    }))\n    const overlay = {\n      name: 'Avg metric',\n      type: 'scatter' as const,\n      yAxisIndex: 1,\n      symbolSize: 12,\n      itemStyle: { color: theme.colors[2], borderColor: theme.tooltipBg, borderWidth: 2 },\n      data: routes.map((r) => r.overlay),\n    }\n\n    return {\n      color: theme.colors,\n      tooltip: {\n        trigger: 'axis',\n        axisPointer: { type: 'shadow' },\n        backgroundColor: theme.tooltipBg,\n        borderColor: theme.tooltipBorder,\n        textStyle: { color: theme.tooltipText, fontSize: 12 },\n      },\n      legend: {\n        top: 8,\n        icon: 'circle',\n        itemWidth: 8,\n        itemHeight: 8,\n        textStyle: { color: theme.textColor, fontSize: 11 },\n      },\n      grid: { left: 56, right: 56, top: 42, bottom: 72, containLabel: false },\n      xAxis: {\n        type: 'category' as const,\n        data: x,\n        axisLine: { lineStyle: { color: theme.axisColor } },\n        axisLabel: {\n          interval: 0,\n          rotate: 35,\n          fontSize: 10,\n          color: theme.textColor,\n          formatter: (val: string) => val.replace(' → ', '\\n'),\n        },\n        axisTick: { show: false },\n      },\n      yAxis: [\n        {\n          type: 'value' as const,\n          name: 'Volume',\n          nameTextStyle: { color: theme.textColor, fontSize: 10 },\n          splitLine: { lineStyle: { color: theme.splitLineColor } },\n          axisLabel: { color: theme.textColor, fontSize: 10 },\n        },\n        {\n          type: 'value' as const,\n          name: 'Metric',\n          nameTextStyle: { color: theme.textColor, fontSize: 10 },\n          splitLine: { show: false },\n          axisLabel: { color: theme.textColor, fontSize: 10 },\n        },\n      ],\n      series: [...stacks, overlay],\n    }\n  })\n\n  readonly drillOption = computed<EChartsCoreOption>(() => {\n    this.themeTick()\n    const theme = chartTheme()\n    const series = Object.entries(drillSeries).map(([name, values], i) => ({\n      name,\n      type: 'line' as const,\n      smooth: true,\n      symbolSize: 8,\n      connectNulls: false,\n      lineStyle: { width: 2, color: theme.colors[i % theme.colors.length] },\n      itemStyle: { color: theme.colors[i % theme.colors.length] },\n      data: values.map((v) => (v < 0 ? null : v)),\n    }))\n\n    return {\n      color: theme.colors,\n      tooltip: {\n        trigger: 'axis',\n        backgroundColor: theme.tooltipBg,\n        borderColor: theme.tooltipBorder,\n        textStyle: { color: theme.tooltipText, fontSize: 12 },\n      },\n      legend: {\n        top: 8,\n        icon: 'circle',\n        itemWidth: 8,\n        itemHeight: 8,\n        textStyle: { color: theme.textColor, fontSize: 11 },\n      },\n      grid: { left: 48, right: 24, top: 42, bottom: 48, containLabel: false },\n      xAxis: {\n        type: 'category' as const,\n        data: drillMonths,\n        boundaryGap: false,\n        axisLine: { lineStyle: { color: theme.axisColor } },\n        axisLabel: { color: theme.textColor, fontSize: 11 },\n        axisTick: { show: false },\n      },\n      yAxis: {\n        type: 'value' as const,\n        splitLine: { lineStyle: { color: theme.splitLineColor } },\n        axisLabel: { color: theme.textColor, fontSize: 11, formatter: (v: number) => `${v}d` },\n      },\n      series,\n    }\n  })\n\n  readonly activeLabel = computed(() => routes.find((r) => r.id === this.activeRoute())?.label ?? null)\n\n  constructor() {\n    const unsubscribe = onChartThemeChange(() => this.themeTick.update((n) => n + 1))\n    inject(DestroyRef).onDestroy(unsubscribe)\n  }\n\n  onOverviewClick(params: ECElementEvent | null | undefined): void {\n    if (!params?.name) return\n    const r = routes.find((rt) => rt.label === params.name)\n    if (r) this.activeRoute.set(r.id)\n  }\n\n  resetDrill(): void {\n    this.activeRoute.set(null)\n  }\n\n  setOverviewMetric(event: Event): void {\n    this.overviewMetric.set((event.target as HTMLSelectElement).value as AnalyticsOverviewMetric)\n  }\n\n  setDrillView(event: Event): void {\n    this.drillView.set((event.target as HTMLSelectElement).value as AnalyticsOverviewDrillView)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/analytics-overview/analytics-overview.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/analytics-overview/index.ts",
      "content": "export {\n  UiAnalyticsOverviewComponent,\n  UiAnalyticsOverviewChartDirective,\n  type AnalyticsOverviewMetric,\n  type AnalyticsOverviewDrillView,\n} from './analytics-overview.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/analytics-overview/index.ts"
    }
  ],
  "dependencies": [
    "echarts"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/raw-chart.json"
  ],
  "description": "Two-pane analytics dashboard: stacked bar with dot overlay on a dual y-axis above, and an interactive drill-down line chart below. Click a bar to filter the bottom pane to that route.",
  "categories": [
    "analytics",
    "dashboard"
  ]
}