fix: timeline chart linear scale, date filter on daily chart, null-safe chart init

This commit is contained in:
Vadym Samoilenko 2026-03-26 15:16:12 +00:00
parent 0ee3c3fbd9
commit 46db7476a3
3 changed files with 29 additions and 13 deletions

View file

@ -163,11 +163,12 @@ async def projects_overview(
@router.get("/timeline", response_model=list[DailyPoint])
async def timeline(
user: CurrentUser,
days: int = Query(default=30, ge=7, le=365),
from_date: date | None = Query(default=None, alias="from"),
to_date: date | None = Query(default=None, alias="to"),
db: AsyncSession = Depends(get_db),
):
to_date = date.today()
from_date = to_date - timedelta(days=days - 1)
from_date, to_date = _date_range(from_date, to_date)
days = (to_date - from_date).days + 1
result = await db.execute(
select(DailyStat.date, func.sum(DailyStat.total_hours), func.sum(DailyStat.session_count))

View file

@ -177,17 +177,26 @@ const ChartDefs = {
},
timeline(sessions) {
// Gantt-like bars: each session = one horizontal bar
// Filter out sessions with missing/invalid timestamps
// Gantt-like bars: filter invalid timestamps first
sessions = sessions.filter(s => {
const st = new Date(s.start_at).getTime();
const en = new Date(s.end_at).getTime();
return st > 0 && en > 0 && !isNaN(st) && !isNaN(en) && en >= st;
});
if (!sessions.length) return null;
const labels = sessions.map(s => s.project_name.substring(0, 20));
const starts = sessions.map(s => new Date(s.start_at).getTime());
const ends = sessions.map(s => new Date(s.end_at).getTime());
const data = sessions.map((s, i) => [starts[i], ends[i]]);
const pad = 15 * 60 * 1000; // 15min padding
const scaleMin = Math.min(...starts) - pad;
const scaleMax = Math.max(...ends) + pad;
const _fmt = ms => {
const d = new Date(ms);
return d.getHours().toString().padStart(2,'0') + ':' + d.getMinutes().toString().padStart(2,'0');
};
return {
type: 'bar',
@ -206,9 +215,14 @@ const ChartDefs = {
indexAxis: 'y',
scales: {
x: {
type: 'time',
time: { unit: 'hour', displayFormats: { hour: 'HH:mm' } },
type: 'linear',
min: scaleMin,
max: scaleMax,
grid: { color: GRID_COLOR },
ticks: {
maxTicksLimit: 12,
callback: v => _fmt(v),
},
},
y: { grid: { display: false } },
},
@ -219,8 +233,8 @@ const ChartDefs = {
callbacks: {
label: ctx => {
const s = sessions[ctx.dataIndex];
const from = new Date(s.start_at).toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit' });
const to = new Date(s.end_at).toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit' });
const from = _fmt(new Date(s.start_at).getTime());
const to = _fmt(new Date(s.end_at).getTime());
return ` ${from} ${to} (${s.active_hours.toFixed(1)}h)`;
},
},

View file

@ -40,7 +40,7 @@ const DashboardPage = (() => {
<div class="chart-wrap"><canvas id="chart-donut"></canvas></div>
</div>
<div class="chart-card wide">
<h3>Daily Activity (last 30 days)</h3>
<h3>Daily Activity</h3>
<div class="chart-wrap"><canvas id="chart-daily"></canvas></div>
</div>
<div class="chart-card">
@ -82,7 +82,7 @@ const DashboardPage = (() => {
const [summary, projects, timeline, monthly, dow, tools, activity] = await Promise.all([
Api.get('/api/dashboard/summary', params),
Api.get('/api/dashboard/projects', params),
Api.get('/api/dashboard/timeline', { days: 30 }),
Api.get('/api/dashboard/timeline', params),
Api.get('/api/dashboard/monthly'),
Api.get('/api/dashboard/dow'),
Api.get('/api/dashboard/tools', params),
@ -159,9 +159,10 @@ const DashboardPage = (() => {
));
// Timeline (today's sessions)
if (activity.length > 0) {
const timelineCfg = activity.length > 0 ? ChartDefs.timeline(activity) : null;
if (timelineCfg) {
document.getElementById('timeline-wrap').style.height = Math.max(160, activity.length * 36) + 'px';
makeChart('chart-timeline', ChartDefs.timeline(activity));
makeChart('chart-timeline', timelineCfg);
} else {
const wrap = document.getElementById('timeline-wrap');
if (wrap) wrap.innerHTML = '<div class="empty" style="padding:20px">No sessions today</div>';