---
name: interactive-chart-builder
description: create interactive analytical charts for a Laravel Blade chatbot using vanilla JavaScript and Chart.js. Use when the user wants bar, line, pie, donut, area, scatter, heatmap, candlestick, funnel, radar, or switchable chart visualizations inside a custom ChatGPT-like chatbot UI powered by Laravel, Blade templates, Marked.js markdown rendering, and Azure OpenAI/Gemini responses.
---

# Interactive Chart Builder

## Purpose

Generate production-ready chart instructions, data schemas, and frontend code patterns for a Laravel Blade chatbot that uses:

- Laravel/PHP backend services such as `AiDataAgentService.php`
- Blade templates inside a Filament admin panel ecosystem
- Vanilla JavaScript for streaming, UI interactions, and DOM updates
- Marked.js for markdown rendering
- Chart.js for interactive analytical visualizations

Use this skill to help the chatbot render dynamic, changeable chart types from AI responses or structured analytical data.

## Core Behavior

When the user asks for chatbot charts, always produce a solution that fits a Laravel Blade + vanilla JavaScript + Chart.js stack unless they explicitly specify another stack.

Prioritize:

1. Clean JSON chart payloads from the AI/backend
2. Safe parsing and validation in JavaScript
3. Reusable chart renderer functions
4. Runtime chart-type switching
5. Progressive enhancement inside markdown-rendered chatbot messages
6. Compatibility with Blade templates and Filament admin pages

Do not assume React, Vue, Angular, or TypeScript unless the user asks for them.

## Supported Chart Types

Support these chart types:

- `bar`
- `line`
- `pie`
- `donut`
- `area`
- `scatter`
- `heatmap`
- `candlestick`
- `funnel`
- `radar`

Use Chart.js native chart types where available.

For non-native Chart.js charts:

- `donut`: use Chart.js `doughnut`
- `area`: use Chart.js `line` with `fill: true`
- `heatmap`: use a matrix/heatmap plugin or fallback to grouped rectangles/table-style canvas rendering
- `candlestick`: use a financial chart plugin such as `chartjs-chart-financial`
- `funnel`: use a funnel plugin or fallback to a horizontal bar chart sorted descending

Always mention plugin requirements when a requested chart type is not supported by Chart.js core.

```json
{
  "type": "chart",
  "chartId": "unique_id",
  "title": "Descriptive Business Title",
  "defaultChartType": "line",
  "labels": ["Point A", "Point B"],
  "datasets": [
    {
      "label": "Metric Name",
      "data": [10, 20]
    }
  ]
}
```

**CRITICAL RULE**: You MUST use `labels` and `datasets`. Never use `data` (at top level), `values`, or `metrics`.
**FULL TREND MANDATE**: If you have a table with 10 rows, the chart MUST have 10 labels and 10 data points. Do NOT summarize or truncate the chart data.

For scatter charts:

```json
{
  "type": "chart",
  "chartId": "revenue_vs_users",
  "defaultChartType": "scatter",
  "allowedChartTypes": ["scatter", "line"],
  "title": "Revenue vs Users",
  "datasets": [
    {
      "label": "Accounts",
      "data": [
        { "x": 100, "y": 5000 },
        { "x": 250, "y": 12000 }
      ]
    }
  ]
}
```

For candlestick charts:

```json
{
  "type": "chart",
  "chartId": "stock_prices",
  "defaultChartType": "candlestick",
  "allowedChartTypes": ["candlestick", "line"],
  "title": "Price Movement",
  "datasets": [
    {
      "label": "Price",
      "data": [
        { "x": "2026-04-01", "o": 100, "h": 120, "l": 95, "c": 115 }
      ]
    }
  ]
}
```

For heatmap charts:

```json
{
  "type": "chart",
  "chartId": "hourly_usage",
  "defaultChartType": "heatmap",
  "allowedChartTypes": ["heatmap", "bar"],
  "title": "Hourly Usage Heatmap",
  "datasets": [
    {
      "label": "Usage",
      "data": [
        { "x": "Mon", "y": "09:00", "v": 42 },
        { "x": "Mon", "y": "10:00", "v": 58 }
      ]
    }
  ]
}
```

## Response Format for Code Generation

When asked to generate code, structure the answer like this:

1. **Blade markup**
   - Include a chat message container
   - Include a chart placeholder or `<canvas>`
   - Include chart type switcher controls

2. **Script imports**
   - Chart.js CDN or local asset reference
   - Required plugins only when needed

