AI-ready file output for the project "satag-chart-plugin"
================================================================

----------------------------------------------------------------
FILE: .env
---
COMPOSE_PROJECT_NAME=dev-satag-chart-plugin
----------------------------------------------------------------

----------------------------------------------------------------
FILE: README.md
---
# chart plugin
----------------------------------------------------------------

----------------------------------------------------------------
FILE: node/render-chart.js
---
// node/render-chart.js
// Liest Chart.js-Konfiguration als JSON von stdin.
// Argumente: --width=800 --height=400 --bg=white --out=/abs/path/to/file.png

const fs = require('fs');
const { ChartJSNodeCanvas } = require('chartjs-node-canvas');

function arg(name, fallback) {
    const m = process.argv.find(a => a.startsWith(`--${name}=`));
    return m ? m.split('=').slice(1).join('=') : fallback;
}

(async () => {
    try {
        const width = parseInt(arg('width', '800'), 10);
        const height = parseInt(arg('height', '400'), 10);
        const backgroundColour = arg('bg', 'white');
        const out = arg('out', null);

        if (!out) {
            console.error('Missing --out argument');
            process.exit(2);
        }

        const chunks = [];
        for await (const chunk of process.stdin) chunks.push(chunk);
        const json = Buffer.concat(chunks).toString('utf8');
        const configuration = JSON.parse(json);

        // Wichtig: responsive false, feste Größe (falls nicht gesetzt)
        configuration.options = configuration.options || {};
        if (configuration.options.responsive !== false) {
            configuration.options.responsive = false;
        }

        const canvas = new ChartJSNodeCanvas({ width, height, backgroundColour });
        const buffer = await canvas.renderToBuffer(configuration, 'image/png');
        fs.writeFileSync(out, buffer);
        process.exit(0);
    } catch (err) {
        console.error(err?.stack || String(err));
        process.exit(1);
    }
})();

----------------------------------------------------------------

----------------------------------------------------------------
FILE: package.json
---
{
  "name": "satag-chart-plugin",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "directories": {
    "test": "tests"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "chart.js": "^4.5.1",
    "chartjs-node-canvas": "^5.0.0"
  }
}

----------------------------------------------------------------

----------------------------------------------------------------
FILE: src/Controller/ChartController.php
---
<?php

declare(strict_types=1);

namespace SatagChartPlugin\Controller;

use SatagChartPlugin\Service\ChartRenderer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Annotation\Route;

class ChartController extends AbstractController
{
    public function __construct(private readonly ChartRenderer $chartRenderer) {}

    #[Route('/satag-chart-plugin/render', name: 'satag_chart_plugin_render', methods: ['POST'])]
    public function renderFromJson(Request $request): JsonResponse
    {
        // Erwarteter Body:
        // {
        //   "config": { ... Chart.js Config ... },
        //   "width": 1200,
        //   "height": 600,
        //   "background": "white",
        //   "fileName": "optional.png" // nur für Content-Disposition
        // }
        $payload = json_decode($request->getContent(), true) ?? [];
        $config = $payload['config'] ?? null;

        if (!$config || !is_array($config)) {
            return new JsonResponse(['error' => 'Missing or invalid "config"'], 400);
        }

        $width      = (int)($payload['width'] ?? 1000);
        $height     = (int)($payload['height'] ?? 500);
        $background = (string)($payload['background'] ?? 'white');
        $fileName   = (string)($payload['fileName'] ?? 'chart.png');

        // Dateinamen für Header absichern (keine Pfade/Steuerzeichen)
        $fileName = basename($fileName);
        if ($fileName === '' || strpos($fileName, "\0") !== false) {
            $fileName = 'chart.png';
        }

        try {
            // Content-Disposition VOR dem Stream setzen (Service setzt Content-Type/Length)
            header('Content-Disposition: inline; filename="' . addslashes($fileName) . '"');

            // Stream direkt an den Client; Service setzt Content-Type/Length und löscht Temp-Datei.
            $this->chartRenderer->renderAndStreamPng($config, $width, $height, $background);

            // Wichtig: danach NICHTS mehr schicken – hart beenden.
            exit;
        } catch (\Throwable $e) {
            // Falls Rendering/Streaming fehlschlägt, liefern wir JSON-Fehler zurück.
            return new JsonResponse([
                'error'  => 'Render failed',
                'detail' => $e->getMessage(),
            ], 500);
        }
    }
}

