Source: js/chart-renderer.js

// chart-renderer.js — Generischer Chart-Renderer für Temperatur & Luftfeuchte
import { ensureChartScaffold } from 'chart-scaffold';
import { renderGenericChart } from 'chart-renderer-core';
import { getSeries } from 'bootstrap-api';

/**
 * Chart-Konfigurationen
 */
const CHART_CONFIGS = {
  temperature: {
    title: 'Temperatur',
    cacheKey: '__chartScaffold',
    chartClass: 'temp-chart',
    unit: '°C',
    colorOut: '#b2182b',
    colorIn: '#2166ac',
    labelOut: 'Außen',
    labelIn: 'Innen',
    fields: {
      out: ['temp', 'outdoor_temp_avg'],
      in: ['indoortemp', 'indoor_temp_avg'],
    },
  },
  humidity: {
    title: 'Luftfeuchte',
    cacheKey: '__humidityChartScaffold',
    chartClass: 'humidity-chart',
    unit: '%',
    colorOut: '#b2182b',
    colorIn: '#2166ac',
    labelOut: 'Außen',
    labelIn: 'Innen',
    fields: {
      out: ['humidity', 'outdoor_humidity_avg'],
      in: ['indoorhumidity', 'indoor_humidity_avg'],
    },
  },
  pressure: {
    title: 'Luftdruck',
    cacheKey: '__pressureChartScaffold',
    chartClass: 'pressure-chart',
    unit: 'hPa',
    color: '#2166ac',
    label: 'Luftdruck',
    fields: {
      value: ['pressure', 'airpress_rel', 'pressure_avg'],
    },
  },
};

/**
 * Erstellt das Chart-Scaffold (Card-Struktur, Header, Legende)
 */
function ensureGenericChartScaffold(mount, config) {
  // Single-Value-Chart (z.B. Luftdruck)
  if (config.color && config.label) {
    const legendHTML = `
      <span class="legend-item d-inline-flex align-items-center gap-1">
        <span class="legend-dot" style="display:inline-block;width:8px;height:8px;border-radius:999px;background:${config.color}"></span>
        <span class="legend-label">${config.label}</span>
      </span>
    `;
    return ensureChartScaffold(mount, {
      title: config.title,
      legendHTML,
      cacheKey: config.cacheKey,
    });
  }

  // Dual-Value-Chart (z.B. Temperatur, Feuchte)
  const legendHTML = `
    <span class="legend-item d-inline-flex align-items-center gap-1" data-ser="out">
      <span class="legend-dot" style="display:inline-block;width:8px;height:8px;border-radius:999px;background:${config.colorOut}"></span>
      <span class="legend-label">${config.labelOut}</span>
    </span>
    <span class="legend-item d-inline-flex align-items-center gap-1" data-ser="in">
      <span class="legend-dot" style="display:inline-block;width:8px;height:8px;border-radius:999px;background:${config.colorIn}"></span>
      <span class="legend-label">${config.labelIn}</span>
    </span>
  `;

  return ensureChartScaffold(mount, {
    title: config.title,
    legendHTML,
    cacheKey: config.cacheKey,
  });
}

/**
 * Mappt API-Rows zu Chart-Series-Format {t, in, out}
 */
function rowsToSeries(rows, fields) {
  return rows.map((row) => ({
    t: (row.time || row.bucket_epoch) * 1000,
    in:
      typeof row[fields.in[0]] === 'number'
        ? row[fields.in[0]]
        : typeof row[fields.in[1]] === 'number'
        ? row[fields.in[1]]
        : null,
    out:
      typeof row[fields.out[0]] === 'number'
        ? row[fields.out[0]]
        : typeof row[fields.out[1]] === 'number'
        ? row[fields.out[1]]
        : null,
  }));
}

/**
 * Interpoliert Randwerte, damit Chart exakt das Zeitfenster füllt
 */
function interpolateEdges(series, win) {
  if (series.length < 2) return series;

  // Linker Rand
  if (series[0].t > win.start) {
    const a = series[0];
    const b = series[1];
    const dt = b.t - a.t;
    if (dt > 0) {
      const frac = (win.start - a.t) / dt;
      series.unshift({
        t: win.start,
        in: a.in + frac * (b.in - a.in),
        out: a.out + frac * (b.out - a.out),
      });
    }
  }

  // Rechter Rand
  if (series[series.length - 1].t < win.end) {
    const a = series[series.length - 2];
    const b = series[series.length - 1];
    const dt = b.t - a.t;
    if (dt > 0) {
      const frac = (win.end - b.t) / dt;
      series.push({
        t: win.end,
        in: b.in + frac * (b.in - a.in),
        out: b.out + frac * (b.out - a.out),
      });
    }
  }

  return series;
}

