// 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);
}