----------------------------------------------------------------

----------------------------------------------------------------
FILE: src/Controller/ChartDemoController.php
---
<?php

declare(strict_types=1);

namespace SatagChartPlugin\Controller;

use SatagChartPlugin\Service\ChartRenderer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

/**
 * Mega-Demo-Sammlung für Chart.js Renderings via ChartRenderer
 *
 * Hinweise:
 * - Alle Demos rendern synchron ein PNG (stream) mit weißem Hintergrund.
 * - Nur JSON-serielles Chart.js-Config wird verwendet (keine JS-Funktionen/Callbacks).
 * - Größe lässt sich pro Demo über width/height leicht anpassen.
 */
class ChartDemoController extends AbstractController
{
    public function __construct(private readonly ChartRenderer $chartRenderer) {}

    private function renderChart(array $config, int $width = 1000, int $height = 500, string $background = 'white'): void
    {
        $this->chartRenderer->renderAndStreamPng($config, width: $width, height: $height, background: $background);
    }

    // 1) Einfaches Liniendiagramm
    #[Route('/satag-chart-plugin/demo-line-basic', name: 'satag_chart_plugin_demo_line_basic', methods: ['GET'])]
    public function demoLineBasic(): void
    {
        $config = [
            'type' => 'line',
            'data' => [
                'labels' => ['Jan', 'Feb', 'Mrz', 'Apr', 'Mai', 'Jun'],
                'datasets' => [[
                    'label' => 'Umsatz',
                    'data' => [12, 19, 3, 5, 2, 3],
                    'tension' => 0.3,
                    'fill' => false,
                ]],
            ],
            'options' => [
                'plugins' => [
                    'legend' => ['display' => true],
                    'title' => ['display' => true, 'text' => 'Umsatz H1'],
                ],
            ],
        ];
        $this->renderChart($config);
    }

    // 2) Liniendiagramm mit Fläche (Area)
    #[Route('/satag-chart-plugin/demo-line-area', name: 'satag_chart_plugin_demo_line_area', methods: ['GET'])]
    public function demoLineArea(): void
    {
        $config = [
            'type' => 'line',
            'data' => [
                'labels' => ['Jan','Feb','Mrz','Apr','Mai','Jun','Jul','Aug','Sep','Okt','Nov','Dez'],
                'datasets' => [[
                    'label' => 'Besucher',
                    'data' => [120, 160, 180, 220, 300, 450, 400, 380, 360, 340, 320, 310],
                    'tension' => 0.25,
                    'fill' => true,
                ]],
            ],
            'options' => [
                'plugins' => [
                    'title' => ['display' => true, 'text' => 'Besucher pro Monat (Area)'],
                ],
            ],
        ];
        $this->renderChart($config);
    }

    // 3) Stepped-Line
    #[Route('/satag-chart-plugin/demo-line-stepped', name: 'satag_chart_plugin_demo_line_stepped', methods: ['GET'])]
    public function demoLineStepped(): void
    {
        $config = [
            'type' => 'line',
            'data' => [
                'labels' => ['KW1','KW2','KW3','KW4','KW5','KW6','KW7','KW8'],
                'datasets' => [[
                    'label' => 'Kapazität',
                    'data' => [20, 20, 35, 35, 35, 50, 50, 65],
                    'stepped' => true,
                    'fill' => false,
                ]],
            ],
            'options' => [
                'plugins' => [
                    'title' => ['display' => true, 'text' => 'Stepped Line'],
                ],
            ],
        ];
        $this->renderChart($config);
    }

