Remove Gaze Samples stat and Attention Timeline section from report
Browse filesTrims the stat row to the 4 cards that matter for a glance (Session
Length, Elements Fixated, Most Attended, Mouse Events) and drops the
Attention Timeline chart along with its now-unused data plumbing
(compute_timeline(), TIMELINE/DURATION JS constants, legend blurb).
- analysis.py +4 -107
analysis.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
"""
|
| 2 |
analysis.py
|
| 3 |
Turns a raw InsightUX session (gaze_log.jsonl + dom_log.jsonl + screenshots/)
|
| 4 |
-
into a self-contained HTML report: ranked attention (in plain language)
|
| 5 |
-
|
| 6 |
-
|
| 7 |
|
| 8 |
Usage:
|
| 9 |
from analysis import generate_report
|
|
@@ -201,25 +201,6 @@ def compute_dwell_ranking(attributed):
|
|
| 201 |
return ranking
|
| 202 |
|
| 203 |
|
| 204 |
-
def compute_timeline(attributed):
|
| 205 |
-
segments = []
|
| 206 |
-
if not attributed:
|
| 207 |
-
return segments
|
| 208 |
-
cur_raw = attributed[0][1]
|
| 209 |
-
seg_start = attributed[0][0]
|
| 210 |
-
last_t = attributed[0][0]
|
| 211 |
-
for t, label in attributed[1:]:
|
| 212 |
-
if label != cur_raw:
|
| 213 |
-
segments.append({"start": round(seg_start, 2), "end": round(last_t, 2),
|
| 214 |
-
"label": friendly_label(cur_raw) if cur_raw else None})
|
| 215 |
-
cur_raw = label
|
| 216 |
-
seg_start = t
|
| 217 |
-
last_t = t
|
| 218 |
-
segments.append({"start": round(seg_start, 2), "end": round(last_t, 2),
|
| 219 |
-
"label": friendly_label(cur_raw) if cur_raw else None})
|
| 220 |
-
return segments
|
| 221 |
-
|
| 222 |
-
|
| 223 |
def session_summary(gaze, dom):
|
| 224 |
if not gaze:
|
| 225 |
return {"duration": 0.0, "url": None, "samples": 0}
|
|
@@ -402,7 +383,7 @@ __THEME_CSS__
|
|
| 402 |
.topbar .actions { margin-left: auto; display: flex; gap: 8px; }
|
| 403 |
.topbar .actions .iux-btn { padding: 8px 13px; font-size: 12px; }
|
| 404 |
|
| 405 |
-
.stat-row { display: grid; grid-template-columns: repeat(
|
| 406 |
.stat {
|
| 407 |
padding: 16px 18px; position: relative; overflow: hidden;
|
| 408 |
}
|
|
@@ -550,7 +531,6 @@ __THEME_CSS__
|
|
| 550 |
<div class="stat iux-card hoverable"><div class="icon-badge">__LAYERS_ICON__</div><div class="v">__NUM_ELEMENTS__</div><div class="l">Elements Fixated</div></div>
|
| 551 |
<div class="stat iux-card hoverable"><div class="icon-badge">__TARGET_ICON__</div><div class="v" style="font-size:14px;">__TOP_LABEL__</div><div class="l">Most Attended</div></div>
|
| 552 |
<div class="stat iux-card hoverable"><div class="icon-badge">__CURSOR_ICON__</div><div class="v">__CLICK_COUNT__</div><div class="l">Mouse Events</div></div>
|
| 553 |
-
<div class="stat iux-card hoverable"><div class="icon-badge">__TYPE_ICON__</div><div class="v">__SAMPLES__</div><div class="l">Gaze Samples</div></div>
|
| 554 |
</div>
|
| 555 |
|
| 556 |
<div class="grid">
|
|
@@ -575,8 +555,6 @@ __THEME_CSS__
|
|
| 575 |
the page automatically whenever you scrolled far enough that the view
|
| 576 |
changed meaningfully. Use the full-session export buttons to download
|
| 577 |
one stitched image covering the entire page.</p>
|
| 578 |
-
<p><b>Timeline</b> — the same attention data laid out across time,
|
| 579 |
-
so you can see the order things were looked at, not just the totals.</p>
|
| 580 |
</div>
|
| 581 |
</div>
|
| 582 |
<div class="panel iux-card full iux-fade-in">
|
|
@@ -613,10 +591,6 @@ __THEME_CSS__
|
|
| 613 |
</div>
|
| 614 |
<div class="elem-chart-hint" id="elemChartHint" style="display:none;">Scroll to see every element →</div>
|
| 615 |
</div>
|
| 616 |
-
<div class="panel iux-card full iux-fade-in">
|
| 617 |
-
<h2>__CLOCK_ICON__ Attention Timeline</h2>
|
| 618 |
-
<canvas id="timeline" width="1240" height="320"></canvas>
|
| 619 |
-
</div>
|
| 620 |
<div class="panel iux-card iux-fade-in">
|
| 621 |
<h2>__CURSOR_ICON__ Mouse — Most Interacted Elements</h2>
|
| 622 |
<div id="mouseInterests"></div>
|
|
@@ -665,9 +639,7 @@ document.getElementById('themeToggle').addEventListener('click', function(){
|
|
| 665 |
});
|
| 666 |
|
| 667 |
const RANKING = __RANKING_JSON__;
|
| 668 |
-
const TIMELINE = __TIMELINE_JSON__;
|
| 669 |
const SEGMENTS = __SEGMENTS_JSON__;
|
| 670 |
-
const DURATION = __DURATION_JSON__;
|
| 671 |
const MOUSE_INTERESTS = __MOUSE_INTERESTS_JSON__;
|
| 672 |
const MOUSE_CLICKS = __MOUSE_CLICKS_JSON__;
|
| 673 |
const MOUSE_SEGMENTS = __MOUSE_SEGMENTS_JSON__;
|
|
@@ -1587,78 +1559,6 @@ function showToast(message, isError, onRetry){
|
|
| 1587 |
window.addEventListener('resize', function(){ layout(); paint(); });
|
| 1588 |
})();
|
| 1589 |
|
| 1590 |
-
// ---------- Timeline ----------
|
| 1591 |
-
(function(){
|
| 1592 |
-
const cv = document.getElementById('timeline');
|
| 1593 |
-
const ctx = cv.getContext('2d');
|
| 1594 |
-
|
| 1595 |
-
function paintTimeline(){
|
| 1596 |
-
const bg = getComputedStyle(document.documentElement).getPropertyValue('--iux-bg-alt').trim() || '#171923';
|
| 1597 |
-
const dim = getComputedStyle(document.documentElement).getPropertyValue('--iux-text-dim').trim() || '#9a95b3';
|
| 1598 |
-
ctx.clearRect(0, 0, cv.width, cv.height);
|
| 1599 |
-
ctx.fillStyle = bg;
|
| 1600 |
-
ctx.fillRect(0, 0, cv.width, cv.height);
|
| 1601 |
-
if (!TIMELINE.length || DURATION <= 0) { return; }
|
| 1602 |
-
|
| 1603 |
-
const labels = [...new Set(TIMELINE.filter(s => s.label).map(s => s.label))];
|
| 1604 |
-
const rowH = Math.min(28, (cv.height - 40) / Math.max(labels.length, 1));
|
| 1605 |
-
const leftPad = 220, topPad = 10, plotW = cv.width - leftPad - 20;
|
| 1606 |
-
|
| 1607 |
-
ctx.font = '11px -apple-system, Arial';
|
| 1608 |
-
ctx.fillStyle = dim;
|
| 1609 |
-
labels.forEach((lab, i) => {
|
| 1610 |
-
const y = topPad + i * rowH;
|
| 1611 |
-
ctx.fillText(lab.length > 28 ? lab.slice(0,28)+'…' : lab, 4, y + rowH*0.65);
|
| 1612 |
-
});
|
| 1613 |
-
|
| 1614 |
-
const colors = ['#6366F1', '#8B5CF6', '#C084FC', '#67E8F9', '#FBBF24', '#34D399'];
|
| 1615 |
-
TIMELINE.forEach(seg => {
|
| 1616 |
-
if (!seg.label) return;
|
| 1617 |
-
const rowIdx = labels.indexOf(seg.label);
|
| 1618 |
-
const x = leftPad + (seg.start / DURATION) * plotW;
|
| 1619 |
-
const w = Math.max(2, ((seg.end - seg.start) / DURATION) * plotW);
|
| 1620 |
-
const y = topPad + rowIdx * rowH + 3;
|
| 1621 |
-
ctx.fillStyle = colors[rowIdx % colors.length];
|
| 1622 |
-
const rr = Math.min(4, (rowH - 6) / 2, w / 2);
|
| 1623 |
-
ctx.beginPath();
|
| 1624 |
-
ctx.moveTo(x + rr, y);
|
| 1625 |
-
ctx.arcTo(x + w, y, x + w, y + (rowH - 6), rr);
|
| 1626 |
-
ctx.arcTo(x + w, y + (rowH - 6), x, y + (rowH - 6), rr);
|
| 1627 |
-
ctx.arcTo(x, y + (rowH - 6), x, y, rr);
|
| 1628 |
-
ctx.arcTo(x, y, x + w, y, rr);
|
| 1629 |
-
ctx.closePath();
|
| 1630 |
-
ctx.fill();
|
| 1631 |
-
});
|
| 1632 |
-
|
| 1633 |
-
ctx.strokeStyle = '#302a3d55';
|
| 1634 |
-
ctx.beginPath();
|
| 1635 |
-
ctx.moveTo(leftPad, cv.height - 18);
|
| 1636 |
-
ctx.lineTo(cv.width - 10, cv.height - 18);
|
| 1637 |
-
ctx.stroke();
|
| 1638 |
-
ctx.fillStyle = dim;
|
| 1639 |
-
[0, 0.25, 0.5, 0.75, 1].forEach(f => {
|
| 1640 |
-
const x = leftPad + f * plotW;
|
| 1641 |
-
ctx.fillText((f * DURATION).toFixed(1) + 's', x, cv.height - 4);
|
| 1642 |
-
});
|
| 1643 |
-
}
|
| 1644 |
-
|
| 1645 |
-
paintTimeline();
|
| 1646 |
-
window.insightuxRepaintCanvases.push(paintTimeline);
|
| 1647 |
-
|
| 1648 |
-
const leftPadForHit = 220, topPadForHit = 10;
|
| 1649 |
-
cv.addEventListener('mousemove', function(e){
|
| 1650 |
-
cv.title = '';
|
| 1651 |
-
if (!TIMELINE.length || DURATION <= 0) return;
|
| 1652 |
-
const rect = cv.getBoundingClientRect();
|
| 1653 |
-
const scaleX = cv.width / rect.width;
|
| 1654 |
-
const mx = (e.clientX - rect.left) * scaleX;
|
| 1655 |
-
const plotW = cv.width - leftPadForHit - 20;
|
| 1656 |
-
if (mx < leftPadForHit) return;
|
| 1657 |
-
const t = ((mx - leftPadForHit) / plotW) * DURATION;
|
| 1658 |
-
const hit = TIMELINE.find(s => s.label && t >= s.start && t <= s.end);
|
| 1659 |
-
if (hit) cv.title = hit.label + ' — ' + hit.start.toFixed(1) + 's to ' + hit.end.toFixed(1) + 's';
|
| 1660 |
-
});
|
| 1661 |
-
})();
|
| 1662 |
</script>
|
| 1663 |
</body>
|
| 1664 |
</html>
|
|
@@ -1670,7 +1570,6 @@ def generate_report(session_dir):
|
|
| 1670 |
attributed = attribute_gaze(gaze, dom)
|
| 1671 |
|
| 1672 |
ranking = compute_dwell_ranking(attributed)
|
| 1673 |
-
timeline = compute_timeline(attributed)
|
| 1674 |
segments = build_screenshot_segments(gaze, dom, session_dir)
|
| 1675 |
summary = session_summary(gaze, dom)
|
| 1676 |
mouse = summarize_mouse(session_dir)
|
|
@@ -1698,9 +1597,7 @@ def generate_report(session_dir):
|
|
| 1698 |
html = html.replace("__TOP_LABEL__", top_label)
|
| 1699 |
html = html.replace("__CLICK_COUNT__", str(mouse["click_count"]))
|
| 1700 |
html = html.replace("__RANKING_JSON__", json.dumps(ranking))
|
| 1701 |
-
html = html.replace("__TIMELINE_JSON__", json.dumps(timeline))
|
| 1702 |
html = html.replace("__SEGMENTS_JSON__", json.dumps(segments))
|
| 1703 |
-
html = html.replace("__DURATION_JSON__", json.dumps(summary["duration"]))
|
| 1704 |
html = html.replace("__MOUSE_INTERESTS_JSON__", json.dumps(mouse["interests"]))
|
| 1705 |
html = html.replace("__MOUSE_CLICKS_JSON__", json.dumps(mouse["clicks"]))
|
| 1706 |
html = html.replace("__MOUSE_SEGMENTS_JSON__", json.dumps(mouse_segments))
|
|
|
|
| 1 |
"""
|
| 2 |
analysis.py
|
| 3 |
Turns a raw InsightUX session (gaze_log.jsonl + dom_log.jsonl + screenshots/)
|
| 4 |
+
into a self-contained HTML report: ranked attention (in plain language)
|
| 5 |
+
and per-scroll-position heatmaps drawn directly on top of what the page
|
| 6 |
+
actually looked like.
|
| 7 |
|
| 8 |
Usage:
|
| 9 |
from analysis import generate_report
|
|
|
|
| 201 |
return ranking
|
| 202 |
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
def session_summary(gaze, dom):
|
| 205 |
if not gaze:
|
| 206 |
return {"duration": 0.0, "url": None, "samples": 0}
|
|
|
|
| 383 |
.topbar .actions { margin-left: auto; display: flex; gap: 8px; }
|
| 384 |
.topbar .actions .iux-btn { padding: 8px 13px; font-size: 12px; }
|
| 385 |
|
| 386 |
+
.stat-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 22px; }
|
| 387 |
.stat {
|
| 388 |
padding: 16px 18px; position: relative; overflow: hidden;
|
| 389 |
}
|
|
|
|
| 531 |
<div class="stat iux-card hoverable"><div class="icon-badge">__LAYERS_ICON__</div><div class="v">__NUM_ELEMENTS__</div><div class="l">Elements Fixated</div></div>
|
| 532 |
<div class="stat iux-card hoverable"><div class="icon-badge">__TARGET_ICON__</div><div class="v" style="font-size:14px;">__TOP_LABEL__</div><div class="l">Most Attended</div></div>
|
| 533 |
<div class="stat iux-card hoverable"><div class="icon-badge">__CURSOR_ICON__</div><div class="v">__CLICK_COUNT__</div><div class="l">Mouse Events</div></div>
|
|
|
|
| 534 |
</div>
|
| 535 |
|
| 536 |
<div class="grid">
|
|
|
|
| 555 |
the page automatically whenever you scrolled far enough that the view
|
| 556 |
changed meaningfully. Use the full-session export buttons to download
|
| 557 |
one stitched image covering the entire page.</p>
|
|
|
|
|
|
|
| 558 |
</div>
|
| 559 |
</div>
|
| 560 |
<div class="panel iux-card full iux-fade-in">
|
|
|
|
| 591 |
</div>
|
| 592 |
<div class="elem-chart-hint" id="elemChartHint" style="display:none;">Scroll to see every element →</div>
|
| 593 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
<div class="panel iux-card iux-fade-in">
|
| 595 |
<h2>__CURSOR_ICON__ Mouse — Most Interacted Elements</h2>
|
| 596 |
<div id="mouseInterests"></div>
|
|
|
|
| 639 |
});
|
| 640 |
|
| 641 |
const RANKING = __RANKING_JSON__;
|
|
|
|
| 642 |
const SEGMENTS = __SEGMENTS_JSON__;
|
|
|
|
| 643 |
const MOUSE_INTERESTS = __MOUSE_INTERESTS_JSON__;
|
| 644 |
const MOUSE_CLICKS = __MOUSE_CLICKS_JSON__;
|
| 645 |
const MOUSE_SEGMENTS = __MOUSE_SEGMENTS_JSON__;
|
|
|
|
| 1559 |
window.addEventListener('resize', function(){ layout(); paint(); });
|
| 1560 |
})();
|
| 1561 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1562 |
</script>
|
| 1563 |
</body>
|
| 1564 |
</html>
|
|
|
|
| 1570 |
attributed = attribute_gaze(gaze, dom)
|
| 1571 |
|
| 1572 |
ranking = compute_dwell_ranking(attributed)
|
|
|
|
| 1573 |
segments = build_screenshot_segments(gaze, dom, session_dir)
|
| 1574 |
summary = session_summary(gaze, dom)
|
| 1575 |
mouse = summarize_mouse(session_dir)
|
|
|
|
| 1597 |
html = html.replace("__TOP_LABEL__", top_label)
|
| 1598 |
html = html.replace("__CLICK_COUNT__", str(mouse["click_count"]))
|
| 1599 |
html = html.replace("__RANKING_JSON__", json.dumps(ranking))
|
|
|
|
| 1600 |
html = html.replace("__SEGMENTS_JSON__", json.dumps(segments))
|
|
|
|
| 1601 |
html = html.replace("__MOUSE_INTERESTS_JSON__", json.dumps(mouse["interests"]))
|
| 1602 |
html = html.replace("__MOUSE_CLICKS_JSON__", json.dumps(mouse["clicks"]))
|
| 1603 |
html = html.replace("__MOUSE_SEGMENTS_JSON__", json.dumps(mouse_segments))
|