This commit is contained in:
Thomas Peterson 2026-02-12 10:31:46 +01:00
parent 05d50c5ad7
commit 556e72311f
23 changed files with 304 additions and 38 deletions

View File

@ -73,6 +73,17 @@ void WidgetConfig::serialize(uint8_t* buf) const {
buf[pos++] = chartSeriesColor[i].g;
buf[pos++] = chartSeriesColor[i].b;
}
// Chart flags (1 byte)
buf[pos++] = (chartShowXLine ? 0x01 : 0) |
(chartShowBg ? 0x02 : 0) |
(chartShowGrid ? 0x04 : 0) |
(chartShowYLine ? 0x08 : 0) |
(chartShowXLabels ? 0x10 : 0) |
(chartShowYLabels ? 0x20 : 0);
buf[pos++] = chartLineWidth;
buf[pos++] = chartPointCount;
buf[pos++] = chartShowPoints ? 1 : 0;
buf[pos++] = chartPointSize;
// Secondary KNX address (left value)
buf[pos++] = knxAddress2 & 0xFF;
@ -238,6 +249,37 @@ void WidgetConfig::deserialize(const uint8_t* buf) {
chartSeriesColor[i].g = buf[pos++];
chartSeriesColor[i].b = buf[pos++];
}
// Chart flags
if (pos + 1 <= SERIALIZED_SIZE) {
uint8_t chartFlags = buf[pos++];
chartShowXLine = (chartFlags & 0x01) != 0;
chartShowBg = (chartFlags & 0x02) != 0;
chartShowGrid = (chartFlags & 0x04) != 0;
chartShowYLine = (chartFlags & 0x08) != 0;
chartShowXLabels = (chartFlags & 0x10) != 0;
chartShowYLabels = (chartFlags & 0x20) != 0;
} else {
chartShowXLine = true;
chartShowYLine = true;
chartShowXLabels = true;
chartShowYLabels = true;
chartShowBg = true;
chartShowGrid = true;
}
if (pos + 4 <= SERIALIZED_SIZE) {
chartLineWidth = buf[pos++];
if (chartLineWidth == 0) chartLineWidth = 2;
chartPointCount = buf[pos++];
if (chartPointCount == 0) chartPointCount = 120;
chartShowPoints = buf[pos++] != 0;
chartPointSize = buf[pos++];
if (chartPointSize == 0) chartPointSize = 4;
} else {
chartLineWidth = 2;
chartPointCount = 120;
chartShowPoints = false;
chartPointSize = 4;
}
// Secondary KNX address (left value) - check bounds for backward compatibility
if (pos + 19 <= SERIALIZED_SIZE) {

View File

@ -307,6 +307,16 @@ struct WidgetConfig {
uint16_t chartKnxAddress[CHART_MAX_SERIES];
TextSource chartTextSource[CHART_MAX_SERIES];
Color chartSeriesColor[CHART_MAX_SERIES];
bool chartShowXLine; // Show X-axis line/ticks
bool chartShowYLine; // Show Y-axis line/ticks
bool chartShowXLabels; // Show X-axis labels
bool chartShowYLabels; // Show Y-axis labels
bool chartShowBg; // Show chart background
bool chartShowGrid; // Show grid/div lines
uint8_t chartLineWidth; // Line thickness (1-10, default 2)
uint8_t chartPointCount; // Number of data points (10-120, default 120)
bool chartShowPoints; // Show point markers
uint8_t chartPointSize; // Point diameter (2-20, default 4)
// Secondary KNX address (for PowerNode LEFT value)
uint16_t knxAddress2;
@ -348,7 +358,7 @@ struct WidgetConfig {
bool themeFixed; // true = colors stay fixed in night mode
// Serialization size (fixed for NVS storage)
static constexpr size_t SERIALIZED_SIZE = 406;
static constexpr size_t SERIALIZED_SIZE = 411;
void serialize(uint8_t* buf) const;
void deserialize(const uint8_t* buf);

View File

@ -1738,6 +1738,16 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const {
if (w.type == WidgetType::CHART) {
cJSON* chart = cJSON_AddObjectToObject(widget, "chart");
cJSON_AddNumberToObject(chart, "period", w.chartPeriod);
cJSON_AddBoolToObject(chart, "showXLine", w.chartShowXLine);
cJSON_AddBoolToObject(chart, "showYLine", w.chartShowYLine);
cJSON_AddBoolToObject(chart, "showXLabels", w.chartShowXLabels);
cJSON_AddBoolToObject(chart, "showYLabels", w.chartShowYLabels);
cJSON_AddBoolToObject(chart, "showBg", w.chartShowBg);
cJSON_AddBoolToObject(chart, "showGrid", w.chartShowGrid);
cJSON_AddNumberToObject(chart, "lineWidth", w.chartLineWidth);
cJSON_AddNumberToObject(chart, "pointCount", w.chartPointCount);
cJSON_AddBoolToObject(chart, "showPoints", w.chartShowPoints);
cJSON_AddNumberToObject(chart, "pointSize", w.chartPointSize);
cJSON* series = cJSON_AddArrayToObject(chart, "series");
uint8_t seriesCount = w.chartSeriesCount;
if (seriesCount > CHART_MAX_SERIES) seriesCount = CHART_MAX_SERIES;
@ -1861,6 +1871,16 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
w.isContainer = false;
w.chartPeriod = static_cast<uint8_t>(ChartPeriod::HOUR_1);
w.chartSeriesCount = 1;
w.chartShowXLine = true;
w.chartShowYLine = true;
w.chartShowXLabels = true;
w.chartShowYLabels = true;
w.chartShowBg = true;
w.chartShowGrid = true;
w.chartLineWidth = 2;
w.chartPointCount = 120;
w.chartShowPoints = false;
w.chartPointSize = 4;
for (size_t i = 0; i < CHART_MAX_SERIES; ++i) {
w.chartKnxAddress[i] = 0;
w.chartTextSource[i] = TextSource::KNX_DPT_TEMP;
@ -2151,6 +2171,30 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
w.chartPeriod = static_cast<uint8_t>(periodVal);
}
cJSON* showXLine = cJSON_GetObjectItem(chart, "showXLine");
w.chartShowXLine = cJSON_IsBool(showXLine) ? cJSON_IsTrue(showXLine) : true;
cJSON* showYLine = cJSON_GetObjectItem(chart, "showYLine");
w.chartShowYLine = cJSON_IsBool(showYLine) ? cJSON_IsTrue(showYLine) : true;
cJSON* showXLabels = cJSON_GetObjectItem(chart, "showXLabels");
w.chartShowXLabels = cJSON_IsBool(showXLabels) ? cJSON_IsTrue(showXLabels) : true;
cJSON* showYLabels = cJSON_GetObjectItem(chart, "showYLabels");
w.chartShowYLabels = cJSON_IsBool(showYLabels) ? cJSON_IsTrue(showYLabels) : true;
cJSON* showBg = cJSON_GetObjectItem(chart, "showBg");
w.chartShowBg = cJSON_IsBool(showBg) ? cJSON_IsTrue(showBg) : true;
cJSON* showGrid = cJSON_GetObjectItem(chart, "showGrid");
w.chartShowGrid = cJSON_IsBool(showGrid) ? cJSON_IsTrue(showGrid) : true;
cJSON* lineWidth = cJSON_GetObjectItem(chart, "lineWidth");
w.chartLineWidth = cJSON_IsNumber(lineWidth) ? lineWidth->valueint : 2;
if (w.chartLineWidth == 0) w.chartLineWidth = 2;
cJSON* pointCount = cJSON_GetObjectItem(chart, "pointCount");
w.chartPointCount = cJSON_IsNumber(pointCount) ? pointCount->valueint : 120;
if (w.chartPointCount < 10) w.chartPointCount = 10;
if (w.chartPointCount > 120) w.chartPointCount = 120;
cJSON* showPoints = cJSON_GetObjectItem(chart, "showPoints");
w.chartShowPoints = cJSON_IsBool(showPoints) ? cJSON_IsTrue(showPoints) : false;
cJSON* pointSize = cJSON_GetObjectItem(chart, "pointSize");
w.chartPointSize = cJSON_IsNumber(pointSize) ? pointSize->valueint : 4;
cJSON* series = cJSON_GetObjectItem(chart, "series");
if (cJSON_IsArray(series)) {
uint8_t idx = 0;

View File

@ -18,43 +18,55 @@ lv_obj_t* ChartWidget::create(lv_obj_t* parent) {
lv_obj_set_size(obj_, width, height);
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE);
const int32_t yAxisWidth = 48;
const int32_t xAxisHeight = 26;
const bool hasYAxis = config_.chartShowYLine || config_.chartShowYLabels;
const bool hasXAxis = config_.chartShowXLine || config_.chartShowXLabels;
const int32_t yAxisWidth = hasYAxis ? 36 : 0;
const int32_t xAxisHeight = hasXAxis ? 24 : 0;
const int32_t topPad = 8;
int32_t chartWidth = width - yAxisWidth;
int32_t chartHeight = height - xAxisHeight;
int32_t chartHeight = height - xAxisHeight - topPad;
if (chartWidth < 20) chartWidth = 20;
if (chartHeight < 20) chartHeight = 20;
yScale_ = lv_scale_create(obj_);
if (yScale_) {
lv_scale_set_mode(yScale_, LV_SCALE_MODE_VERTICAL_LEFT);
lv_scale_set_total_tick_count(yScale_, 5);
lv_scale_set_major_tick_every(yScale_, 1);
lv_scale_set_label_show(yScale_, true);
lv_scale_set_range(yScale_, 0, 100);
lv_obj_set_pos(yScale_, 0, 0);
lv_obj_set_size(yScale_, yAxisWidth, chartHeight);
if (hasYAxis) {
yScale_ = lv_scale_create(obj_);
if (yScale_) {
lv_scale_set_mode(yScale_, LV_SCALE_MODE_VERTICAL_LEFT);
lv_scale_set_total_tick_count(yScale_, 5);
lv_scale_set_major_tick_every(yScale_, 1);
lv_scale_set_label_show(yScale_, config_.chartShowYLabels);
lv_scale_set_range(yScale_, 0, 100);
lv_obj_set_pos(yScale_, 0, topPad);
lv_obj_set_size(yScale_, yAxisWidth, chartHeight);
}
}
xScale_ = lv_scale_create(obj_);
if (xScale_) {
lv_scale_set_mode(xScale_, LV_SCALE_MODE_HORIZONTAL_BOTTOM);
lv_scale_set_label_show(xScale_, true);
lv_obj_set_pos(xScale_, yAxisWidth, chartHeight);
lv_obj_set_size(xScale_, chartWidth, xAxisHeight);
if (hasXAxis) {
xScale_ = lv_scale_create(obj_);
if (xScale_) {
lv_scale_set_mode(xScale_, LV_SCALE_MODE_HORIZONTAL_BOTTOM);
lv_scale_set_label_show(xScale_, config_.chartShowXLabels);
lv_obj_set_pos(xScale_, yAxisWidth, topPad + chartHeight);
lv_obj_set_size(xScale_, chartWidth, xAxisHeight);
}
}
chart_ = lv_chart_create(obj_);
if (!chart_) {
return obj_;
}
lv_obj_set_pos(chart_, yAxisWidth, 0);
lv_obj_set_pos(chart_, yAxisWidth, topPad);
lv_obj_set_size(chart_, chartWidth, chartHeight);
lv_obj_clear_flag(chart_, LV_OBJ_FLAG_SCROLLABLE);
lv_chart_set_type(chart_, LV_CHART_TYPE_LINE);
lv_chart_set_point_count(chart_, HistoryStore::CHART_POINT_COUNT);
lv_chart_set_div_line_count(chart_, 4, 6);
uint8_t pointCount = config_.chartPointCount;
if (pointCount < 10) pointCount = 10;
if (pointCount > HistoryStore::CHART_POINT_COUNT) pointCount = HistoryStore::CHART_POINT_COUNT;
pointCount_ = pointCount;
lv_chart_set_point_count(chart_, pointCount);
lv_chart_set_div_line_count(chart_, config_.chartShowGrid ? 4 : 0,
config_.chartShowGrid ? 6 : 0);
uint8_t count = config_.chartSeriesCount;
if (count > CHART_MAX_SERIES) count = CHART_MAX_SERIES;
@ -84,8 +96,23 @@ void ChartWidget::applyStyle() {
if (chart_) {
lv_obj_set_style_border_width(chart_, 0, 0);
lv_obj_set_style_pad_all(chart_, 6, 0);
lv_obj_set_style_line_width(chart_, 2, LV_PART_ITEMS);
lv_obj_set_style_pad_left(chart_, 2, 0);
lv_obj_set_style_pad_right(chart_, 4, 0);
lv_obj_set_style_pad_top(chart_, 4, 0);
lv_obj_set_style_pad_bottom(chart_, 4, 0);
uint8_t lw = config_.chartLineWidth;
if (lw == 0) lw = 2;
lv_obj_set_style_line_width(chart_, lw, LV_PART_ITEMS);
if (config_.chartShowPoints) {
uint8_t ps = config_.chartPointSize;
if (ps == 0) ps = 4;
lv_obj_set_style_size(chart_, ps, ps, LV_PART_INDICATOR);
} else {
lv_obj_set_style_size(chart_, 0, 0, LV_PART_INDICATOR);
}
if (!config_.chartShowBg) {
lv_obj_set_style_bg_opa(chart_, LV_OPA_TRANSP, 0);
}
}
const lv_color_t textColor = lv_color_make(
@ -96,12 +123,20 @@ void ChartWidget::applyStyle() {
lv_obj_set_style_text_font(yScale_, axisFont, LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(yScale_, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(yScale_, 0, 0);
if (!config_.chartShowYLine) {
lv_obj_set_style_line_opa(yScale_, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_line_opa(yScale_, LV_OPA_TRANSP, LV_PART_INDICATOR);
}
}
if (xScale_) {
lv_obj_set_style_text_color(xScale_, textColor, LV_PART_INDICATOR);
lv_obj_set_style_text_font(xScale_, axisFont, LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(xScale_, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(xScale_, 0, 0);
if (!config_.chartShowXLine) {
lv_obj_set_style_line_opa(xScale_, LV_OPA_TRANSP, LV_PART_MAIN);
lv_obj_set_style_line_opa(xScale_, LV_OPA_TRANSP, LV_PART_INDICATOR);
}
}
}
@ -127,9 +162,9 @@ void ChartWidget::refreshData() {
config_.chartTextSource[i],
static_cast<ChartPeriod>(config_.chartPeriod),
seriesData_[i].data(),
seriesData_[i].size());
pointCount_);
for (size_t j = 0; j < seriesData_[i].size(); ++j) {
for (size_t j = 0; j < pointCount_; ++j) {
int32_t value = seriesData_[i][j];
if (value == HistoryStore::NO_POINT) continue;
if (!hasAny) {

View File

@ -22,4 +22,5 @@ private:
lv_obj_t* xScale_ = nullptr;
lv_chart_series_t* series_[CHART_MAX_SERIES] = {};
std::array<std::array<int32_t, HistoryStore::CHART_POINT_COUNT>, CHART_MAX_SERIES> seriesData_ = {};
uint8_t pointCount_ = HistoryStore::CHART_POINT_COUNT;
};

View File

@ -7,16 +7,42 @@
@touchstart.stop="$emit('drag-start', { id: widget.id, event: $event })"
@click.stop="$emit('select')"
>
<div class="w-full h-full flex flex-col gap-2">
<div class="text-[11px] uppercase tracking-[0.12em] opacity-80">
{{ widget.text || 'Chart' }}
<div class="w-full h-full flex flex-col" style="overflow: hidden;">
<!-- Chart area -->
<div class="flex-1 flex min-h-0">
<!-- Y axis -->
<div v-if="hasYAxis" class="flex flex-col justify-between items-end shrink-0 pr-[3px]" :style="{ width: yAxisWidth + 'px', paddingTop: topPad + 'px' }">
<template v-if="showYLabels">
<span v-for="(label, i) in yLabels" :key="i" :style="{ fontSize: axisFontSize + 'px', color: widget.textColor, opacity: 0.7, lineHeight: '1' }">{{ label }}</span>
</template>
<template v-else>
<span v-for="(label, i) in yLabels" :key="i" :style="{ fontSize: axisFontSize + 'px', opacity: 0 }">{{ label }}</span>
</template>
</div>
<!-- Chart plot area -->
<div class="flex-1 relative" :style="{ marginTop: topPad + 'px' }">
<!-- Background -->
<div v-if="showBg" class="absolute inset-0 rounded-[4px]" :style="{ background: 'rgba(0,0,0,0.15)' }"></div>
<!-- Grid -->
<svg v-if="showGrid" class="absolute inset-0 w-full h-full" preserveAspectRatio="none">
<line v-for="i in 4" :key="'h'+i" :x1="0" :y1="(i * 100 / 4) + '%'" :x2="'100%'" :y2="(i * 100 / 4) + '%'" stroke="currentColor" :stroke-opacity="0.12" stroke-width="1" />
<line v-for="i in (xLabels.length - 1)" :key="'v'+i" :x1="(i * 100 / (xLabels.length - 1)) + '%'" :y1="0" :x2="(i * 100 / (xLabels.length - 1)) + '%'" :y2="'100%'" stroke="currentColor" :stroke-opacity="0.12" stroke-width="1" />
</svg>
<!-- Series lines -->
<svg class="absolute inset-0 w-full h-full" viewBox="0 0 100 50" preserveAspectRatio="none">
<polyline v-for="(path, i) in seriesPaths" :key="'s'+i" :points="path.line" fill="none" :stroke="seriesColors[i]" :stroke-width="lineWidth" vector-effect="non-scaling-stroke" stroke-linejoin="round" />
</svg>
<!-- Point markers -->
<div v-if="showPoints" class="absolute inset-0">
<div v-for="(pt, pi) in allPoints" :key="pi" class="absolute rounded-full" :style="{ left: pt.x + '%', top: pt.y + '%', width: pointDiameter + 'px', height: pointDiameter + 'px', transform: 'translate(-50%, -50%)', backgroundColor: pt.color }"></div>
</div>
</div>
</div>
<div class="flex-1 rounded-[10px] bg-black/20 relative overflow-hidden">
<div class="absolute inset-0 opacity-30" style="background-image: linear-gradient(to right, rgba(255,255,255,0.1) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.1) 1px, transparent 1px); background-size: 24px 24px;"></div>
<svg class="absolute inset-0" viewBox="0 0 100 40" preserveAspectRatio="none">
<path d="M0,30 L15,22 L30,26 L45,14 L60,18 L75,10 L100,16" fill="none" stroke="rgba(239,99,81,0.8)" stroke-width="2" />
<path d="M0,34 L20,28 L40,32 L60,20 L80,24 L100,18" fill="none" stroke="rgba(125,211,176,0.8)" stroke-width="2" />
</svg>
<!-- X axis -->
<div v-if="hasXAxis" class="flex shrink-0" :style="{ height: xAxisHeight + 'px', paddingLeft: yAxisWidth + 'px' }">
<div class="flex-1 flex justify-between items-start">
<span v-for="(label, i) in xLabels" :key="i" :style="{ fontSize: axisFontSize + 'px', color: widget.textColor, opacity: showXLabels ? 0.7 : 0, lineHeight: '1' }">{{ label }}</span>
</div>
</div>
</div>
@ -45,10 +71,89 @@ const props = defineProps({
defineEmits(['select', 'drag-start', 'resize-start']);
const showXLine = computed(() => props.widget?.chart?.showXLine !== false);
const showYLine = computed(() => props.widget?.chart?.showYLine !== false);
const showXLabels = computed(() => props.widget?.chart?.showXLabels !== false);
const showYLabels = computed(() => props.widget?.chart?.showYLabels !== false);
const showBg = computed(() => props.widget?.chart?.showBg !== false);
const showGrid = computed(() => props.widget?.chart?.showGrid !== false);
const showPoints = computed(() => props.widget?.chart?.showPoints === true);
const hasYAxis = computed(() => showYLine.value || showYLabels.value);
const hasXAxis = computed(() => showXLine.value || showXLabels.value);
const yAxisWidth = computed(() => hasYAxis.value ? Math.round(36 * props.scale) : 0);
const xAxisHeight = computed(() => hasXAxis.value ? Math.round(24 * props.scale) : 0);
const topPad = computed(() => Math.round(8 * props.scale));
const axisFontSize = computed(() => Math.round(9 * props.scale));
const lineWidth = computed(() => Math.max(1, props.widget?.chart?.lineWidth ?? 2));
const pointDiameter = computed(() => Math.max(2, (props.widget?.chart?.pointSize ?? 4) * props.scale));
const numPoints = computed(() => {
const pc = props.widget?.chart?.pointCount ?? 120;
// Scale down for preview (don't render 120 dots)
return Math.max(5, Math.min(pc, 20));
});
const periodXLabels = {
0: ['-60m', '-45m', '-30m', '-15m', '0'],
1: ['-3h', '-2h', '-1h', '-30m', '0'],
2: ['-5h', '-4h', '-3h', '-2h', '-1h', '0'],
3: ['-12h', '-9h', '-6h', '-3h', '0'],
4: ['-24h', '-18h', '-12h', '-6h', '0'],
5: ['-30d', '-21d', '-14d', '-7d', '0']
};
const xLabels = computed(() => {
const period = props.widget?.chart?.period ?? 0;
return periodXLabels[period] || periodXLabels[0];
});
const yLabels = computed(() => ['100', '75', '50', '25', '0']);
const defaultColors = ['#EF6351', '#7DD3B0', '#5EA2EF'];
const seriesColors = computed(() => {
const series = props.widget?.chart?.series ?? [];
return series.map((s, i) => s.color || defaultColors[i] || defaultColors[0]);
});
// Generate demo wave data per series
const seriesPaths = computed(() => {
const series = props.widget?.chart?.series ?? [];
const count = Math.max(1, Math.min(series.length, 3));
const steps = numPoints.value;
const result = [];
for (let s = 0; s < count; s++) {
const linePoints = [];
const dots = [];
const phase = s * 2.2;
const amp = 12 + s * 4;
for (let i = 0; i <= steps; i++) {
const x = (i / steps) * 100;
const y = 25 + amp * Math.sin((i / steps) * Math.PI * 2.5 + phase) * 0.4;
const cx = clamp(x, 0, 100);
const cy = clamp(y, 2, 48);
linePoints.push(`${cx.toFixed(1)},${cy.toFixed(1)}`);
dots.push({ x: cx, y: (cy / 50) * 100 }); // map 0-50 viewBox to 0-100%
}
result.push({ line: linePoints.join(' '), dots });
}
return result;
});
const allPoints = computed(() => {
const colors = seriesColors.value;
const pts = [];
seriesPaths.value.forEach((sp, si) => {
sp.dots.forEach(d => {
pts.push({ x: d.x, y: d.y, color: colors[si] || '#EF6351' });
});
});
return pts;
});
const computedStyle = computed(() => {
const w = props.widget;
const s = props.scale;
const style = getBaseStyle(w, s);
if (w.bgOpacity > 0) {
@ -61,8 +166,7 @@ const computedStyle = computed(() => {
}
Object.assign(style, getBorderStyle(w, s));
style.padding = `${12 * s}px`;
style.padding = `${4 * s}px`;
return style;
});

View File

@ -15,6 +15,16 @@
<option :value="3">3</option>
</select>
</div>
<div :class="rowClass"><label :class="labelClass">X-Linie</label><input class="accent-[var(--accent)]" type="checkbox" v-model="widget.chart.showXLine"></div>
<div :class="rowClass"><label :class="labelClass">Y-Linie</label><input class="accent-[var(--accent)]" type="checkbox" v-model="widget.chart.showYLine"></div>
<div :class="rowClass"><label :class="labelClass">X-Labels</label><input class="accent-[var(--accent)]" type="checkbox" v-model="widget.chart.showXLabels"></div>
<div :class="rowClass"><label :class="labelClass">Y-Labels</label><input class="accent-[var(--accent)]" type="checkbox" v-model="widget.chart.showYLabels"></div>
<div :class="rowClass"><label :class="labelClass">Hintergrund</label><input class="accent-[var(--accent)]" type="checkbox" v-model="widget.chart.showBg"></div>
<div :class="rowClass"><label :class="labelClass">Gitterlinien</label><input class="accent-[var(--accent)]" type="checkbox" v-model="widget.chart.showGrid"></div>
<div :class="rowClass"><label :class="labelClass">Liniendicke</label><input :class="inputClass" type="number" min="1" max="10" v-model.number="widget.chart.lineWidth"></div>
<div :class="rowClass"><label :class="labelClass">Datenpunkte</label><input :class="inputClass" type="number" min="10" max="120" v-model.number="widget.chart.pointCount"></div>
<div :class="rowClass"><label :class="labelClass">Punkte zeigen</label><input class="accent-[var(--accent)]" type="checkbox" v-model="widget.chart.showPoints"></div>
<div v-if="widget.chart.showPoints" :class="rowClass"><label :class="labelClass">Punktgröße</label><input :class="inputClass" type="number" min="2" max="20" v-model.number="widget.chart.pointSize"></div>
<div v-for="(series, idx) in chartSeries" :key="idx" class="border border-border rounded-lg px-2.5 py-2 mb-2 bg-panel-2">
<div class="flex items-center gap-2 mb-2 text-[11px] text-muted">
<label class="w-[70px] text-[11px] text-muted">Serie {{ idx + 1 }}</label>

View File

@ -434,6 +434,16 @@ export const WIDGET_DEFAULTS = {
themeFixed: false,
chart: {
period: 0,
showXLine: true,
showYLine: true,
showXLabels: true,
showYLabels: true,
showBg: true,
showGrid: true,
lineWidth: 2,
pointCount: 120,
showPoints: false,
pointSize: 4,
series: [
{ knxAddr: 0, textSrc: 1, color: '#EF6351' }
]

View File

@ -157,6 +157,16 @@ export function normalizeWidget(w, nextWidgetIdRef) {
if (w.chart.period === undefined || w.chart.period === null) {
w.chart.period = defaults.chart.period ?? 0;
}
if (w.chart.showXLine === undefined) w.chart.showXLine = true;
if (w.chart.showYLine === undefined) w.chart.showYLine = true;
if (w.chart.showXLabels === undefined) w.chart.showXLabels = true;
if (w.chart.showYLabels === undefined) w.chart.showYLabels = true;
if (w.chart.showBg === undefined) w.chart.showBg = true;
if (w.chart.showGrid === undefined) w.chart.showGrid = true;
if (w.chart.lineWidth === undefined) w.chart.lineWidth = 2;
if (w.chart.pointCount === undefined) w.chart.pointCount = 120;
if (w.chart.showPoints === undefined) w.chart.showPoints = false;
if (w.chart.pointSize === undefined) w.chart.pointSize = 4;
if (!Array.isArray(w.chart.series)) {
w.chart.series = [];
}