3. **Vanilla JS renderer**
   - Parse chart JSON
   - Normalize chart type aliases
   - Build Chart.js config
   - Render chart
   - Destroy existing chart instance before re-rendering
   - Allow chart type switching

4. **Laravel integration notes**
   - Where to place the code in the Blade template
   - How to pass chart payloads from `AiDataAgentService.php`
   - How to render charts after Marked.js finishes markdown rendering

5. **Safety and validation**
   - Validate chart payload fields
   - Escape or sanitize user/model-provided HTML
   - Do not execute arbitrary script content from model output

## Chart Type Normalization Rules

Use these mapping rules:

```js
const chartTypeMap = {
  bar: 'bar',
  line: 'line',
  pie: 'pie',
  donut: 'doughnut',
  doughnut: 'doughnut',
  area: 'line',
  scatter: 'scatter',
  radar: 'radar',
  heatmap: 'matrix',
  candlestick: 'candlestick',
  funnel: 'bar'
};
```

For `area`, set:

```js
dataset.fill = true;
dataset.tension = 0.35;
```

For `funnel`, use a horizontal bar fallback if no funnel plugin is available:

```js
options.indexAxis = 'y';
```

For `donut`, use:

```js
type = 'doughnut';
```

## Reusable JavaScript Pattern

Prefer this renderer pattern:

```js
const activeCharts = new Map();

function normalizeChartType(type) {
  const map = {
    bar: 'bar',
    line: 'line',
    pie: 'pie',
    donut: 'doughnut',
    doughnut: 'doughnut',
    area: 'line',
    scatter: 'scatter',
    radar: 'radar',
    heatmap: 'matrix',
    candlestick: 'candlestick',
    funnel: 'bar'
  };

  return map[type] || 'bar';
}

function buildChartConfig(payload, selectedType) {
  const originalType = selectedType || payload.defaultChartType || 'bar';
  const chartType = normalizeChartType(originalType);

  const datasets = (payload.datasets || []).map(dataset => {
    const cloned = { ...dataset };

    if (originalType === 'area') {
      cloned.fill = true;
      cloned.tension = 0.35;
    }

    return cloned;
  });

  const options = {
    responsive: true,
    maintainAspectRatio: false,
    plugins: {
      title: {
        display: Boolean(payload.title),
        text: payload.title || ''
      },
      legend: {
        display: true
      },
      tooltip: {
        enabled: true
      }
    },
    scales: {}
  };

  if (originalType === 'funnel') {
    options.indexAxis = 'y';
  }

  if (!['pie', 'doughnut', 'radar'].includes(chartType)) {
    options.scales = {
      x: {
        title: {
          display: Boolean(payload.options?.xLabel),
          text: payload.options?.xLabel || ''
        }
      },
      y: {
        beginAtZero: true,
        title: {
          display: Boolean(payload.options?.yLabel),
          text: payload.options?.yLabel || ''
        }
      }
    };
  }

  return {
    type: chartType,
    data: {
      labels: payload.labels || [],
      datasets
    },
    options
  };
}

function renderInteractiveChart(container, payload) {
  if (!container || !payload || payload.type !== 'chart') return;

  const chartId = payload.chartId || `chart_${Date.now()}`;
  container.innerHTML = `
    <div class="ai-chart-card" data-chart-id="${chartId}">
      <div class="ai-chart-header">
        <div>
          <h3>${escapeHtml(payload.title || 'Chart')}</h3>
          ${payload.description ? `<p>${escapeHtml(payload.description)}</p>` : ''}
        </div>
        <select class="ai-chart-type-select"></select>
      </div>
      <div class="ai-chart-canvas-wrap">
        <canvas></canvas>
      </div>
    </div>
  `;

  const select = container.querySelector('.ai-chart-type-select');
  const canvas = container.querySelector('canvas');
  const allowedTypes = payload.allowedChartTypes || [payload.defaultChartType || 'bar'];

  allowedTypes.forEach(type => {
    const option = document.createElement('option');
    option.value = type;
    option.textContent = type.toUpperCase();
    if (type === payload.defaultChartType) option.selected = true;
    select.appendChild(option);
  });

  function draw(type) {
    if (activeCharts.has(chartId)) {
      activeCharts.get(chartId).destroy();
      activeCharts.delete(chartId);
    }

    const config = buildChartConfig(payload, type);
    const chart = new Chart(canvas, config);
    activeCharts.set(chartId, chart);
  }

  select.addEventListener('change', event => {
    draw(event.target.value);
  });

  draw(payload.defaultChartType || allowedTypes[0] || 'bar');
}

function escapeHtml(value) {
  return String(value)
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
    .replaceAll("'", '&#039;');
}
```

