fix: 折线图纵轴优化——默认约80%头空间、取整到nice刻度,支持手动设置纵轴上限
- 纵轴自动刻度改为精细步进(1/1.5/2/2.5/3/4/5/6/8/10),数据最大值落在约80%处 - ChartInput 新增 yMax 字段(JSON 存储、可拓展),图表弹窗新增「纵轴上限」输入 - 新增 yMax 往返测试
This commit is contained in:
@@ -173,6 +173,7 @@ class ChartInput(BaseModel):
|
||||
taskId: str | None = None
|
||||
metricId: str | None = None
|
||||
days: Annotated[int, Field(ge=7, le=365)] = 30
|
||||
yMax: Annotated[int | None, Field(ge=1)] = None
|
||||
|
||||
|
||||
class ChartUpdate(ChartInput):
|
||||
|
||||
@@ -109,20 +109,32 @@ function periodText(task) { const metrics = task.metrics || []; return [...new S
|
||||
function summaryFor(taskId) { return state.summary?.tasks?.[taskId] || null; }
|
||||
function metricSummary(taskId, metricId) { return summaryFor(taskId)?.metrics?.find((metric) => metric.metricId === metricId) || null; }
|
||||
|
||||
function niceCeil(value) {
|
||||
if (!(value > 0)) return 1;
|
||||
const magnitude = Math.pow(10, Math.floor(Math.log10(value)));
|
||||
const steps = [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10];
|
||||
for (const step of steps) {
|
||||
const candidate = step * magnitude;
|
||||
if (value <= candidate) return Number(candidate.toFixed(6));
|
||||
}
|
||||
return magnitude * 10;
|
||||
}
|
||||
|
||||
function renderChartSvg(chart) {
|
||||
const points = chart.data?.points || [];
|
||||
if (!points.length || points.every((point) => Number(point.value) === 0)) return `<div class="chart-empty">暂无数据</div>`;
|
||||
const width = 600, height = 200, pad = { left: 34, right: 12, top: 14, bottom: 30 };
|
||||
const max = Math.max(...points.map((point) => Number(point.value) || 0), 1);
|
||||
const width = 600, height = 200, pad = { left: 34, right: 12, top: 16, bottom: 30 };
|
||||
const dataMax = Math.max(...points.map((point) => Number(point.value) || 0), 1);
|
||||
const scaleMax = chart.yMax ? Math.max(Number(chart.yMax), dataMax) : niceCeil(dataMax * 1.25);
|
||||
const x = (index) => pad.left + (points.length === 1 ? 0 : index * (width - pad.left - pad.right) / (points.length - 1));
|
||||
const y = (value) => pad.top + (height - pad.top - pad.bottom) * (1 - value / max);
|
||||
const y = (value) => pad.top + (height - pad.top - pad.bottom) * (1 - value / scaleMax);
|
||||
const coords = points.map((point, index) => `${x(index).toFixed(1)},${y(Number(point.value) || 0).toFixed(1)}`).join(" ");
|
||||
const areaPath = `M ${pad.left.toFixed(1)} ${y(0).toFixed(1)} L ${coords.replace(/ /g, " L ")} L ${x(points.length - 1).toFixed(1)} ${y(0).toFixed(1)} Z`;
|
||||
const labels = [points[0], points[Math.floor((points.length - 1) / 2)], points[points.length - 1]].filter((point, index, list) => point && list.findIndex((item) => item.date === point.date) === index).map((point) => {
|
||||
const index = points.indexOf(point); return `<text x="${x(index)}" y="${height - 7}" text-anchor="middle">${escapeHtml(point.date.slice(5))}</text>`;
|
||||
}).join("");
|
||||
const circles = points.map((point, index) => `<circle cx="${x(index).toFixed(1)}" cy="${y(Number(point.value) || 0).toFixed(1)}" r="2.2"/>`).join("");
|
||||
return `<svg class="chart-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" role="img" aria-label="${escapeHtml(chart.title)}"><line class="chart-guide" x1="${pad.left}" y1="${y(0)}" x2="${width - pad.right}" y2="${y(0)}"/><line class="chart-guide" x1="${pad.left}" y1="${y(max)}" x2="${width - pad.right}" y2="${y(max)}"/><text class="chart-max" x="4" y="${y(max) + 4}">${max}</text><path class="chart-area" d="${areaPath}"/><polyline points="${coords}"/><g class="chart-points">${circles}</g><g class="chart-labels">${labels}</g></svg>`;
|
||||
return `<svg class="chart-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" role="img" aria-label="${escapeHtml(chart.title)}"><line class="chart-guide" x1="${pad.left}" y1="${y(0)}" x2="${width - pad.right}" y2="${y(0)}"/><line class="chart-guide" x1="${pad.left}" y1="${y(scaleMax)}" x2="${width - pad.right}" y2="${y(scaleMax)}"/><text class="chart-max" x="4" y="${y(scaleMax) + 4}">${scaleMax}</text><path class="chart-area" d="${areaPath}"/><polyline points="${coords}"/><g class="chart-points">${circles}</g><g class="chart-labels">${labels}</g></svg>`;
|
||||
}
|
||||
|
||||
function renderReviewCharts() {
|
||||
@@ -147,7 +159,7 @@ function openChartDialog(chart = null) {
|
||||
state.editingChartId = chart?.chartId || null;
|
||||
const form = $("#chartForm"); form.reset(); $("#chartDialogTitle").textContent = chart ? "编辑图表" : "添加图表";
|
||||
$("#chartTaskSelect").innerHTML = `<option value="">全部任务</option>${state.tasks.map((task) => `<option value="${escapeHtml(task.taskId)}">${escapeHtml(task.name)}</option>`).join("")}`;
|
||||
form.title.value = chart?.title || ""; form.days.value = chart?.days || 30; $("#chartTaskSelect").value = chart?.taskId || ""; fillChartMetrics($("#chartTaskSelect").value, chart?.metricId || ""); $("#chartDialog").showModal();
|
||||
form.title.value = chart?.title || ""; form.days.value = chart?.days || 30; form.yMax.value = chart?.yMax || ""; $("#chartTaskSelect").value = chart?.taskId || ""; fillChartMetrics($("#chartTaskSelect").value, chart?.metricId || ""); $("#chartDialog").showModal();
|
||||
}
|
||||
|
||||
function bindReviewCharts() {
|
||||
@@ -565,7 +577,7 @@ $("#chartTaskSelect").addEventListener("change", (event) => fillChartMetrics(eve
|
||||
$("#chartForm").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const form = event.currentTarget;
|
||||
const payload = { title: form.title.value, taskId: form.taskId.value || null, metricId: form.metricId.value || null, days: Number(form.days.value) };
|
||||
const payload = { title: form.title.value, taskId: form.taskId.value || null, metricId: form.metricId.value || null, days: Number(form.days.value), yMax: form.yMax.value ? Number(form.yMax.value) : null };
|
||||
try {
|
||||
const editing = state.editingChartId ? state.charts.find((chart) => chart.chartId === state.editingChartId) : null;
|
||||
if (editing) payload.expectedVersion = editing.version;
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
<label>任务<select name="taskId" id="chartTaskSelect"><option value="">全部任务</option></select></label>
|
||||
<label>指标<select name="metricId" id="chartMetricSelect"><option value="">完成数</option></select></label>
|
||||
<label>天数<input name="days" type="number" min="7" max="365" value="30" required></label>
|
||||
<label>纵轴上限<input name="yMax" type="number" min="1" placeholder="自动"></label>
|
||||
</div>
|
||||
<div class="dialog-actions"><button class="button" id="cancelChartButton" type="button">取消</button><button class="button primary" type="submit">保存图表</button></div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user