{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "analytics-overview",
  "title": "Analytics Overview",
  "type": "registry:block",
  "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"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/analytics-overview/analytics-overview.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n// Side-effect import: registers <uip-raw-chart> (used for the drill-down pane)\n// for standalone use — and is what the registry build scans for dependencies.\nimport '../../components/charts/raw-chart'\n\n// Two-pane analytics dashboard: a combo chart (stacked bars with a dot\n// overlay on a dual y-axis) sits above an interactive drill-down line chart.\n// Click a bar in the top pane to filter the bottom pane to that route.\n\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  { id: 'r1', label: 'NODE A → NODE B', volumes: { ALPHA: 90, BETA: 25, GAMMA: 18, DELTA: 12, EPSILON: 8 }, overlay: 12 },\n  { id: 'r2', label: 'NODE C → NODE D', volumes: { ALPHA: 4, BETA: 100, GAMMA: 14, DELTA: 12, EPSILON: 24 }, overlay: 14.7 },\n  { id: 'r3', label: 'NODE E → NODE F', volumes: { ALPHA: 14, BETA: 38, GAMMA: 18, DELTA: 32, EPSILON: 28 }, overlay: 8 },\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  { id: 'r10', label: 'NODE S → NODE T', volumes: { ALPHA: 60, BETA: 30, GAMMA: 0, DELTA: 0, EPSILON: 0 }, overlay: 23 },\n]\n\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],\n}\n\ninterface BlockTheme {\n  colors: string[]\n  textColor: string\n  axisColor: string\n  splitLineColor: string\n  tooltipBg: string\n  tooltipBorder: string\n  tooltipText: string\n}\n\nconst FALLBACK_THEME: BlockTheme = {\n  colors: ['#888888', '#888888', '#888888', '#888888', '#888888'],\n  textColor: '#888888',\n  axisColor: '#e5e5e5',\n  splitLineColor: '#f0f0f0',\n  tooltipBg: 'rgba(255,255,255,0.96)',\n  tooltipBorder: '#e5e5e5',\n  tooltipText: '#111111',\n}\n\nfunction readTheme(): BlockTheme {\n  if (typeof window === 'undefined' || typeof getComputedStyle === 'undefined') return FALLBACK_THEME\n  const cs = getComputedStyle(document.documentElement)\n  const tok = (name: string, fallback: string) => cs?.getPropertyValue(name).trim() || fallback\n  return {\n    colors: [\n      tok('--chart-1', FALLBACK_THEME.colors[0]!),\n      tok('--chart-2', FALLBACK_THEME.colors[1]!),\n      tok('--chart-3', FALLBACK_THEME.colors[2]!),\n      tok('--chart-4', FALLBACK_THEME.colors[3]!),\n      tok('--chart-5', FALLBACK_THEME.colors[4]!),\n    ],\n    textColor: tok('--muted-foreground', FALLBACK_THEME.textColor),\n    axisColor: tok('--border', FALLBACK_THEME.axisColor),\n    splitLineColor: tok('--border', FALLBACK_THEME.splitLineColor),\n    tooltipBg: tok('--popover', FALLBACK_THEME.tooltipBg),\n    tooltipBorder: tok('--border', FALLBACK_THEME.tooltipBorder),\n    tooltipText: tok('--popover-foreground', FALLBACK_THEME.tooltipText),\n  }\n}\n\n/**\n * <uip-analytics-overview> — the registry AnalyticsOverview block as a web component.\n *\n * Datasets, option builders, and classes are React's verbatim. React state\n * becomes { state: true } fields. The drill-down pane renders <uip-raw-chart>;\n * the overview combo (stacked bars + scatter overlay on a dual y-axis, outside\n * the opinionated wrappers' contract — React's documented escape hatch) is\n * rendered with echarts lazily imported in firstUpdated, so the element stays\n * SSR-safe. No props, no events. (React's metric/view selects are kept\n * verbatim even though the source never feeds them into the options.)\n */\nexport class UipAnalyticsOverview extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    overviewMetric: { state: true },\n    activeRoute: { state: true },\n    drillView: { state: true },\n    theme: { state: true },\n  }\n\n  private overviewMetric: 'overlay' | 'volume' = 'overlay'\n  private activeRoute: string | null = null\n  private drillView: 'Month' | 'Quarter' = 'Month'\n  private theme: BlockTheme = FALLBACK_THEME\n  private overviewChart: any = null\n  private resizeObserver?: ResizeObserver\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'analytics-overview')\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.resizeObserver?.disconnect()\n    this.resizeObserver = undefined\n    try {\n      this.overviewChart?.dispose()\n    } catch {\n      /* noop */\n    }\n    this.overviewChart = null\n  }\n\n  protected async firstUpdated() {\n    this.theme = readTheme()\n    const [core, renderers, charts, components] = await Promise.all([\n      import('echarts/core'),\n      import('echarts/renderers'),\n      import('echarts/charts'),\n      import('echarts/components'),\n    ])\n    core.use([\n      renderers.CanvasRenderer,\n      charts.BarChart,\n      charts.ScatterChart,\n      charts.LineChart,\n      components.GridComponent,\n      components.TooltipComponent,\n      components.LegendComponent,\n    ])\n    const el = this.renderRoot.querySelector<HTMLDivElement>('[data-overview-chart]')\n    if (!this.isConnected || !el) return\n    this.overviewChart = core.init(el)\n    this.overviewChart.setOption(this.buildOverviewOption(), { notMerge: true, lazyUpdate: true })\n    this.overviewChart.on('click', (params: any) => {\n      if (!params?.name) return\n      const r = routes.find((rt) => rt.label === params.name)\n      if (r) this.activeRoute = r.id\n    })\n    if (typeof ResizeObserver !== 'undefined') {\n      this.resizeObserver = new ResizeObserver(() => this.overviewChart?.resize())\n      this.resizeObserver.observe(el)\n    }\n  }\n\n  protected updated(changed: Map<string, unknown>) {\n    if (!this.overviewChart) return\n    if (changed.has('theme')) {\n      this.overviewChart.setOption(this.buildOverviewOption(), { notMerge: true, lazyUpdate: true })\n      return\n    }\n    // Re-read tokens after any update so a light/dark flip repaints the\n    // hand-rolled pane (the drill pane repaints itself via uip-raw-chart).\n    const sig = JSON.stringify(readTheme())\n    if (sig !== JSON.stringify(this.theme)) this.theme = JSON.parse(sig) as BlockTheme\n  }\n\n  private buildOverviewOption() {\n    const theme = this.theme\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    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  private buildDrillOption() {\n    const theme = this.theme\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    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  render() {\n    const activeLabel = routes.find((r) => r.id === this.activeRoute)?.label ?? null\n    return html`\n      <div data-slot=\"analytics-overview\" class=\"space-y-6\">\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\n                per route.\n              </p>\n            </div>\n            <div class=\"text-muted-foreground text-xs\">\n              <span>Performance Metric:</span>\n              <select\n                class=\"border-border text-foreground ml-2 rounded-md border bg-transparent px-2 py-1 font-medium\"\n                .value=${this.overviewMetric}\n                @change=${(e: Event) => {\n                  this.overviewMetric = (e.target as HTMLSelectElement).value as 'overlay' | 'volume'\n                }}\n              >\n                <option value=\"overlay\">Avg metric</option>\n                <option value=\"volume\">Total volume</option>\n              </select>\n            </div>\n          </div>\n          <div\n            data-overview-chart\n            role=\"img\"\n            tabindex=\"0\"\n            style=\"height:320px\"\n            class=\"focus-visible:ring-ring w-full focus-visible:ring-2 focus-visible:outline-none\"\n          ></div>\n          <p class=\"text-muted-foreground mt-2 text-xs\">Click a bar to drill into that route&apos;s monthly trend.</p>\n        </section>\n\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                ${activeLabel\n                  ? html`<span class=\"text-muted-foreground font-normal\">Drill-down:</span>\n                      <span class=\"text-foreground ml-1\">${activeLabel}</span>`\n                  : html`<span class=\"text-muted-foreground font-normal\">Select a route above to drill in</span>`}\n              </h3>\n              ${this.activeRoute\n                ? html`<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=${() => {\n                      this.activeRoute = null\n                    }}\n                  >\n                    ← Back to Overview\n                  </button>`\n                : nothing}\n            </div>\n            <div class=\"text-muted-foreground text-xs\">\n              <span>View By:</span>\n              <select\n                class=\"border-border text-foreground ml-2 rounded-md border bg-transparent px-2 py-1 font-medium\"\n                .value=${this.drillView}\n                @change=${(e: Event) => {\n                  this.drillView = (e.target as HTMLSelectElement).value as 'Month' | 'Quarter'\n                }}\n              >\n                <option value=\"Month\">Month</option>\n                <option value=\"Quarter\">Quarter</option>\n              </select>\n            </div>\n          </div>\n          ${this.activeRoute\n            ? html`<div\n                role=\"img\"\n                tabindex=\"0\"\n                style=\"height:280px\"\n                class=\"focus-visible:ring-ring w-full focus-visible:ring-2 focus-visible:outline-none\"\n              >\n                <uip-raw-chart .option=${this.buildDrillOption()} height=\"280\"></uip-raw-chart>\n              </div>`\n            : html`<div class=\"text-muted-foreground border-border grid h-[260px] place-items-center rounded-md border border-dashed text-xs\">\n                No route selected. Click a bar above.\n              </div>`}\n        </section>\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-analytics-overview') || customElements.define('uip-analytics-overview', UipAnalyticsOverview)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-analytics-overview': UipAnalyticsOverview\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/analytics-overview/analytics-overview.ts"
    }
  ],
  "dependencies": [
    "echarts",
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json",
    "https://uipkge.dev/r/lit/raw-chart.json"
  ]
}