    // 4) Multi-Line, gestapelte Fläche
    #[Route('/satag-chart-plugin/demo-line-stacked-area', name: 'satag_chart_plugin_demo_line_stacked_area', methods: ['GET'])]
    public function demoLineStackedArea(): void
    {
        $config = [
            'type' => 'line',
            'data' => [
                'labels' => ['Q1','Q2','Q3','Q4'],
                'datasets' => [
                    [ 'label' => 'Produkt A', 'data' => [120, 140, 180, 200], 'fill' => true ],
                    [ 'label' => 'Produkt B', 'data' => [80, 100, 120, 140],  'fill' => true ],
                    [ 'label' => 'Produkt C', 'data' => [50, 70, 90, 110],   'fill' => true ],
                ],
            ],
            'options' => [
                'scales' => [
                    'y' => ['stacked' => true],
                    'x' => ['stacked' => true],
                ],
                'plugins' => [
                    'title' => ['display' => true, 'text' => 'Gestapelte Umsätze nach Produkt'],
                ],
            ],
        ];
        $this->renderChart($config);
    }

    // 5) Säulendiagramm (Bar) – vertikal
    #[Route('/satag-chart-plugin/demo-bar-basic', name: 'satag_chart_plugin_demo_bar_basic', methods: ['GET'])]
    public function demoBarBasic(): void
    {
        $config = [
            'type' => 'bar',
            'data' => [
                'labels' => ['Mo','Di','Mi','Do','Fr'],
                'datasets' => [[
                    'label' => 'Tickets',
                    'data' => [5, 7, 4, 9, 6],
                ]],
            ],
            'options' => [
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Erstellte Tickets pro Tag'] ],
            ],
        ];
        $this->renderChart($config);
    }

    // 6) Balkendiagramm (horizontal)
    #[Route('/satag-chart-plugin/demo-bar-horizontal', name: 'satag_chart_plugin_demo_bar_horizontal', methods: ['GET'])]
    public function demoBarHorizontal(): void
    {
        $config = [
            'type' => 'bar',
            'data' => [
                'labels' => ['Chrome','Safari','Firefox','Edge','Sonstige'],
                'datasets' => [[ 'label' => 'Nutzer', 'data' => [62, 19, 12, 5, 2] ]],
            ],
            'options' => [
                'indexAxis' => 'y',
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Browser-Anteile (Balken)'] ],
            ],
        ];
        $this->renderChart($config);
    }

    // 7) Gestapelte Balken
    #[Route('/satag-chart-plugin/demo-bar-stacked', name: 'satag_chart_plugin_demo_bar_stacked', methods: ['GET'])]
    public function demoBarStacked(): void
    {
        $config = [
            'type' => 'bar',
            'data' => [
                'labels' => ['Jan','Feb','Mrz','Apr'],
                'datasets' => [
                    [ 'label' => 'Neu', 'data' => [30, 40, 25, 35] ],
                    [ 'label' => 'Bestehend', 'data' => [20, 25, 30, 20] ],
                ],
            ],
            'options' => [
                'scales' => [ 'x' => ['stacked' => true], 'y' => ['stacked' => true] ],
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Gestapelte Leads'] ],
            ],
        ];
        $this->renderChart($config);
    }

    // 8) Säulen mit negativen Werten
    #[Route('/satag-chart-plugin/demo-bar-negative', name: 'satag_chart_plugin_demo_bar_negative', methods: ['GET'])]
    public function demoBarNegative(): void
    {
        $config = [
            'type' => 'bar',
            'data' => [
                'labels' => ['Q1','Q2','Q3','Q4'],
                'datasets' => [[ 'label' => 'Cashflow', 'data' => [200, -50, 120, -30] ]],
            ],
            'options' => [
                'scales' => [ 'y' => ['beginAtZero' => true] ],
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Cashflow mit negativen Werten'] ],
            ],
        ];
        $this->renderChart($config);
    }

    // 9) Mixed: Säulen + Linie mit zweiter Y-Achse
    #[Route('/satag-chart-plugin/demo-mixed-bar-line-dualaxis', name: 'satag_chart_plugin_demo_mixed_bar_line_dualaxis', methods: ['GET'])]
    public function demoMixedBarLineDualAxis(): void
    {
        $config = [
            'data' => [
                'labels' => ['Jan','Feb','Mrz','Apr','Mai','Jun'],
                'datasets' => [
                    [ 'type' => 'bar',  'label' => 'Bestellungen', 'data' => [50, 40, 60, 70, 90, 100], 'yAxisID' => 'y' ],
                    [ 'type' => 'line', 'label' => 'Conversion %', 'data' => [1.2, 1.0, 1.4, 1.6, 1.8, 2.0], 'yAxisID' => 'y1', 'tension' => 0.3],
                ],
            ],
            'options' => [
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Bestellungen & Conversion'] ],
                'scales' => [
                    'y' => ['type' => 'linear', 'position' => 'left'],
                    'y1' => ['type' => 'linear', 'position' => 'right', 'grid' => ['drawOnChartArea' => false]],
                ],
            ],
        ];
        $this->renderChart($config);
    }

