v0.14.24
This commit is contained in:
+2
-7
@@ -10226,6 +10226,7 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr
|
||||
min-width: 720px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.public-custom-chart-card .legend {
|
||||
@@ -10241,13 +10242,7 @@ select.select-native-proxy[aria-invalid="true"]+.custom-select .custom-select-tr
|
||||
}
|
||||
|
||||
.public-chart-legend-item {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.public-chart-legend-item:hover {
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.public-custom-chart-error {
|
||||
|
||||
+128
-21
@@ -4,6 +4,7 @@ const PUBLIC_CHART_COLORS = ['--accent', '--info', '--warning', '--purple', '--d
|
||||
const $ = selector => document.querySelector(selector);
|
||||
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
||||
const PUBLIC_CHART_RANGES = [6, 24, 168, 720, 2160, 8760];
|
||||
const PUBLIC_MAX_CHART_ZOOM = 16;
|
||||
|
||||
function basePathFromScript() {
|
||||
const src = document.currentScript?.src || '';
|
||||
@@ -23,11 +24,8 @@ const publicLanguagePacks = new Map();
|
||||
let publicChartLanguage = PUBLIC_DEFAULT_LANGUAGE;
|
||||
let publicChartTranslations = {};
|
||||
let publicChartLocale = PUBLIC_DEFAULT_LANGUAGE;
|
||||
|
||||
function publicCookie(name) {
|
||||
const row = document.cookie.split('; ').find(item => item.startsWith(`${name}=`));
|
||||
return row ? decodeURIComponent(row.split('=').slice(1).join('=')) : '';
|
||||
}
|
||||
let publicChartZoom = 1;
|
||||
const publicChartHiddenSeries = new Set();
|
||||
|
||||
function publicTr(key, params = {}) {
|
||||
const template = publicChartTranslations[key] ?? key;
|
||||
@@ -85,7 +83,8 @@ function preciseTime(timestamp, locale) {
|
||||
|
||||
function prepareCanvas(canvas, height = 520) {
|
||||
const wrap = canvas.parentElement;
|
||||
const width = Math.max(720, Math.floor(wrap?.clientWidth || canvas.getBoundingClientRect().width || 720));
|
||||
const baseWidth = Math.max(720, Math.floor(wrap?.clientWidth || canvas.getBoundingClientRect().width || 720));
|
||||
const width = Math.max(1, Math.floor(baseWidth * publicChartZoom));
|
||||
const actualHeight = Math.max(180, Math.floor(wrap?.clientHeight || height));
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = Math.floor(width * dpr);
|
||||
@@ -98,12 +97,21 @@ function prepareCanvas(canvas, height = 520) {
|
||||
return { ctx, width, height: actualHeight };
|
||||
}
|
||||
|
||||
function publicChartSeriesKey(item, index) {
|
||||
return item.key || `${index}:${item.label}`;
|
||||
}
|
||||
|
||||
function isPublicChartSeriesHidden(item, index) {
|
||||
return publicChartHiddenSeries.has(publicChartSeriesKey(item, index));
|
||||
}
|
||||
|
||||
function renderLegend(series) {
|
||||
const host = $('#publicCustomChartLegend');
|
||||
host.innerHTML = series.map((item, index) => {
|
||||
const color = cssColor(PUBLIC_CHART_COLORS[index % PUBLIC_CHART_COLORS.length], '#3ecf8e');
|
||||
const hidden = isPublicChartSeriesHidden(item, index);
|
||||
const dashed = item.dashed ? ' is-dashed' : '';
|
||||
return `<span class="legend-item public-chart-legend-item"><i class="legend-line${dashed}" style="--legend-color:${color}"></i><span>${escapeHtml(item.label)}</span></span>`;
|
||||
const title = publicTr(hidden ? 'history.legendShow' : 'history.legendHide', { name: item.label });
|
||||
return `<button type="button" class="legend-item public-chart-legend-item${hidden ? ' is-hidden' : ''}" data-public-chart-legend="${index}" aria-pressed="${hidden ? 'false' : 'true'}" title="${escapeHtml(title)}"><i class="legend-line${dashed}" style="--legend-color:${escapeHtml(item.color)}"></i><span>${escapeHtml(item.label)}</span></button>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@@ -117,6 +125,13 @@ function drawEmpty(canvas, message) {
|
||||
ctx.font = '13px system-ui';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(message, width / 2, height / 2);
|
||||
const wrap = canvas.parentElement;
|
||||
const tooltip = wrap?.querySelector('.chart-tooltip');
|
||||
const line = wrap?.querySelector('.chart-hover-line');
|
||||
if (tooltip) tooltip.hidden = true;
|
||||
if (line) line.hidden = true;
|
||||
canvas.onpointermove = null;
|
||||
canvas.onpointerleave = null;
|
||||
}
|
||||
|
||||
function nearestPoint(points, targetTs) {
|
||||
@@ -142,7 +157,7 @@ function bindTooltip(canvas, series, geometry, locale) {
|
||||
const timestampPoints = timestamps.map(ts => ({ ts }));
|
||||
const seriesPoints = series.map((item, index) => ({
|
||||
item,
|
||||
color: cssColor(PUBLIC_CHART_COLORS[index % PUBLIC_CHART_COLORS.length], '#3ecf8e'),
|
||||
color: item.color || cssColor(PUBLIC_CHART_COLORS[index % PUBLIC_CHART_COLORS.length], '#3ecf8e'),
|
||||
points: item.points.map(point => ({ ts: new Date(point.timestamp).getTime(), value: Number(point.value) }))
|
||||
.filter(point => Number.isFinite(point.ts) && Number.isFinite(point.value))
|
||||
.sort((a, b) => a.ts - b.ts)
|
||||
@@ -178,12 +193,13 @@ function bindTooltip(canvas, series, geometry, locale) {
|
||||
}
|
||||
|
||||
function drawChart(canvas, payload, locale) {
|
||||
const series = (payload.series || []).map((item, index) => ({
|
||||
const allSeries = (payload.series || []).map((item, index) => ({
|
||||
...item,
|
||||
color: cssColor(PUBLIC_CHART_COLORS[index % PUBLIC_CHART_COLORS.length], '#3ecf8e'),
|
||||
points: Array.isArray(item.points) ? item.points : []
|
||||
}));
|
||||
renderLegend(series);
|
||||
renderLegend(allSeries);
|
||||
const series = allSeries.filter((item, index) => !isPublicChartSeriesHidden(item, index));
|
||||
const allPoints = series.flatMap(item => item.points.map(point => ({ ...point, value: Number(point.value) })))
|
||||
.filter(point => Number.isFinite(point.value) && Number.isFinite(new Date(point.timestamp).getTime()));
|
||||
if (!allPoints.length) return drawEmpty(canvas, payload.no_data_label || publicTr('publicChart.noData'));
|
||||
@@ -288,6 +304,80 @@ function redrawPublicChart() {
|
||||
drawChart($('#publicCustomChart'), publicChartPayload, publicChartLocale);
|
||||
}
|
||||
|
||||
function setPublicChartZoom(value, anchorRatio = null, anchorViewportX = null) {
|
||||
const canvas = $('#publicCustomChart');
|
||||
const wrap = canvas?.parentElement;
|
||||
if (!canvas || !wrap || !publicChartPayload) return;
|
||||
const oldWidth = canvas.getBoundingClientRect().width || 1;
|
||||
const ratio = Number.isFinite(anchorRatio)
|
||||
? clamp(anchorRatio, 0, 1)
|
||||
: clamp((wrap.scrollLeft + wrap.clientWidth / 2) / oldWidth, 0, 1);
|
||||
const viewportX = Number.isFinite(anchorViewportX) ? anchorViewportX : wrap.clientWidth / 2;
|
||||
const next = clamp(Number(value) || 1, 1, PUBLIC_MAX_CHART_ZOOM);
|
||||
if (Math.abs(next - publicChartZoom) < 0.001) return;
|
||||
publicChartZoom = next;
|
||||
redrawPublicChart();
|
||||
const nextWidth = canvas.getBoundingClientRect().width || 1;
|
||||
wrap.scrollLeft = Math.max(0, ratio * nextWidth - viewportX);
|
||||
}
|
||||
|
||||
function bindPublicChartTouchGestures() {
|
||||
const canvas = $('#publicCustomChart');
|
||||
const wrap = canvas?.parentElement;
|
||||
if (!canvas || !wrap) return;
|
||||
let pinch = null;
|
||||
let pan = null;
|
||||
const distance = touches => Math.hypot(
|
||||
touches[0].clientX - touches[1].clientX,
|
||||
touches[0].clientY - touches[1].clientY
|
||||
);
|
||||
const beginPinch = touches => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const wrapRect = wrap.getBoundingClientRect();
|
||||
const centerX = (touches[0].clientX + touches[1].clientX) / 2;
|
||||
pinch = {
|
||||
distance: Math.max(1, distance(touches)),
|
||||
zoom: publicChartZoom,
|
||||
anchorRatio: clamp((centerX - rect.left) / Math.max(1, rect.width), 0, 1),
|
||||
anchorViewportX: clamp(centerX - wrapRect.left, 0, wrap.clientWidth),
|
||||
};
|
||||
pan = null;
|
||||
};
|
||||
const beginPan = touch => {
|
||||
pan = { clientX: touch.clientX, scrollLeft: wrap.scrollLeft };
|
||||
};
|
||||
|
||||
canvas.addEventListener('touchstart', event => {
|
||||
if (event.touches.length >= 2) {
|
||||
event.preventDefault();
|
||||
beginPinch(event.touches);
|
||||
} else if (event.touches.length === 1) {
|
||||
beginPan(event.touches[0]);
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchmove', event => {
|
||||
if (event.touches.length >= 2) {
|
||||
event.preventDefault();
|
||||
if (!pinch) beginPinch(event.touches);
|
||||
const scale = distance(event.touches) / Math.max(1, pinch.distance);
|
||||
setPublicChartZoom(pinch.zoom * scale, pinch.anchorRatio, pinch.anchorViewportX);
|
||||
return;
|
||||
}
|
||||
if (event.touches.length === 1 && pan && wrap.scrollWidth > wrap.clientWidth + 1) {
|
||||
event.preventDefault();
|
||||
wrap.scrollLeft = pan.scrollLeft + (pan.clientX - event.touches[0].clientX);
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
canvas.addEventListener('touchend', event => {
|
||||
pinch = null;
|
||||
if (event.touches.length === 1) beginPan(event.touches[0]);
|
||||
else pan = null;
|
||||
}, { passive: true });
|
||||
canvas.addEventListener('touchcancel', () => { pinch = null; pan = null; }, { passive: true });
|
||||
}
|
||||
|
||||
async function loadPublicChart(hours = requestedPublicChartHours()) {
|
||||
const token = publicChartToken();
|
||||
const title = $('#publicChartTitle');
|
||||
@@ -297,6 +387,7 @@ async function loadPublicChart(hours = requestedPublicChartHours()) {
|
||||
const range = $('#publicChartHours');
|
||||
|
||||
if (!token.startsWith('chart_')) {
|
||||
await setPublicChartLanguage('en');
|
||||
title.textContent = publicTr('publicChart.title');
|
||||
hint.textContent = '';
|
||||
error.hidden = false;
|
||||
@@ -306,16 +397,19 @@ async function loadPublicChart(hours = requestedPublicChartHours()) {
|
||||
}
|
||||
|
||||
if (range) range.disabled = true;
|
||||
if (hint) hint.textContent = publicTr('publicChart.loading');
|
||||
if (hint) hint.textContent = Object.keys(publicChartTranslations).length ? publicTr('publicChart.loading') : 'Loading…';
|
||||
error.hidden = true;
|
||||
const response = await fetch(withBase(publicChartApiPath(token, hours)), { headers: { Accept: 'application/json' }, cache: 'no-store' });
|
||||
if (!response.ok) {
|
||||
let message = `${response.status}`;
|
||||
try { message = (await response.json()).error || message; } catch (_) { }
|
||||
throw new Error(message);
|
||||
if (response.status === 404) await setPublicChartLanguage('en');
|
||||
const requestError = new Error(message);
|
||||
requestError.status = response.status;
|
||||
throw requestError;
|
||||
}
|
||||
const payload = await response.json();
|
||||
await setPublicChartLanguage(payload.lang || publicChartLanguage);
|
||||
await setPublicChartLanguage(payload.lang || 'en');
|
||||
title.textContent = payload.title || publicTr('publicChart.title');
|
||||
hint.textContent = payload.hint || publicTr('publicChart.periodHint', { hours: payload.hours });
|
||||
canvas.setAttribute('aria-label', title.textContent);
|
||||
@@ -329,6 +423,18 @@ async function loadPublicChart(hours = requestedPublicChartHours()) {
|
||||
if (range) range.disabled = false;
|
||||
}
|
||||
|
||||
$('#publicCustomChartLegend')?.addEventListener('click', event => {
|
||||
const button = event.target.closest('button[data-public-chart-legend]');
|
||||
if (!button || !publicChartPayload) return;
|
||||
const index = Number(button.dataset.publicChartLegend);
|
||||
const item = publicChartPayload.series?.[index];
|
||||
if (!item) return;
|
||||
const key = publicChartSeriesKey(item, index);
|
||||
if (publicChartHiddenSeries.has(key)) publicChartHiddenSeries.delete(key);
|
||||
else publicChartHiddenSeries.add(key);
|
||||
redrawPublicChart();
|
||||
});
|
||||
|
||||
$('#publicChartHours')?.addEventListener('change', async event => {
|
||||
const hours = Number(event.currentTarget.value);
|
||||
if (!Number.isInteger(hours) || hours < 1 || hours > 87600) return;
|
||||
@@ -351,16 +457,15 @@ window.addEventListener('resize', () => {
|
||||
});
|
||||
|
||||
async function initPublicChart() {
|
||||
try {
|
||||
await setPublicChartLanguage(publicCookie('gree_controller_language') || PUBLIC_DEFAULT_LANGUAGE);
|
||||
} catch (error) {
|
||||
console.error('Public chart language initialization failed:', error);
|
||||
} finally {
|
||||
document.documentElement.classList.remove('i18n-loading');
|
||||
}
|
||||
bindPublicChartTouchGestures();
|
||||
try {
|
||||
await loadPublicChart();
|
||||
} catch (error) {
|
||||
if (!Object.keys(publicChartTranslations).length) {
|
||||
try { await setPublicChartLanguage('en'); } catch (languageError) {
|
||||
console.error('Public chart fallback language initialization failed:', languageError);
|
||||
}
|
||||
}
|
||||
const host = $('#publicChartError');
|
||||
host.hidden = false;
|
||||
host.textContent = publicChartTranslations['publicChart.loadFailed']
|
||||
@@ -369,6 +474,8 @@ async function initPublicChart() {
|
||||
$('#publicChartHint').textContent = '';
|
||||
$('#publicChartHours').disabled = false;
|
||||
drawEmpty($('#publicCustomChart'), host.textContent);
|
||||
} finally {
|
||||
document.documentElement.classList.remove('i18n-loading');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user