## Blade Integration Pattern

Use this kind of structure inside the Blade chatbot message template:

```html
<div class="chat-message assistant">
  <div class="markdown-content" data-message-content></div>
  <div class="chart-slot" data-chart-container></div>
</div>
```

After streaming completes and markdown is rendered through Marked.js:

```js
function renderAssistantMessage(messageElement, markdownText, chartPayloads = []) {
  const markdownTarget = messageElement.querySelector('[data-message-content]');
  markdownTarget.innerHTML = marked.parse(markdownText || '');

  chartPayloads.forEach(payload => {
    const chartContainer = document.createElement('div');
    chartContainer.className = 'chart-slot';
    messageElement.appendChild(chartContainer);
    renderInteractiveChart(chartContainer, payload);
  });
}
```

## Backend Integration Guidance

When working with `AiDataAgentService.php`, recommend returning a structured response with separate text and chart payloads:

```php
return [
    'message' => $assistantText,
    'charts' => [
        [
            'type' => 'chart',
            'chartId' => 'sales_by_month',
            'defaultChartType' => 'bar',
            'allowedChartTypes' => ['bar', 'line', 'area', 'pie', 'donut'],
            'title' => 'Sales by Month',
            'labels' => ['Jan', 'Feb', 'Mar'],
            'datasets' => [
                [
                    'label' => 'Sales',
                    'data' => [120, 190, 300],
                ],
            ],
        ],
    ],
];
```

Avoid embedding executable JavaScript directly in AI responses. The model should return chart JSON only. The frontend should own all rendering logic.

## Styling Pattern

Use CSS that fits a ChatGPT-like custom interface:

```css
.ai-chart-card {
  margin-top: 14px;
  border: 1px solid rgba(148, 163, 184, 0.25);
  border-radius: 16px;
  background: #ffffff;
  padding: 16px;
  box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
}

.ai-chart-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 12px;
}

.ai-chart-header h3 {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
}

.ai-chart-header p {
  margin: 4px 0 0;
  font-size: 13px;
  color: #64748b;
}

.ai-chart-type-select {
  border: 1px solid #cbd5e1;
  border-radius: 10px;
  padding: 6px 10px;
  font-size: 13px;
  background: #fff;
}

.ai-chart-canvas-wrap {
  position: relative;
  height: 340px;
  width: 100%;
}
```

## Decision Rules

When the user asks for a chart:

- If the data is categorical comparison, default to `bar`.
- If the data is time-series, default to `line` or `area`.
- If the data is part-to-whole and has few categories, default to `pie` or `donut`.
- If the data has x/y numeric pairs, default to `scatter`.
- If the data has matrix-like x/y/value points, default to `heatmap`.
- If the data has OHLC values, default to `candlestick`.
- If the data is stage drop-off, default to `funnel`.
- If the data compares multiple dimensions across entities, default to `radar`.

Always include `allowedChartTypes` so the user can switch chart views when the data shape supports multiple visualizations.

## Validation Checklist

Before finalizing generated code or instructions, check:

- Chart.js is loaded before `renderInteractiveChart` runs.
- Each chart container has a unique chart ID.
- Existing chart instances are destroyed before re-rendering.
- Markdown rendering and chart rendering are separate.
- AI responses contain JSON data, not executable scripts.
- Unsupported chart types clearly state required plugins or fallback behavior.
- The code works with vanilla JS and Blade without requiring React/Vue.
- User-facing labels and titles are escaped before insertion into HTML.
- Chart data is validated before rendering.

## Common Output Templates

### Full implementation request

Return:

```markdown
## Blade HTML

[code]

## CSS

[code]

## JavaScript

[code]

## Laravel response shape

[code]

## Notes

[plugin requirements, fallbacks, and integration details]
```

### Debugging request

Return:

```markdown
## Problem

[what is wrong]

## Cause

[why it happens]

## Fix

[exact code change]

## Corrected code

[code]
```

### Adding a new chart type

Return:

```markdown
## Chart type

[name]

## Required dependency

[Chart.js core or plugin]

## Payload format

[JSON example]

## Renderer changes

[JS changes]

## Fallback

[what to render if plugin is unavailable]
```

## Important Constraints

- Do not recommend React components.
- Do not use jQuery unless the user explicitly asks.
- Do not put raw script tags inside AI-generated assistant messages.
- Do not trust AI-generated HTML without sanitization.
- Keep the chart renderer reusable and independent from a specific Filament page.
- Prefer data-driven rendering over hardcoded chart configuration.
- Make chart type switching client-side whenever the same dataset can support multiple chart types.