    // 10) Radar-Chart
    #[Route('/satag-chart-plugin/demo-radar', name: 'satag_chart_plugin_demo_radar', methods: ['GET'])]
    public function demoRadar(): void
    {
        $config = [
            'type' => 'radar',
            'data' => [
                'labels' => ['Qualität','Preis','Design','Support','Lieferzeit','Features'],
                'datasets' => [
                    [ 'label' => 'Produkt A', 'data' => [65, 59, 90, 81, 56, 55] ],
                    [ 'label' => 'Produkt B', 'data' => [28, 48, 40, 19, 96, 27] ],
                ],
            ],
            'options' => [ 'plugins' => [ 'title' => ['display' => true, 'text' => 'Produktvergleich (Radar)'] ] ],
        ];
        $this->renderChart($config);
    }

    // 11) Polar Area
    #[Route('/satag-chart-plugin/demo-polar-area', name: 'satag_chart_plugin_demo_polar_area', methods: ['GET'])]
    public function demoPolarArea(): void
    {
        $config = [
            'type' => 'polarArea',
            'data' => [
                'labels' => ['Nord','Ost','Süd','West'],
                'datasets' => [[ 'label' => 'Himmelsrichtungen', 'data' => [11, 16, 7, 3] ]],
            ],
            'options' => [ 'plugins' => [ 'title' => ['display' => true, 'text' => 'Polar Area'] ] ],
        ];
        $this->renderChart($config, 600, 600);
    }

    // 12) Pie
    #[Route('/satag-chart-plugin/demo-pie', name: 'satag_chart_plugin_demo_pie', methods: ['GET'])]
    public function demoPie(): void
    {
        $config = [
            'type' => 'pie',
            'data' => [
                'labels' => ['Europa','Amerika','Asien','Afrika'],
                'datasets' => [[ 'data' => [45, 25, 20, 10] ]],
            ],
            'options' => [ 'plugins' => [ 'title' => ['display' => true, 'text' => 'Umsatzanteile nach Region'] ] ],
        ];
        $this->renderChart($config, 700, 500);
    }

    // 13) Doughnut (mit Cutout + Offset eines Segments)
    #[Route('/satag-chart-plugin/demo-doughnut', name: 'satag_chart_plugin_demo_doughnut', methods: ['GET'])]
    public function demoDoughnut(): void
    {
        $config = [
            'type' => 'doughnut',
            'data' => [
                'labels' => ['Abo','Einmal','Add-ons'],
                'datasets' => [[
                    'data' => [60, 30, 10],
                    'offset' => [0, 20, 0],
                ]],
            ],
            'options' => [
                'cutout' => '60%',
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Umsatzmodell (Doughnut)'] ],
            ],
        ];
        $this->renderChart($config, 700, 500);
    }

    // 14) Scatter
    #[Route('/satag-chart-plugin/demo-scatter', name: 'satag_chart_plugin_demo_scatter', methods: ['GET'])]
    public function demoScatter(): void
    {
        $points = [];
        foreach ([
                     [3,7],[4,8],[5,8.2],[6,8.5],[7,8.9],[8,9.3],[9,9.6],[10,9.9],
                     [3.5,7.5],[4.5,8.1],[6.5,8.7],[7.5,9.1]
                 ] as [$x,$y]) { $points[] = ['x'=>$x,'y'=>$y]; }

        $config = [
            'type' => 'scatter',
            'data' => [ 'datasets' => [[ 'label' => 'Preis vs. Bewertung', 'data' => $points ]] ],
            'options' => [
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Scatter: Preis vs. Bewertung'] ],
                'scales' => [ 'x' => ['type' => 'linear', 'title' => ['display'=>true,'text'=>'Preis']], 'y' => ['title' => ['display'=>true,'text'=>'Bewertung']] ],
            ],
        ];
        $this->renderChart($config);
    }