/**
 * Generischer Chart-Renderer (für Temperatur & Luftfeuchte)
 *
 * @param {string} chartType - 'temperature' oder 'humidity'
 * @param {HTMLElement} mount - Container-Element
 * @param {Object} options - { mode, gapsEnabled, win, series }
 */
export async function renderChart(chartType, mount, options = {}) {
  const config = CHART_CONFIGS[chartType];
  if (!config) {
    console.error(`[chart-renderer] Unbekannter Chart-Typ: ${chartType}`);
    return;
  }

  const { mode = 'auto', gapsEnabled = false, win, series: providedSeries } = options;

  const scaffold = ensureGenericChartScaffold(mount, config);

  // Falls Series bereits übergeben wurden (z.B. Temperatur-Chart)
  if (providedSeries) {
    renderGenericChart({
      mount,
      series: providedSeries,
      win,
      scaffold,
      style: {
        chartClass: config.chartClass,
        unit: config.unit,
        colorOut: config.colorOut,
        colorIn: config.colorIn,
        labelOut: config.labelOut,
        labelIn: config.labelIn,
      },
      options: {
        mode,
        gapsEnabled,
        padTop: 2,
        padBottom: 0,
      },
    });
    return;
  }

  // Sonst: API-Call und Daten-Mapping (z.B. Humidity-Chart)
  let series = [];
  try {
    const data = await getSeries({
      range:
        mode === 'month' ? 'month' : mode === 'week' ? 'week' : mode === 'auto' ? 'day' : 'day',
      bucket: gapsEnabled
        ? 'minute'
        : mode === 'month'
        ? 'week'
        : mode === 'week'
        ? 'day'
        : mode === 'auto'
        ? 'minute'
        : 'hour',
      start: Math.floor(win.start / 1000),
      end: Math.floor(win.end / 1000),
    });

    let rows = Array.isArray(data?.rows) ? data.rows : Array.isArray(data) ? data : [];
    series = rowsToSeries(rows, config.fields);
    series = series.filter(
      (d) => Number.isFinite(d.t) && (Number.isFinite(d.in) || Number.isFinite(d.out))
    );
    series.sort((a, b) => a.t - b.t);
    series = series.filter((d) => d.t >= win.start && d.t <= win.end);

    // Randwerte interpolieren
    series = interpolateEdges(series, win);
  } catch (e) {
    console.warn(`[chart-renderer] Fehler beim Laden der ${config.title}-Daten:`, e);
  }

  // Generischen Chart-Kern aufrufen
  renderGenericChart({
    mount,
    series,
    win,
    scaffold,
    style: {
      chartClass: config.chartClass,
      unit: config.unit,
      colorOut: config.colorOut,
      colorIn: config.colorIn,
      labelOut: config.labelOut,
      labelIn: config.labelIn,
    },
    options: {
      mode,
      gapsEnabled,
      padTop: 2,
      padBottom: 0,
    },
  });
}

/**
 * Kompatibilitäts-Wrapper für Temperatur-Chart
 */
export function renderTempChart(mount, series, win, options = {}) {
  // Kompatibilität: showGaps → gapsEnabled umbenennen
  const { showGaps, ...rest } = options;
  const gapsEnabled = showGaps !== undefined ? showGaps : rest.gapsEnabled;
  return renderChart('temperature', mount, { ...rest, gapsEnabled, series, win });
}

/**
 * Kompatibilitäts-Wrapper für Humidity-Chart
 */
export async function renderHumidityChart(mount, options = {}) {
  return renderChart('humidity', mount, options);
}

/**
 * Erstellt Temperatur-Chart-Scaffold (für temp-chart.js Kompatibilität)
 */
export function ensureTempChartScaffold(mount) {
  return ensureGenericChartScaffold(mount, CHART_CONFIGS.temperature);
}

/**
 * Erstellt Humidity-Chart-Scaffold (für humidity-chart.js Kompatibilität)
 */
export function ensureHumidityChartScaffold(mount) {
  return ensureGenericChartScaffold(mount, CHART_CONFIGS.humidity);
}

/**
 * Erstellt Pressure-Chart-Scaffold (für pressure-chart.js Kompatibilität)
 */
export function ensurePressureChartScaffold(mount) {
  return ensureGenericChartScaffold(mount, CHART_CONFIGS.pressure);
}