diff --git a/frontend/web_app/src/pages/HomePage.jsx b/frontend/web_app/src/pages/HomePage.jsx index bfa2a0a..fb15c22 100644 --- a/frontend/web_app/src/pages/HomePage.jsx +++ b/frontend/web_app/src/pages/HomePage.jsx @@ -17,6 +17,8 @@ import { import { alignDesignCurveToActualCurve, buildPowerCurveModel, + getAxisMaximum, + getAxisTicks, getDrawableActualCurve, hasDrawablePowerCurveData, } from '../utils/powerCurveModel'; @@ -159,8 +161,8 @@ const DEFAULT_CHART_OPTIONS = { show_legend: true, show_filtered: false, scatter_size: 2, - x_tick_interval: '', - y_tick_interval: '', + x_tick_interval: '1', + y_tick_interval: '200', scatter_color: '#2563eb', scatter_opacity: 42, show_scatter: true, @@ -199,6 +201,12 @@ function normalizeChartOptions(options = {}) { ...DEFAULT_CHART_OPTIONS, ...currentOptions, text_scale: getTextScale(textScale), + x_tick_interval: Number(currentOptions.x_tick_interval) > 0 + ? currentOptions.x_tick_interval + : DEFAULT_CHART_OPTIONS.x_tick_interval, + y_tick_interval: Number(currentOptions.y_tick_interval) > 0 + ? currentOptions.y_tick_interval + : DEFAULT_CHART_OPTIONS.y_tick_interval, }; } @@ -738,39 +746,6 @@ function drawSmoothLine(context, points, toX, toY, style) { context.restore(); } -function getPositiveTickInterval(value) { - const interval = Number(value); - return Number.isFinite(interval) && interval > 0 ? interval : null; -} - -function getNiceTickInterval(min, max) { - const range = Math.max(Math.abs(max - min), 1); - const raw = range / 8; - const magnitude = 10 ** Math.floor(Math.log10(raw)); - const normalized = raw / magnitude; - const factor = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10; - return factor * magnitude; -} - -function resolveTickInterval(min, max, configuredInterval) { - return getPositiveTickInterval(configuredInterval) || getNiceTickInterval(min, max); -} - -function getAxisMaximum(max, configuredInterval) { - const interval = resolveTickInterval(0, max, configuredInterval); - return Math.max(interval, Math.ceil(Math.max(max, 0) / interval) * interval); -} - -function getAxisTicks(min, max, configuredInterval) { - const interval = resolveTickInterval(min, max, configuredInterval); - const first = Math.ceil((min - 1e-9) / interval) * interval; - const ticks = []; - for (let value = first; value <= max + interval * 1e-6 && ticks.length < 120; value += interval) { - ticks.push(Number(value.toFixed(10))); - } - return ticks; -} - function formatTick(value) { if (!Number.isFinite(value)) return ''; return Math.abs(value) >= 100 || Number.isInteger(value) ? value.toFixed(0) : value.toFixed(2).replace(/0+$/, '').replace(/\.$/, ''); @@ -840,6 +815,23 @@ function renderPowerCurvePng({ actualCurve, designCurve, scatterPoints, filtered context.strokeStyle = gridColor; context.lineWidth = 1; context.strokeRect(padding.left, padding.top, plotWidth, plotHeight); + context.strokeStyle = '#94a3b8'; + context.lineWidth = 2; + const inwardTickSize = 10; + for (const xValue of getAxisTicks(xMin, xMax, options.x_tick_interval)) { + const x = toX(xValue); + context.beginPath(); + context.moveTo(x, padding.top + plotHeight); + context.lineTo(x, padding.top + plotHeight - inwardTickSize); + context.stroke(); + } + for (const yValue of getAxisTicks(yMin, yMax, options.y_tick_interval)) { + const y = toY(yValue); + context.beginPath(); + context.moveTo(padding.left, y); + context.lineTo(padding.left + inwardTickSize, y); + context.stroke(); + } context.fillStyle = textColor; for (const xValue of getAxisTicks(xMin, xMax, options.x_tick_interval)) { context.textAlign = 'center'; @@ -1353,7 +1345,7 @@ function PowerCurveChart({ label: activeChartOptions.x_axis_label, stroke: activeChartOptions.text_color, size: xAxisSize, - gap: 8, + gap: 18, labelSize: axisLabelSize, labelGap: 10, font: `${screenTypography.tick}px "Segoe UI", sans-serif`, @@ -1361,12 +1353,13 @@ function PowerCurveChart({ splits: (u, axisIdx, min, max) => getAxisTicks(min, max, activeChartOptions.x_tick_interval), values: (u, splits) => splits.map(formatTick), grid: { show: activeChartOptions.show_grid, stroke: '#d8e0ea', width: 1 }, + ticks: { show: true, stroke: '#94a3b8', width: 1, size: -8 }, }, { label: activeChartOptions.y_axis_label, stroke: activeChartOptions.text_color, size: yAxisSize, - gap: 8, + gap: 18, labelSize: axisLabelSize, labelGap: 10, font: `${screenTypography.tick}px "Segoe UI", sans-serif`, @@ -1374,6 +1367,7 @@ function PowerCurveChart({ splits: (u, axisIdx, min, max) => getAxisTicks(min, max, activeChartOptions.y_tick_interval), values: (u, splits) => splits.map(formatTick), grid: { show: activeChartOptions.show_grid, stroke: '#d8e0ea', width: 1 }, + ticks: { show: true, stroke: '#94a3b8', width: 1, size: -8 }, }, ], series: [ @@ -3000,23 +2994,23 @@ export default function HomePage({ activeSection = 'settings' }) { {chartOptions.scatter_size}