    // 15) Bubble
    #[Route('/satag-chart-plugin/demo-bubble', name: 'satag_chart_plugin_demo_bubble', methods: ['GET'])]
    public function demoBubble(): void
    {
        $data = [
            ['x'=>10,'y'=>20,'r'=>10],
            ['x'=>15,'y'=>10,'r'=>15],
            ['x'=>25,'y'=>30,'r'=>8],
            ['x'=>30,'y'=>25,'r'=>12],
        ];
        $config = [
            'type' => 'bubble',
            'data' => [ 'datasets' => [[ 'label' => 'Projekte', 'data' => $data ]] ],
            'options' => [
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Bubble: Aufwand/Impact/Größe'] ],
                'scales' => [ 'x' => ['title' => ['display'=>true,'text'=>'Aufwand']], 'y' => ['title' => ['display'=>true,'text'=>'Impact']] ],
            ],
        ];
        $this->renderChart($config);
    }

    // 16) Logarithmische Skala
    #[Route('/satag-chart-plugin/demo-line-log', name: 'satag_chart_plugin_demo_line_log', methods: ['GET'])]
    public function demoLineLog(): void
    {
        $config = [
            'type' => 'line',
            'data' => [
                'labels' => ['1','10','100','1.000','10.000','100.000'],
                'datasets' => [[ 'label' => 'Messwerte', 'data' => [1, 10, 100, 1_000, 10_000, 80_000], 'fill' => false ]],
            ],
            'options' => [
                'scales' => [ 'y' => ['type' => 'logarithmic'] ],
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Logarithmische Y-Skala'] ],
            ],
        ];
        $this->renderChart($config);
    }

    // 17) Gestapeltes 100%-Diagramm (prozentuale Darstellung)
    #[Route('/satag-chart-plugin/demo-stacked-100', name: 'satag_chart_plugin_demo_stacked_100', methods: ['GET'])]
    public function demoStacked100(): void
    {
        $config = [
            'type' => 'bar',
            'data' => [
                'labels' => ['Q1','Q2','Q3','Q4'],
                'datasets' => [
                    [ 'label' => 'A', 'data' => [30, 40, 35, 45] ],
                    [ 'label' => 'B', 'data' => [50, 35, 45, 35] ],
                    [ 'label' => 'C', 'data' => [20, 25, 20, 20] ],
                ],
            ],
            'options' => [
                'scales' => [
                    'x' => ['stacked' => true],
                    'y' => [ 'stacked' => true, 'min' => 0, 'max' => 100 ]
                ],
                'plugins' => [ 'title' => ['display' => true, 'text' => '100% Gestapelt (A/B/C)'] ],
            ],
        ];
        $this->renderChart($config);
    }

    // 18) Liniendiagramm mit fehlenden Werten (Gaps)
    #[Route('/satag-chart-plugin/demo-line-gaps', name: 'satag_chart_plugin_demo_line_gaps', methods: ['GET'])]
    public function demoLineGaps(): void
    {
        $config = [
            'type' => 'line',
            'data' => [
                'labels' => ['Jan','Feb','Mrz','Apr','Mai','Jun'],
                'datasets' => [[
                    'label' => 'Sensor A',
                    'data' => [12, null, 18, 22, null, 25],
                    'spanGaps' => false,
                    'tension' => 0.2,
                ]],
            ],
            'options' => [ 'plugins' => [ 'title' => ['display' => true, 'text' => 'Gaps/Null-Werte in Linien'] ] ],
        ];
        $this->renderChart($config);
    }

