Dämmerungsgrafik-Achsen und Overlay glätten
This commit is contained in:
+129
-63
@@ -544,6 +544,22 @@ function formatMonthShort(date, timeZone) {
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatDayMonthShort(date, timeZone) {
|
||||
return new Intl.DateTimeFormat('de-DE', {
|
||||
timeZone,
|
||||
day: '2-digit',
|
||||
month: '2-digit'
|
||||
}).format(date).replace(/\s/g, '');
|
||||
}
|
||||
|
||||
function formatMonthYearShort(date, timeZone) {
|
||||
return new Intl.DateTimeFormat('de-DE', {
|
||||
timeZone,
|
||||
month: 'short',
|
||||
year: '2-digit'
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatDuration(minutes) {
|
||||
const safeMinutes = Math.max(0, Math.round(minutes));
|
||||
const hours = Math.floor(safeMinutes / 60);
|
||||
@@ -649,6 +665,56 @@ function localDateFromDayIndex(dayDates, dayIndex) {
|
||||
return dayDates[dayIndex];
|
||||
}
|
||||
|
||||
function buildXAxisTicks(dayDates, timeZone) {
|
||||
const totalDays = dayDates.length;
|
||||
if (totalDays <= 1) {
|
||||
return [{ dayIndex: 0, label: formatDateGerman(dayDates[0], timeZone) }];
|
||||
}
|
||||
|
||||
if (totalDays <= 16) {
|
||||
return dayDates.map((date, dayIndex) => ({
|
||||
dayIndex,
|
||||
label: formatDayMonthShort(date, timeZone)
|
||||
}));
|
||||
}
|
||||
|
||||
if (totalDays <= 45) {
|
||||
return dayDates
|
||||
.map((date, dayIndex) => ({ date, dayIndex }))
|
||||
.filter((entry) => entry.dayIndex === 0 || entry.dayIndex === totalDays - 1 || entry.dayIndex % 7 === 0)
|
||||
.map((entry) => ({
|
||||
dayIndex: entry.dayIndex,
|
||||
label: formatDayMonthShort(entry.date, timeZone)
|
||||
}));
|
||||
}
|
||||
|
||||
if (totalDays <= 120) {
|
||||
return dayDates
|
||||
.map((date, dayIndex) => ({ date, dayIndex }))
|
||||
.filter((entry) => entry.dayIndex === 0 || entry.dayIndex === totalDays - 1 || entry.dayIndex % 14 === 0)
|
||||
.map((entry) => ({
|
||||
dayIndex: entry.dayIndex,
|
||||
label: formatDayMonthShort(entry.date, timeZone)
|
||||
}));
|
||||
}
|
||||
|
||||
const monthTicks = dayDates
|
||||
.map((date, dayIndex) => ({ date, dayIndex, parts: getTimeZoneParts(date, timeZone) }))
|
||||
.filter((entry) => entry.dayIndex === 0 || entry.parts.day === 1)
|
||||
.map((entry) => ({
|
||||
dayIndex: entry.dayIndex,
|
||||
label: totalDays <= 730
|
||||
? formatMonthShort(entry.date, timeZone).replace('.', '')
|
||||
: formatMonthYearShort(entry.date, timeZone).replace('.', '')
|
||||
}));
|
||||
|
||||
if (totalDays <= 730) {
|
||||
return monthTicks;
|
||||
}
|
||||
|
||||
return monthTicks.filter((entry, index) => index === 0 || index === monthTicks.length - 1 || index % 3 === 0);
|
||||
}
|
||||
|
||||
function sunAltitude(observer, utcDate) {
|
||||
const equator = Astronomy.Equator(Astronomy.Body.Sun, utcDate, observer, true, true);
|
||||
const horizon = Astronomy.Horizon(utcDate, observer, equator.ra, equator.dec, 'normal');
|
||||
@@ -857,6 +923,19 @@ function phaseIndexFromCurve(dayCurve, minutesOfDay) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function mixPhaseColors(leftPhaseIndex, rightPhaseIndex, blend) {
|
||||
const leftColor = PHASES[leftPhaseIndex].color;
|
||||
const rightColor = PHASES[rightPhaseIndex].color;
|
||||
const inverseBlend = 1 - blend;
|
||||
|
||||
return [
|
||||
Math.round((leftColor[0] * inverseBlend) + (rightColor[0] * blend)),
|
||||
Math.round((leftColor[1] * inverseBlend) + (rightColor[1] * blend)),
|
||||
Math.round((leftColor[2] * inverseBlend) + (rightColor[2] * blend)),
|
||||
Math.round((leftColor[3] * inverseBlend) + (rightColor[3] * blend))
|
||||
];
|
||||
}
|
||||
|
||||
function cloneSegments(segments) {
|
||||
return Array.isArray(segments) ? segments.map((segment) => [segment[0], segment[1]]) : [];
|
||||
}
|
||||
@@ -1056,7 +1135,9 @@ async function buildDisplayRaster(chart, columns, stepMinutes = 1) {
|
||||
for (let horizontalIndex = 0; horizontalIndex < horizontalSamples; horizontalIndex += 1) {
|
||||
const sampleX = (columnIndex + ((horizontalIndex + 0.5) / horizontalSamples)) / width;
|
||||
const dayPosition = sampleX * (chart.totalDays - 1);
|
||||
const dayIndex = Math.max(0, Math.min(chart.totalDays - 1, Math.round(dayPosition)));
|
||||
const leftDayIndex = Math.max(0, Math.min(chart.totalDays - 1, Math.floor(dayPosition)));
|
||||
const rightDayIndex = Math.max(0, Math.min(chart.totalDays - 1, Math.ceil(dayPosition)));
|
||||
const dayBlend = dayPosition - leftDayIndex;
|
||||
|
||||
for (let verticalIndex = 0; verticalIndex < verticalSamples; verticalIndex += 1) {
|
||||
const sampleMinutes = (rowIndex + ((verticalIndex + 0.5) / verticalSamples)) * stepMinutes;
|
||||
@@ -1064,8 +1145,12 @@ async function buildDisplayRaster(chart, columns, stepMinutes = 1) {
|
||||
0,
|
||||
Math.min(sourceRows - 1, Math.round(sampleMinutes / sourceStepMinutes))
|
||||
);
|
||||
const phaseIndex = sourceGrid[(sourceRowIndex * chart.totalDays) + dayIndex];
|
||||
const color = PHASES[phaseIndex].color;
|
||||
const rowOffset = sourceRowIndex * chart.totalDays;
|
||||
const leftPhaseIndex = sourceGrid[rowOffset + leftDayIndex];
|
||||
const rightPhaseIndex = sourceGrid[rowOffset + rightDayIndex];
|
||||
const color = leftDayIndex === rightDayIndex
|
||||
? PHASES[leftPhaseIndex].color
|
||||
: mixPhaseColors(leftPhaseIndex, rightPhaseIndex, dayBlend);
|
||||
|
||||
red += color[0];
|
||||
green += color[1];
|
||||
@@ -1107,7 +1192,7 @@ async function buildChartData(state, token) {
|
||||
imageCanvas.height = rows;
|
||||
const imageContext = imageCanvas.getContext('2d');
|
||||
const image = imageContext.createImageData(totalDays, rows);
|
||||
const monthBoundaries = [];
|
||||
const xAxisTicks = buildXAxisTicks(state.dayDates, state.timeZone);
|
||||
let longestDayMinutes = -1;
|
||||
let longestDayIndex = 0;
|
||||
let longestNightMinutes = -1;
|
||||
@@ -1120,10 +1205,6 @@ async function buildChartData(state, token) {
|
||||
|
||||
const localDate = localDateFromDayIndex(state.dayDates, dayIndex);
|
||||
const localParts = getTimeZoneParts(localDate, state.timeZone);
|
||||
if (dayIndex === 0 || localParts.day === 1) {
|
||||
monthBoundaries.push({ dayIndex, label: formatMonthShort(localDate, state.timeZone) });
|
||||
}
|
||||
|
||||
const dayCurve = buildThresholdIntervals(observer, localParts, state.timeZone);
|
||||
dayCurves.push(dayCurve);
|
||||
|
||||
@@ -1181,7 +1262,7 @@ async function buildChartData(state, token) {
|
||||
previewStepMinutes: state.stepMinutes,
|
||||
dayCurves,
|
||||
imageCanvas,
|
||||
monthBoundaries,
|
||||
xAxisTicks,
|
||||
longestDayIndex,
|
||||
longestDayMinutes,
|
||||
longestNightIndex,
|
||||
@@ -1246,19 +1327,15 @@ function drawChart(chart, targetContext = null, targetWidth = null, targetHeight
|
||||
context.textAlign = 'center';
|
||||
context.textBaseline = 'top';
|
||||
|
||||
chart.monthBoundaries.forEach((boundary, index) => {
|
||||
const x = margin.left + (boundary.dayIndex / chart.totalDays) * plotWidth;
|
||||
chart.xAxisTicks.forEach((tick) => {
|
||||
const x = margin.left + (tick.dayIndex / chart.totalDays) * plotWidth;
|
||||
context.beginPath();
|
||||
context.moveTo(x, margin.top);
|
||||
context.lineTo(x, margin.top + plotHeight);
|
||||
context.stroke();
|
||||
|
||||
const nextBoundary = chart.monthBoundaries[index + 1];
|
||||
const startDay = boundary.dayIndex;
|
||||
const endDay = nextBoundary ? nextBoundary.dayIndex : chart.totalDays;
|
||||
const midDay = startDay + ((endDay - startDay) / 2);
|
||||
const labelX = margin.left + (midDay / chart.totalDays) * plotWidth;
|
||||
context.fillText(boundary.label.replace('.', ''), labelX, margin.top + plotHeight + 10);
|
||||
const labelX = Math.min(margin.left + plotWidth - 24, Math.max(margin.left + 24, x));
|
||||
context.fillText(tick.label, labelX, margin.top + plotHeight + 10);
|
||||
});
|
||||
|
||||
if (chart.state.showLines) {
|
||||
@@ -1305,6 +1382,36 @@ function strokePointSeries(context, points) {
|
||||
}
|
||||
}
|
||||
|
||||
function strokePointRuns(context, points, maxJumpPixels) {
|
||||
if (points.length < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let run = [points[0]];
|
||||
|
||||
for (let index = 1; index < points.length; index += 1) {
|
||||
const point = points[index];
|
||||
const previous = points[index - 1];
|
||||
if (Math.abs(point.y - previous.y) > maxJumpPixels) {
|
||||
if (run.length > 1) {
|
||||
context.beginPath();
|
||||
strokePointSeries(context, run);
|
||||
context.stroke();
|
||||
}
|
||||
run = [point];
|
||||
continue;
|
||||
}
|
||||
|
||||
run.push(point);
|
||||
}
|
||||
|
||||
if (run.length > 1) {
|
||||
context.beginPath();
|
||||
strokePointSeries(context, run);
|
||||
context.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function fillPixelCurveSegment(context, points, fillStyle) {
|
||||
if (points.length < 2) {
|
||||
return;
|
||||
@@ -1370,6 +1477,7 @@ function drawThresholdLines(context, chart, margin, plotWidth, plotHeight) {
|
||||
1: 'astronomical'
|
||||
};
|
||||
const divisor = Math.max(1, curves.length - 1);
|
||||
const maxJumpPixels = (1.5 / 24) * plotHeight;
|
||||
|
||||
thresholds.forEach((threshold) => {
|
||||
context.strokeStyle = threshold.color;
|
||||
@@ -1383,29 +1491,8 @@ function drawThresholdLines(context, chart, margin, plotWidth, plotHeight) {
|
||||
const segments = intervalSegments(curves[curveIndex][curveKeyByPhaseIndex[threshold.phaseIndex]]);
|
||||
const segment = segments[segmentIndex] || null;
|
||||
if (!segment) {
|
||||
if (topPoints.length > 1) {
|
||||
context.beginPath();
|
||||
topPoints.forEach((point, index) => {
|
||||
if (index === 0) {
|
||||
context.moveTo(point.x, point.y);
|
||||
} else {
|
||||
context.lineTo(point.x, point.y);
|
||||
}
|
||||
});
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
if (bottomPoints.length > 1) {
|
||||
context.beginPath();
|
||||
bottomPoints.forEach((point, index) => {
|
||||
if (index === 0) {
|
||||
context.moveTo(point.x, point.y);
|
||||
} else {
|
||||
context.lineTo(point.x, point.y);
|
||||
}
|
||||
});
|
||||
context.stroke();
|
||||
}
|
||||
strokePointRuns(context, topPoints, maxJumpPixels);
|
||||
strokePointRuns(context, bottomPoints, maxJumpPixels);
|
||||
|
||||
topPoints.length = 0;
|
||||
bottomPoints.length = 0;
|
||||
@@ -1423,29 +1510,8 @@ function drawThresholdLines(context, chart, margin, plotWidth, plotHeight) {
|
||||
});
|
||||
}
|
||||
|
||||
if (topPoints.length > 1) {
|
||||
context.beginPath();
|
||||
topPoints.forEach((point, index) => {
|
||||
if (index === 0) {
|
||||
context.moveTo(point.x, point.y);
|
||||
} else {
|
||||
context.lineTo(point.x, point.y);
|
||||
}
|
||||
});
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
if (bottomPoints.length > 1) {
|
||||
context.beginPath();
|
||||
bottomPoints.forEach((point, index) => {
|
||||
if (index === 0) {
|
||||
context.moveTo(point.x, point.y);
|
||||
} else {
|
||||
context.lineTo(point.x, point.y);
|
||||
}
|
||||
});
|
||||
context.stroke();
|
||||
}
|
||||
strokePointRuns(context, topPoints, maxJumpPixels);
|
||||
strokePointRuns(context, bottomPoints, maxJumpPixels);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user