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}