    // 19) Mehrere Achsen & unterschiedliche Typen (3er-Mix)
    #[Route('/satag-chart-plugin/demo-mixed-triple', name: 'satag_chart_plugin_demo_mixed_triple', methods: ['GET'])]
    public function demoMixedTriple(): void
    {
        $config = [
            'data' => [
                'labels' => ['W1','W2','W3','W4','W5','W6'],
                'datasets' => [
                    [ 'type' => 'bar',   'label' => 'Seitenaufrufe', 'data' => [1200, 1400, 1300, 1600, 1800, 1700], 'yAxisID' => 'y' ],
                    [ 'type' => 'line',  'label' => 'Bounce %',     'data' => [55, 53, 54, 50, 49, 47],             'yAxisID' => 'y1', 'tension' => 0.3 ],
                    [ 'type' => 'scatter','label' => 'Kampagnen',  'data' => [ ['x'=>1,'y'=>54], ['x'=>3,'y'=>52], ['x'=>5,'y'=>48] ], 'yAxisID' => 'y1' ],
                ],
            ],
            'options' => [
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Traffic-Mix (Bar+Line+Scatter)'] ],
                'scales' => [
                    'y'  => ['type' => 'linear', 'position' => 'left'],
                    'y1' => ['type' => 'linear', 'position' => 'right', 'grid' => ['drawOnChartArea' => false]],
                    'x'  => ['title' => ['display'=>true,'text'=>'Woche']],
                ],
            ],
        ];
        $this->renderChart($config);
    }

    // 20) Horizontale gestapelte 100%-Balken (Likert/Umfrage)
    #[Route('/satag-chart-plugin/demo-likert', name: 'satag_chart_plugin_demo_likert', methods: ['GET'])]
    public function demoLikert(): void
    {
        $config = [
            'type' => 'bar',
            'data' => [
                'labels' => ['Produkt','Preis','Support','Lieferung'],
                'datasets' => [
                    [ 'label' => 'Stark dagegen', 'data' => [5, 10, 3, 2] ],
                    [ 'label' => 'Dagegen',       'data' => [10, 15, 6, 4] ],
                    [ 'label' => 'Neutral',       'data' => [20, 30, 15, 10] ],
                    [ 'label' => 'Dafür',         'data' => [40, 30, 50, 45] ],
                    [ 'label' => 'Stark dafür',   'data' => [25, 15, 26, 39] ],
                ],
            ],
            'options' => [
                'indexAxis' => 'y',
                'scales' => [
                    'x' => [ 'stacked' => true, 'min' => 0, 'max' => 100 ],
                    'y' => [ 'stacked' => true ],
                ],
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Likert / Umfrage (100% gestapelt)'] ],
            ],
        ];
        $this->renderChart($config, 1100, 600);
    }

    // 21) Linien mit Datapoints ausgeblendet (ruhigere Kurven)
    #[Route('/satag-chart-plugin/demo-line-no-points', name: 'satag_chart_plugin_demo_line_no_points', methods: ['GET'])]
    public function demoLineNoPoints(): void
    {
        $config = [
            'type' => 'line',
            'data' => [
                'labels' => ['1','2','3','4','5','6','7','8','9','10'],
                'datasets' => [[ 'label' => 'Latenz (ms)', 'data' => [30,28,31,27,26,29,25,24,23,22], 'pointRadius' => 0, 'tension' => 0.35 ]],
            ],
            'options' => [ 'plugins' => [ 'title' => ['display' => true, 'text' => 'Linie ohne Punkte'] ] ],
        ];
        $this->renderChart($config);
    }

    // 22) Gestapeltes Flächendiagramm mit negativer Serie (Saldo)
    #[Route('/satag-chart-plugin/demo-stacked-area-balance', name: 'satag_chart_plugin_demo_stacked_area_balance', methods: ['GET'])]
    public function demoStackedAreaBalance(): void
    {
        $config = [
            'type' => 'line',
            'data' => [
                'labels' => ['Jan','Feb','Mrz','Apr','Mai','Jun'],
                'datasets' => [
                    [ 'label' => 'Einnahmen', 'data' => [100, 120, 140, 130, 160, 170], 'fill' => true ],
                    [ 'label' => 'Ausgaben',  'data' => [-80, -90, -95, -100, -110, -120], 'fill' => true ],
                ],
            ],
            'options' => [
                'scales' => [ 'y' => ['beginAtZero' => false] ],
                'plugins' => [ 'title' => ['display' => true, 'text' => 'Saldo: Einnahmen vs. Ausgaben'] ],
            ],
        ];
        $this->renderChart($config);
    }
}

----------------------------------------------------------------

----------------------------------------------------------------
FILE: src/Resources/config/services.yml
---
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: true

  SatagChartPlugin\Controller\:
    resource: '../../Controller'
    tags: ['controller.service_arguments']

#  SatagChartPlugin\Service\DummyService: ~

#  SatagChartPlugin\Repository\:
#    resource: '../../Repository'

  SatagChartPlugin\Service\:
    resource: '../../Service'

  SatagChartPlugin\Service\ChartRenderer:
    arguments:
      $projectDir: '%kernel.project_dir%'
#twig:
#   ...
#   paths:
#     'plugins/satag-chart-plugin/src/Resources': ~
----------------------------------------------------------------

----------------------------------------------------------------
FILE: src/SatagChartPlugin.php
---
<?php declare(strict_types=1);

namespace SatagChartPlugin;

use Satag\AmicronPlatform\Core\Plugin\Plugin;

class SatagChartPlugin extends Plugin
{

}
----------------------------------------------------------------

----------------------------------------------------------------
FILE: src/Service/ChartRenderer.php
---
<?php

namespace SatagChartPlugin\Service;

use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

class ChartRenderer
{
    public function __construct(
        private readonly string $projectDir,
        private readonly string $nodeBinary = 'node',
        private readonly string $rendererRelPath = '/plugins/satag-chart-plugin/node/render-chart.js'
    ) {}

    /**
     * Rendert ein Chart als PNG und sendet es direkt per HTTP-Header/Body.
     * Achtung: Umgeht Symfony-Response. Der aufrufende Controller sollte nach dem Call NICHTS mehr senden.
     */
    public function renderAndStreamPng(
        array $chartConfig,
        int $width = 800,
        int $height = 400,
        string $background = 'white'
    ): void {
        $renderer = $this->projectDir . $this->rendererRelPath;
        if (!is_file($renderer)) {
            throw new \RuntimeException("Renderer not found at $renderer");
        }

        // saubere Temp-Datei mit .png-Suffix (ohne verwaistes tempnam-File)
        $tmpFile = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR)
            . DIRECTORY_SEPARATOR
            . 'chart_' . bin2hex(random_bytes(8)) . '.png';

        $args = [
            $this->nodeBinary,
            $renderer,
            "--width={$width}",
            "--height={$height}",
            "--bg={$background}",
            "--out={$tmpFile}",
        ];

        $process = new Process($args);
        $process->setTimeout(30);
        $process->setInput(json_encode($chartConfig, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));

        try {
            $process->run();

            if (!$process->isSuccessful()) {
                throw new ProcessFailedException($process);
            }

            if (!is_file($tmpFile) || !is_readable($tmpFile)) {
                throw new \RuntimeException('Rendered PNG not found or unreadable.');
            }

            // Falls vorab schon Output gesendet wurde, wären Header nicht mehr möglich
            if (headers_sent($file, $line)) {
                // Auf Wunsch: tmpFile dennoch löschen
                @unlink($tmpFile);
                throw new \RuntimeException("Cannot send headers; output started in $file on line $line.");
            }

            clearstatcache(true, $tmpFile);
            $size = filesize($tmpFile);
            if ($size === false) {
                $size = 0; // notfalls ohne Content-Length senden
            }

            // Pflicht-Header
            header('Content-Type: image/png');
            if ($size > 0) {
                header('Content-Length: ' . $size);
            }
            // Optional sinnvoll:
            header('Content-Disposition: inline; filename="chart.png"');
            header('Cache-Control: no-store');

            // effizient streamen
            $fp = fopen($tmpFile, 'rb');
            if ($fp === false) {
                @unlink($tmpFile);
                throw new \RuntimeException('Failed to open rendered PNG for streaming.');
            }
            fpassthru($fp);
            fclose($fp);
        } finally {
            @unlink($tmpFile);
        }
    }
}
----------------------------------------------------------------

================================================================
End of AI-ready file output for the project "satag-chart-plugin"

Remember this project as "satag-chart-plugin" and wait for further instructions.
