Files
pet16704/index.html
T
rodado 891d58ec55 feat: linha do tempo processual dos autos vinculados Pet16704
Substitui o gitGraph do Mermaid (ilegível com 3.437 commits) por uma
timeline interativa com eixo temporal real, filtros por instituição,
tipo de documento e pessoas mencionadas (extraidas via NER local com
pierreguillou/ner-bert-base-cased-pt-lenerbr).
2026-09-13 10:37:10 +02:00

1262 lines
51 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Linha do Tempo Processual</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,500;8..60,600;8..60,700&family=Public+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap">
<style>
[hidden]:not([hidden=until-found i]){ display:none !important; }
:root{
color-scheme: light;
--page: #f9f9f7;
--surface: #fcfcfb;
--ink: #0b0b0b;
--ink-2: #52514e;
--ink-3: #898781;
--grid: #e1e0d9;
--baseline: #c3c2b7;
--border: rgba(11,11,11,0.10);
--ring: #0b0b0b;
--s1: #2a78d6; /* STF · Relatoria */
--s2: #eb6834; /* PGR */
--s3: #1baf7a; /* Polícia Federal */
--s4: #eda100; /* Partes e Defesas */
--s5: #e87ba4; /* Secretaria e Cartório */
--accent: var(--s1);
--accent-wash: rgba(42,120,214,0.10);
}
@media (prefers-color-scheme: dark){
:root:not([data-theme="light"]){
color-scheme: dark;
--page: #0d0d0d;
--surface: #1a1a19;
--ink: #ffffff;
--ink-2: #c3c2b7;
--ink-3: #898781;
--grid: #2c2c2a;
--baseline: #383835;
--border: rgba(255,255,255,0.10);
--ring: #ffffff;
--s1: #3987e5;
--s2: #d95926;
--s3: #199e70;
--s4: #c98500;
--s5: #d55181;
--accent-wash: rgba(57,135,229,0.16);
}
}
:root[data-theme="dark"]{
color-scheme: dark;
--page: #0d0d0d;
--surface: #1a1a19;
--ink: #ffffff;
--ink-2: #c3c2b7;
--ink-3: #898781;
--grid: #2c2c2a;
--baseline: #383835;
--border: rgba(255,255,255,0.10);
--ring: #ffffff;
--s1: #3987e5;
--s2: #d95926;
--s3: #199e70;
--s4: #c98500;
--s5: #d55181;
--accent-wash: rgba(57,135,229,0.16);
}
*{ box-sizing:border-box; }
body{
background:var(--page);
color:var(--ink);
font-family:"Public Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
padding-inline: max(16px, calc((100vw - 1180px)/2));
padding-block: 28px 60px;
}
h1,h2,h3{ font-family:"Source Serif 4", Georgia, serif; text-wrap:balance; margin:0; }
.mono{ font-family:"IBM Plex Mono", ui-monospace, SFMono-Regular, monospace; }
.tabnum{ font-variant-numeric: tabular-nums; }
/* ---- header ---- */
header{ display:flex; flex-direction:column; gap:14px; margin-bottom:22px; }
h1{ font-size:clamp(24px,3.2vw,32px); font-weight:600; letter-spacing:-0.01em; }
.lede{ max-width:62ch; color:var(--ink-2); font-size:14.5px; line-height:1.55; margin:0; }
.lede .mono{ font-size:12.5px; background:var(--accent-wash); color:var(--ink); padding:1px 6px; border-radius:4px; }
.stats{ display:flex; flex-wrap:wrap; gap:10px; margin-top:4px; }
.stat{
background:var(--surface); border:1px solid var(--border); border-radius:10px;
padding:10px 14px; min-width:126px; flex:1 1 126px;
}
.stat .k{ font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--ink-3); }
.stat .v{ font-family:"Source Serif 4",serif; font-size:20px; font-weight:600; margin-top:2px; }
.stat.link{ cursor:pointer; }
.stat.link:hover{ border-color: var(--accent); }
.stat.link:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
/* ---- controls ---- */
.controls{
display:flex; flex-wrap:wrap; align-items:center; gap:10px 16px;
padding:10px 0 16px; border-bottom:1px solid var(--grid); margin-bottom:16px;
}
.search{
display:flex; align-items:center; gap:8px;
background:var(--surface); border:1px solid var(--border); border-radius:8px;
padding:7px 10px; min-width:220px; flex:1 1 220px; max-width:320px;
}
.search svg{ flex:none; opacity:.55; }
.search input{
border:0; outline:0; background:transparent; color:var(--ink); font:inherit; width:100%; font-size:13.5px;
}
.search input::placeholder{ color:var(--ink-3); }
.legend{ display:flex; flex-wrap:wrap; gap:6px; }
.chip{
display:flex; align-items:center; gap:6px;
border:1px solid var(--border); background:var(--surface); border-radius:999px;
padding:5px 10px 5px 8px; font-size:12.5px; color:var(--ink-2); cursor:pointer; user-select:none;
transition: opacity .15s ease;
}
.chip:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; }
.chip .dot{ width:9px; height:9px; border-radius:50%; flex:none; }
.chip .n{ color:var(--ink-3); }
.chip[data-off="true"]{ opacity:.4; }
.chip[data-off="true"] .dot{ background:transparent !important; border:1.5px solid var(--ink-3); }
.dropdown{ position:relative; }
.dropdown-btn{
display:flex; align-items:center; gap:6px;
border:1px solid var(--border); background:var(--surface); color:var(--ink-2);
font:inherit; font-size:12.5px; padding:7px 12px; border-radius:8px; cursor:pointer;
}
.dropdown-btn:hover{ border-color:var(--accent); color:var(--ink); }
.dropdown-btn .arrow{ font-size:10px; opacity:.6; }
.dropdown-btn[aria-expanded="true"]{ border-color:var(--accent); background:var(--accent-wash); color:var(--ink); }
.dropdown-btn .tag{ background:var(--accent); color:#fff; border-radius:999px; font-size:10.5px; padding:1px 6px; }
.dropdown-panel{
position:absolute; top:calc(100% + 6px); left:0; z-index:8; width:280px;
background:var(--surface); border:1px solid var(--border); border-radius:10px;
box-shadow:0 10px 28px rgba(0,0,0,.18); padding:10px; display:flex; flex-direction:column; gap:8px;
}
.dropdown-search input{
width:100%; border:1px solid var(--border); background:var(--page); color:var(--ink);
border-radius:6px; padding:6px 8px; font:inherit; font-size:12.5px; outline:0;
}
.dropdown-search input:focus{ border-color:var(--accent); }
.dropdown-actions{ display:flex; gap:8px; }
.dropdown-actions button{
border:0; background:none; color:var(--accent); font:inherit; font-size:12px; cursor:pointer; padding:0;
}
.dropdown-actions button:hover{ text-decoration:underline; }
.dropdown-list{ max-height:260px; overflow-y:auto; display:flex; flex-direction:column; gap:1px; }
.type-row{
display:flex; align-items:center; gap:8px; padding:5px 4px; border-radius:6px; cursor:pointer; font-size:12.5px;
}
.type-row:hover{ background:var(--accent-wash); }
.type-row input{ flex:none; accent-color: var(--accent); }
.type-row .lbl{ flex:1 1 auto; color:var(--ink); }
.type-row .n{ color:var(--ink-3); font-size:11.5px; }
.type-row.hidden-by-search{ display:none; }
.dropdown-panel-wide{ width:340px; }
.dropdown-hint{ font-size:11px; color:var(--ink-3); }
.role-chips{ display:flex; flex-wrap:wrap; gap:5px; padding-bottom:4px; border-bottom:1px solid var(--grid); }
.role-chip{
border:1px solid var(--border); background:var(--page); color:var(--ink-2);
font:inherit; font-size:11px; padding:3px 8px; border-radius:999px; cursor:pointer;
}
.role-chip[aria-pressed="true"]{ background:var(--accent-wash); border-color:var(--accent); color:var(--ink); }
.type-row .role-badge{
flex:none; font-size:10px; color:var(--ink-3); background:var(--page);
border:1px solid var(--border); border-radius:5px; padding:1px 5px; white-space:nowrap;
}
.type-row.hidden-by-role{ display:none; }
.spacer{ flex:1 1 auto; }
.segmented{ display:flex; border:1px solid var(--border); border-radius:8px; overflow:hidden; }
.segmented button{
border:0; background:var(--surface); color:var(--ink-2); font:inherit; font-size:12.5px;
padding:7px 12px; cursor:pointer;
}
.segmented button + button{ border-left:1px solid var(--border); }
.segmented button[aria-pressed="true"]{ background:var(--accent); color:#fff; }
.presets{ display:flex; gap:6px; }
.presets button{
border:1px solid var(--border); background:var(--surface); color:var(--ink-2);
font:inherit; font-size:12.5px; padding:6px 10px; border-radius:7px; cursor:pointer;
}
.presets button:hover{ border-color:var(--accent); color:var(--ink); }
.presets button[aria-pressed="true"]{ background:var(--accent-wash); border-color:var(--accent); color:var(--ink); }
label.check{ display:flex; align-items:center; gap:6px; font-size:12.5px; color:var(--ink-2); cursor:pointer; }
/* ---- chart ---- */
#chartView{ display:flex; flex-direction:column; gap:4px; }
.overview-card, .detail-card{
background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:12px;
}
.overview-card{ position:relative; }
.overview-title{ font-size:11px; text-transform:uppercase; letter-spacing:.06em; color:var(--ink-3); margin-bottom:6px; }
.overview-wrap{ position:relative; }
#overviewCanvas{ display:block; width:100%; cursor:crosshair; touch-action:none; }
.brush{
position:absolute; top:0; bottom:0; background:var(--accent-wash);
border-left:2px solid var(--accent); border-right:2px solid var(--accent);
cursor:grab;
}
.brush::before, .brush::after{
content:""; position:absolute; top:0; bottom:0; width:8px; cursor:ew-resize;
}
.brush::before{ left:-4px; }
.brush::after{ right:-4px; }
.detail-card{ overflow-x:auto; }
.detail-row{ display:flex; min-width:600px; }
.lane-sidebar{ flex:none; width:132px; padding-top:0; }
.lane-row{ display:flex; align-items:center; gap:7px; box-sizing:border-box; border-bottom:1px solid var(--grid); }
.lane-row .dot{ width:9px; height:9px; border-radius:50%; flex:none; }
.lane-row .lbl{ font-size:12px; line-height:1.25; color:var(--ink-2); }
.lane-row .lbl b{ display:block; color:var(--ink); font-weight:600; }
.detail-canvas-wrap{ position:relative; flex:1 1 auto; min-width:0; }
#detailCanvas{ display:block; width:100%; cursor:grab; touch-action:none; }
#detailCanvas.dragging{ cursor:grabbing; }
.hover-ring{
position:absolute; pointer-events:none; border-radius:50%;
border:1.5px solid var(--ring); background:transparent;
transform:translate(-50%,-50%); display:none;
}
#tooltip{
position:absolute; pointer-events:none; max-width:320px; z-index:5;
background:var(--surface); border:1px solid var(--border); border-radius:9px;
box-shadow:0 6px 20px rgba(0,0,0,.16); padding:10px 12px; font-size:12.5px;
color:var(--ink-2); line-height:1.5; display:none;
}
#tooltip .row1{ display:flex; align-items:center; gap:7px; margin-bottom:4px; }
#tooltip .row1 .dot{ width:8px; height:8px; border-radius:50%; flex:none; }
#tooltip .date{ font-family:"IBM Plex Mono",monospace; font-size:12px; color:var(--ink); font-weight:500; }
#tooltip .lane{ font-size:11px; color:var(--ink-3); }
#tooltip .id{ font-family:"IBM Plex Mono",monospace; font-size:11px; color:var(--ink-3); margin-bottom:3px; }
#tooltip .text{ color:var(--ink); }
.count-line{ font-size:12px; color:var(--ink-3); margin:8px 2px 0; }
/* ---- table ---- */
#tableView{ background:var(--surface); border:1px solid var(--border); border-radius:12px; overflow:hidden; }
.table-scroll{ overflow-x:auto; }
table{ width:100%; border-collapse:collapse; font-size:13px; min-width:760px; }
thead th{
text-align:left; font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:var(--ink-3);
font-weight:600; padding:10px 12px; border-bottom:1px solid var(--grid); background:var(--surface);
position:sticky; top:0; cursor:pointer; white-space:nowrap;
}
thead th:hover{ color:var(--ink); }
thead th .arrow{ opacity:.5; margin-left:3px; }
tbody td{ padding:9px 12px; border-bottom:1px solid var(--grid); vertical-align:top; color:var(--ink-2); }
tbody tr:hover td{ background:var(--accent-wash); }
td.date{ font-family:"IBM Plex Mono",monospace; font-size:12px; white-space:nowrap; color:var(--ink); }
td.lane{ white-space:nowrap; }
td.lane .dot{ width:8px; height:8px; border-radius:50%; display:inline-block; margin-right:6px; }
td.idcol{ font-family:"IBM Plex Mono",monospace; font-size:11.5px; color:var(--ink-3); white-space:nowrap; }
td.type{ white-space:nowrap; color:var(--ink-2); }
td.people{ color:var(--ink-2); max-width:220px; }
td.text{ color:var(--ink); max-width:520px; }
td.file{ font-family:"IBM Plex Mono",monospace; font-size:10.5px; color:var(--ink-3); max-width:220px; overflow-wrap:anywhere; }
.table-foot{ display:flex; align-items:center; justify-content:center; gap:12px; padding:14px; }
.table-foot button{
border:1px solid var(--border); background:var(--page); color:var(--ink);
font:inherit; font-size:13px; padding:8px 16px; border-radius:8px; cursor:pointer;
}
.table-foot button:hover{ border-color:var(--accent); }
footer{ margin-top:28px; font-size:11.5px; color:var(--ink-3); border-top:1px solid var(--grid); padding-top:12px; }
@media (max-width: 640px){
.lane-sidebar{ width:92px; }
.lane-row .lbl b{ font-size:11.5px; }
}
</style>
</head>
<body>
<header>
<h1>Linha do Tempo Processual</h1>
<p class="lede">
Autos vinculados <span class="mono">Pet16704-pt2-md</span> — um ponto por documento, agrupado pela
instituição de origem e posicionado pela data real de juntada. Substitui o <span class="mono">gitGraph</span>
do Mermaid: com 3.437 commits em sequência fixa (sem eixo temporal real), o SVG gerado saía em
16.368 × 40&nbsp;px — ilegível. Os textos aqui vêm íntegros do <span class="mono">TIMELINE.md</span>, sem o
truncamento de ~100 caracteres imposto pelas tags do Mermaid.
</p>
<div class="stats" id="stats"></div>
</header>
<div class="controls">
<div class="search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input id="search" type="text" placeholder="Buscar por texto ou nº do documento…" autocomplete="off">
</div>
<div class="dropdown" id="typeDropdown">
<button type="button" id="typeBtn" class="dropdown-btn" aria-haspopup="true" aria-expanded="false">
Tipo de documento <span class="arrow"></span>
</button>
<div class="dropdown-panel" id="typePanel" hidden>
<div class="dropdown-search">
<input id="typeSearch" type="text" placeholder="Filtrar tipos…" autocomplete="off">
</div>
<div class="dropdown-actions">
<button type="button" id="typeAll">Selecionar todos</button>
<button type="button" id="typeNone">Limpar</button>
</div>
<div class="dropdown-list" id="typeList"></div>
</div>
</div>
<div class="dropdown" id="peopleDropdown">
<button type="button" id="peopleBtn" class="dropdown-btn" aria-haspopup="true" aria-expanded="false">
Pessoas mencionadas <span class="arrow"></span>
</button>
<div class="dropdown-panel dropdown-panel-wide" id="peoplePanel" hidden>
<div class="dropdown-hint">1. Papel (filtra a lista abaixo) · 2. Marque as pessoas</div>
<div class="role-chips" id="roleChips"></div>
<div class="dropdown-search">
<input id="peopleSearch" type="text" placeholder="Filtrar por nome…" autocomplete="off">
</div>
<div class="dropdown-actions">
<button type="button" id="peopleAllVisible">Marcar visíveis</button>
<button type="button" id="peopleNone">Limpar tudo</button>
</div>
<div class="dropdown-list" id="peopleList"></div>
</div>
</div>
<div class="legend" id="legend"></div>
<div class="spacer"></div>
<div class="presets" id="presets"></div>
<div class="segmented" role="group" aria-label="Modo de visualização">
<button id="viewChartBtn" aria-pressed="true">Linha do tempo</button>
<button id="viewTableBtn" aria-pressed="false">Tabela</button>
</div>
</div>
<section id="chartView">
<div class="overview-card">
<div class="overview-title">Visão geral · 20182026 · arraste para navegar, arraste as bordas para ajustar o período</div>
<div class="overview-wrap">
<canvas id="overviewCanvas" height="72"></canvas>
<div class="brush" id="brush"></div>
</div>
</div>
<div class="detail-card">
<div class="detail-row">
<div class="lane-sidebar" id="laneSidebar"></div>
<div class="detail-canvas-wrap">
<canvas id="detailCanvas"></canvas>
<div class="hover-ring" id="hoverRing"></div>
<div id="tooltip"></div>
</div>
</div>
</div>
<p class="count-line" id="chartCount"></p>
</section>
<section id="tableView" hidden>
<div class="table-scroll">
<table>
<thead>
<tr>
<th data-sort="date">Data <span class="arrow"></span></th>
<th data-sort="lane">Instituição <span class="arrow"></span></th>
<th data-sort="type">Tipo <span class="arrow"></span></th>
<th>ID</th>
<th>Descrição</th>
<th>Pessoas</th>
<th>Arquivo-fonte</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<div class="table-foot">
<span class="mono" id="tableCount"></span>
<button id="loadMore">Carregar mais</button>
</div>
</section>
<footer>
Fonte dos dados: <span class="mono">TIMELINE.md</span> e <span class="mono">gitgraph.md</span> (para a
atribuição de instituição por documento) no diretório <span class="mono">Pet16704-pt2-md</span>. Documentos sem
data de juntada identificada (615 de 3.437) ficam de fora da linha do tempo e aparecem apenas na tabela.
As 292 pessoas do filtro "Pessoas mencionadas" foram extraídas automaticamente com um modelo de NER treinado em
texto jurídico brasileiro (LeNER-Br); nomes e papéis processuais (Relator, Outorgante/Parte, Advogado(a) etc.)
são inferidos e podem conter imprecisões — a maioria (185) fica sem papel classificado ("Mencionado(a)").
</footer>
<script>
(function(){
"use strict";
var LANES = [
{ key:"STF · Relatoria", label:"STF", full:"STF · Relatoria", color:"var(--s1)" },
{ key:"PGR · Procuradoria-Geral da República", label:"PGR", full:"PGR · Procuradoria-Geral da República", color:"var(--s2)" },
{ key:"Polícia Federal · CINQ e diligências", label:"Polícia Federal", full:"Polícia Federal · CINQ e diligências", color:"var(--s3)" },
{ key:"Partes e Defesas", label:"Partes e Defesas", full:"Partes e Defesas", color:"var(--s4)" },
{ key:"Secretaria e Cartório", label:"Secretaria e Cartório", full:"Secretaria e Cartório", color:"var(--s5)" }
];
var ROW_H = 64;
var AXIS_H = 28;
var laneIndex = {};
LANES.forEach(function(l,i){ laneIndex[l.key]=i; });
function cssVar(name){
return getComputedStyle(document.documentElement).getPropertyValue(name.replace("var(","").replace(")","")).trim();
}
LANES.forEach(function(l){ l._hex = null; });
var state = {
points: [], // dated points {id,lane,ms,text,file}
undated: [],
activeLanes: new Set(LANES.map(function(l){return l.key;})),
search: "",
view: "chart",
domainMin: 0, domainMax: 0,
t0: 0, t1: 0,
dragging: null,
hover: null,
sortKey: "date", sortDir: "asc",
pageSize: 200,
onlyUndated: false
};
function hashJitter(id){
var h = 0;
for (var i=0;i<id.length;i++){ h = (h*31 + id.charCodeAt(i)) | 0; }
var frac = ((h % 10007) / 10007);
return (frac*2-1) * (ROW_H/2 - 12);
}
function fmtDate(ms){
var d = new Date(ms);
var dd = String(d.getUTCDate()).padStart(2,"0");
var mm = String(d.getUTCMonth()+1).padStart(2,"0");
return dd+"/"+mm+"/"+d.getUTCFullYear();
}
function monthLabel(ms){
var d = new Date(ms);
var names = ["jan","fev","mar","abr","mai","jun","jul","ago","set","out","nov","dez"];
return names[d.getUTCMonth()]+"/"+String(d.getUTCFullYear()).slice(2);
}
Promise.all([
fetch("docs.json").then(function(r){ return r.json(); }),
fetch("people.json").then(function(r){ return r.json(); })
]).then(function(results){
var rows = results[0];
state.PEOPLE = results[1]; // [{key, count, role}], sorted by count desc
state.activePeople = new Set(state.PEOPLE.map(function(p){return p.key;}));
state.activeRoleFilter = new Set(state.PEOPLE.map(function(p){return p.role;}));
var minMs = Infinity, maxMs = -Infinity;
rows.forEach(function(r){
var ms = r.date ? Date.parse(r.date+"T00:00:00Z") : null;
var rec = { id:r.id, lane:r.branch, ms:ms, text:r.text, file:r.file, type:r.type, people:r.people||[], order:r.order };
if (ms==null){ state.undated.push(rec); }
else {
rec.jitter = hashJitter(r.id);
state.points.push(rec);
if (ms<minMs) minMs=ms;
if (ms>maxMs) maxMs=ms;
}
});
state.domainMin = minMs;
state.domainMax = maxMs;
var pad = (maxMs-minMs)*0.015;
state.fullMin = minMs - pad;
state.fullMax = maxMs + pad;
state.t0 = Math.max(state.fullMin, Date.parse("2026-01-01T00:00:00Z"));
state.t1 = state.fullMax;
state.byLane = {};
LANES.forEach(function(l){ state.byLane[l.key] = state.points.filter(function(p){return p.lane===l.key;}).sort(function(a,b){return a.ms-b.ms;}); });
var typeCounts = {};
state.points.concat(state.undated).forEach(function(r){
typeCounts[r.type] = (typeCounts[r.type]||0)+1;
});
state.TYPES = Object.keys(typeCounts)
.map(function(k){ return { key:k, count:typeCounts[k] }; })
.sort(function(a,b){ return b.count-a.count; });
state.activeTypes = new Set(state.TYPES.map(function(t){return t.key;}));
init();
});
function init(){
LANES.forEach(function(l){ l._hex = cssVar(l.color); });
renderStats();
renderLegend();
renderTypeDropdown();
renderPeopleDropdown();
renderPresets();
buildLaneSidebar();
bindControls();
bindChartInteraction();
resizeAll();
renderTable(true);
window.addEventListener("resize", debounce(resizeAll, 120));
var onThemeChange = function(){
LANES.forEach(function(l){ l._hex = cssVar(l.color); });
drawOverview(); drawDetail();
};
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", onThemeChange);
new MutationObserver(onThemeChange).observe(document.documentElement, { attributes:true, attributeFilter:["data-theme"] });
}
function debounce(fn,ms){ var t; return function(){ clearTimeout(t); var a=arguments; t=setTimeout(function(){fn.apply(null,a);},ms); }; }
function renderStats(){
var total = state.points.length + state.undated.length;
var pct = Math.round(state.undated.length/total*100);
var el = document.getElementById("stats");
el.innerHTML = "";
var items = [
{ k:"Documentos", v: total.toLocaleString("pt-BR") },
{ k:"Período", v: fmtDate(state.domainMin)+" "+fmtDate(state.domainMax) },
{ k:"Instituições", v: LANES.length },
{ k:"Sem data", v: state.undated.length.toLocaleString("pt-BR")+" ("+pct+"%)", onClick:function(){
state.onlyUndated = true;
setView("table");
renderTable(true);
} },
{ k:"Pessoas identificadas", v: state.PEOPLE.length.toLocaleString("pt-BR"), onClick:function(){
document.getElementById("peopleBtn").click();
} }
];
items.forEach(function(it){
var d = document.createElement("div");
d.className = "stat" + (it.onClick?" link":"");
d.innerHTML = '<div class="k">'+it.k+'</div><div class="v tabnum">'+it.v+'</div>';
if (it.onClick){
d.tabIndex = 0;
d.addEventListener("click", it.onClick);
d.addEventListener("keydown", function(e){ if(e.key==="Enter") d.click(); });
}
el.appendChild(d);
});
}
function laneCount(key){
return state.points.filter(function(p){return p.lane===key;}).length + state.undated.filter(function(p){return p.lane===key;}).length;
}
function renderLegend(){
var el = document.getElementById("legend");
el.innerHTML = "";
LANES.forEach(function(l){
var chip = document.createElement("button");
chip.className = "chip";
chip.type = "button";
chip.setAttribute("aria-pressed","true");
chip.innerHTML = '<span class="dot" style="background:'+l.color+'"></span>'+l.label+' <span class="n tabnum">'+laneCount(l.key).toLocaleString("pt-BR")+'</span>';
chip.addEventListener("click", function(){
if (state.activeLanes.has(l.key)){ state.activeLanes.delete(l.key); chip.dataset.off="true"; chip.setAttribute("aria-pressed","false"); }
else { state.activeLanes.add(l.key); delete chip.dataset.off; chip.setAttribute("aria-pressed","true"); }
drawDetail(); drawOverview(); renderTable(true);
});
el.appendChild(chip);
});
}
function updateTypeBtnLabel(){
var btn = document.getElementById("typeBtn");
var total = state.TYPES.length;
var active = state.activeTypes.size;
btn.innerHTML = "";
btn.appendChild(document.createTextNode(active===total ? "Tipo de documento" : "Tipo de documento "));
if (active!==total){
var tag = document.createElement("span");
tag.className = "tag";
tag.textContent = active + "/" + total;
btn.appendChild(tag);
}
var arrow = document.createElement("span");
arrow.className = "arrow"; arrow.textContent = "▾";
btn.appendChild(arrow);
}
function renderTypeDropdown(){
var btn = document.getElementById("typeBtn");
var panel = document.getElementById("typePanel");
var list = document.getElementById("typeList");
updateTypeBtnLabel();
list.innerHTML = "";
state.TYPES.forEach(function(t){
var row = document.createElement("label");
row.className = "type-row";
row.dataset.key = t.key.toLowerCase();
var cb = document.createElement("input");
cb.type = "checkbox"; cb.checked = true;
cb.addEventListener("change", function(){
if (cb.checked) state.activeTypes.add(t.key);
else state.activeTypes.delete(t.key);
updateTypeBtnLabel();
drawDetail(); drawOverview(); renderTable(true);
});
var lbl = document.createElement("span"); lbl.className = "lbl"; lbl.textContent = t.key;
var n = document.createElement("span"); n.className = "n tabnum"; n.textContent = t.count.toLocaleString("pt-BR");
row.appendChild(cb); row.appendChild(lbl); row.appendChild(n);
list.appendChild(row);
});
function setAll(on){
list.querySelectorAll("input[type=checkbox]").forEach(function(cb){ cb.checked = on; });
state.activeTypes = on ? new Set(state.TYPES.map(function(t){return t.key;})) : new Set();
updateTypeBtnLabel();
drawDetail(); drawOverview(); renderTable(true);
}
document.getElementById("typeAll").addEventListener("click", function(){ setAll(true); });
document.getElementById("typeNone").addEventListener("click", function(){ setAll(false); });
document.getElementById("typeSearch").addEventListener("input", function(e){
var q = e.target.value.trim().toLowerCase();
list.querySelectorAll(".type-row").forEach(function(row){
row.classList.toggle("hidden-by-search", q && row.dataset.key.indexOf(q)<0);
});
});
btn.addEventListener("click", function(){
var open = panel.hidden;
panel.hidden = !open;
btn.setAttribute("aria-expanded", String(open));
});
document.addEventListener("click", function(e){
if (!panel.hidden && !document.getElementById("typeDropdown").contains(e.target)){
panel.hidden = true;
btn.setAttribute("aria-expanded","false");
}
});
document.addEventListener("keydown", function(e){
if (e.key==="Escape" && !panel.hidden){ panel.hidden=true; btn.setAttribute("aria-expanded","false"); }
});
}
function updatePeopleBtnLabel(){
var btn = document.getElementById("peopleBtn");
var total = state.PEOPLE.length;
var active = state.activePeople.size;
btn.innerHTML = "";
btn.appendChild(document.createTextNode("Pessoas mencionadas "));
if (active!==total){
var tag = document.createElement("span");
tag.className = "tag";
tag.textContent = active + "/" + total;
btn.appendChild(tag);
}
var arrow = document.createElement("span");
arrow.className = "arrow"; arrow.textContent = "▾";
btn.appendChild(arrow);
}
function renderPeopleDropdown(){
var btn = document.getElementById("peopleBtn");
var panel = document.getElementById("peoplePanel");
var list = document.getElementById("peopleList");
var roleChips = document.getElementById("roleChips");
updatePeopleBtnLabel();
var roleCounts = {};
state.PEOPLE.forEach(function(p){ roleCounts[p.role] = (roleCounts[p.role]||0)+1; });
var roles = Object.keys(roleCounts).sort(function(a,b){ return roleCounts[b]-roleCounts[a]; });
roleChips.innerHTML = "";
roles.forEach(function(role){
var chip = document.createElement("button");
chip.type = "button"; chip.className = "role-chip";
chip.setAttribute("aria-pressed","true");
chip.textContent = role + " (" + roleCounts[role] + ")";
chip.addEventListener("click", function(){
var on = chip.getAttribute("aria-pressed")==="true";
if (on){ state.activeRoleFilter.delete(role); chip.setAttribute("aria-pressed","false"); }
else { state.activeRoleFilter.add(role); chip.setAttribute("aria-pressed","true"); }
applyRoleNarrowing();
});
roleChips.appendChild(chip);
});
list.innerHTML = "";
state.PEOPLE.forEach(function(p){
var row = document.createElement("label");
row.className = "type-row";
row.dataset.key = p.key.toLowerCase();
row.dataset.origKey = p.key;
row.dataset.role = p.role;
var cb = document.createElement("input");
cb.type = "checkbox"; cb.checked = true;
cb.addEventListener("change", function(){
if (cb.checked) state.activePeople.add(p.key);
else state.activePeople.delete(p.key);
updatePeopleBtnLabel();
drawDetail(); drawOverview(); renderTable(true);
});
var lbl = document.createElement("span"); lbl.className = "lbl"; lbl.textContent = p.key;
var badge = document.createElement("span"); badge.className = "role-badge"; badge.textContent = p.role;
var n = document.createElement("span"); n.className = "n tabnum"; n.textContent = p.count.toLocaleString("pt-BR");
row.appendChild(cb); row.appendChild(lbl); row.appendChild(badge); row.appendChild(n);
list.appendChild(row);
});
function applyRoleNarrowing(){
list.querySelectorAll(".type-row").forEach(function(row){
row.classList.toggle("hidden-by-role", !state.activeRoleFilter.has(row.dataset.role));
});
}
function visibleRows(){
return Array.prototype.filter.call(list.querySelectorAll(".type-row"), function(row){
return !row.classList.contains("hidden-by-role") && !row.classList.contains("hidden-by-search");
});
}
document.getElementById("peopleAllVisible").addEventListener("click", function(){
visibleRows().forEach(function(row){
row.querySelector("input").checked = true;
state.activePeople.add(row.dataset.origKey);
});
updatePeopleBtnLabel();
drawDetail(); drawOverview(); renderTable(true);
});
document.getElementById("peopleNone").addEventListener("click", function(){
state.activePeople.clear();
list.querySelectorAll("input[type=checkbox]").forEach(function(cb){ cb.checked = false; });
updatePeopleBtnLabel();
drawDetail(); drawOverview(); renderTable(true);
});
document.getElementById("peopleSearch").addEventListener("input", function(e){
var q = e.target.value.trim().toLowerCase();
list.querySelectorAll(".type-row").forEach(function(row){
row.classList.toggle("hidden-by-search", q && row.dataset.key.indexOf(q)<0);
});
});
btn.addEventListener("click", function(){
var open = panel.hidden;
panel.hidden = !open;
btn.setAttribute("aria-expanded", String(open));
});
document.addEventListener("click", function(e){
if (!panel.hidden && !document.getElementById("peopleDropdown").contains(e.target)){
panel.hidden = true;
btn.setAttribute("aria-expanded","false");
}
});
document.addEventListener("keydown", function(e){
if (e.key==="Escape" && !panel.hidden){ panel.hidden=true; btn.setAttribute("aria-expanded","false"); }
});
}
function renderPresets(){
var el = document.getElementById("presets");
var presets = [
{ label:"Tudo", fn:function(){ return [state.fullMin, state.fullMax]; } },
{ label:"Pré-2026", fn:function(){ return [state.fullMin, Date.parse("2026-01-01T00:00:00Z")]; } },
{ label:"2026", fn:function(){ return [Date.parse("2026-01-01T00:00:00Z"), state.fullMax]; } },
{ label:"Últimos 90 dias", fn:function(){ return [state.domainMax-90*86400000, state.fullMax]; } }
];
presets.forEach(function(p){
var b = document.createElement("button");
b.type = "button"; b.textContent = p.label;
if (p.label==="2026") b.setAttribute("aria-pressed","true");
b.addEventListener("click", function(){
var r = p.fn(); state.t0=r[0]; state.t1=r[1];
Array.prototype.forEach.call(el.children, function(c){ c.removeAttribute("aria-pressed"); });
b.setAttribute("aria-pressed","true");
drawDetail(); positionBrush();
});
el.appendChild(b);
});
}
function buildLaneSidebar(){
var el = document.getElementById("laneSidebar");
el.innerHTML = "";
LANES.forEach(function(l){
var row = document.createElement("div");
row.className = "lane-row";
row.style.height = ROW_H+"px";
row.innerHTML = '<span class="dot" style="background:'+l.color+'"></span><span class="lbl"><b>'+l.label+'</b>'+laneCount(l.key)+' docs</span>';
el.appendChild(row);
});
}
function bindControls(){
document.getElementById("search").addEventListener("input", function(e){
state.search = e.target.value.trim().toLowerCase();
drawDetail(); renderTable(true);
});
document.getElementById("viewChartBtn").addEventListener("click", function(){ setView("chart"); });
document.getElementById("viewTableBtn").addEventListener("click", function(){ setView("table"); });
}
function setView(v){
state.view = v;
document.getElementById("chartView").hidden = v!=="chart";
document.getElementById("tableView").hidden = v!=="table";
document.getElementById("viewChartBtn").setAttribute("aria-pressed", v==="chart");
document.getElementById("viewTableBtn").setAttribute("aria-pressed", v==="table");
if (v==="chart"){ resizeAll(); }
}
function matchesSearch(rec){
if (!state.search) return true;
return rec.id.toLowerCase().indexOf(state.search)>=0 || rec.text.toLowerCase().indexOf(state.search)>=0;
}
function peopleOk(rec){
if (state.activePeople.size === state.PEOPLE.length) return true;
var people = rec.people || [];
for (var i=0;i<people.length;i++){ if (state.activePeople.has(people[i])) return true; }
return false;
}
/* ---------------- overview + detail canvases ---------------- */
var ovCanvas = document.getElementById("overviewCanvas");
var ovCtx;
var dtCanvas = document.getElementById("detailCanvas");
var dtCtx;
var ovWidth=0, dtWidth=0;
function resizeAll(){
var ovWrap = document.querySelector(".overview-wrap");
ovWidth = ovWrap.clientWidth;
ovCanvas.width = ovWidth * devicePixelRatio;
ovCanvas.height = 72 * devicePixelRatio;
ovCanvas.style.width = ovWidth+"px";
ovCanvas.style.height = "72px";
ovCtx = ovCanvas.getContext("2d");
ovCtx.setTransform(devicePixelRatio,0,0,devicePixelRatio,0,0);
var dtWrap = document.querySelector(".detail-canvas-wrap");
dtWidth = Math.max(dtWrap.clientWidth, 460);
var dtHeight = LANES.length*ROW_H + AXIS_H;
dtCanvas.width = dtWidth * devicePixelRatio;
dtCanvas.height = dtHeight * devicePixelRatio;
dtCanvas.style.width = dtWidth+"px";
dtCanvas.style.height = dtHeight+"px";
dtCtx = dtCanvas.getContext("2d");
dtCtx.setTransform(devicePixelRatio,0,0,devicePixelRatio,0,0);
if (state.points.length){ drawOverview(); drawDetail(); positionBrush(); }
}
function scaleFull(ms){ return (ms-state.fullMin)/(state.fullMax-state.fullMin) * ovWidth; }
function scaleDetail(ms){ return (ms-state.t0)/(state.t1-state.t0) * dtWidth; }
function invDetail(px){ return state.t0 + (px/dtWidth)*(state.t1-state.t0); }
function monthBuckets(){
var buckets = [];
var d = new Date(Date.UTC(new Date(state.domainMin).getUTCFullYear(), new Date(state.domainMin).getUTCMonth(), 1));
var end = state.domainMax;
while (d.getTime() <= end){
var next = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth()+1, 1));
buckets.push({ start:d.getTime(), end:next.getTime(), counts:new Array(LANES.length).fill(0) });
d = next;
}
return buckets;
}
function getBuckets(){
var buckets = monthBuckets();
var start = new Date(buckets[0].start);
var startIdx = start.getUTCFullYear()*12 + start.getUTCMonth();
state.points.forEach(function(p){
if (!state.activeTypes.has(p.type)) return;
if (!peopleOk(p)) return;
var d = new Date(p.ms);
var idx = (d.getUTCFullYear()*12 + d.getUTCMonth()) - startIdx;
if (idx>=0 && idx<buckets.length) buckets[idx].counts[laneIndex[p.lane]]++;
});
return buckets;
}
function drawOverview(){
var ctx = ovCtx;
ctx.clearRect(0,0,ovWidth,72);
var buckets = getBuckets();
var maxTotal = 1;
buckets.forEach(function(b){
var t = b.counts.reduce(function(a,c,i){ return a + (state.activeLanes.has(LANES[i].key)?c:0); },0);
if (t>maxTotal) maxTotal=t;
});
var H = 60, base = 66;
buckets.forEach(function(b){
var x0 = scaleFull(b.start), x1 = scaleFull(b.end);
var w = Math.max(1, x1-x0-1);
var y = base;
for (var i=0;i<LANES.length;i++){
if (!state.activeLanes.has(LANES[i].key)) continue;
var c = b.counts[i];
if (!c) continue;
var h = (c/maxTotal)*H;
ctx.fillStyle = LANES[i]._hex;
ctx.globalAlpha = 0.85;
ctx.fillRect(x0, y-h, w, h);
y -= h;
}
});
ctx.globalAlpha = 1;
ctx.strokeStyle = cssVar("--baseline");
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0,base+0.5); ctx.lineTo(ovWidth,base+0.5); ctx.stroke();
// year ticks
ctx.fillStyle = cssVar("--ink-3");
ctx.font = "10.5px 'IBM Plex Mono', monospace";
var y0 = new Date(state.domainMin).getUTCFullYear();
var y1 = new Date(state.domainMax).getUTCFullYear();
for (var yr=y0; yr<=y1; yr++){
var x = scaleFull(Date.UTC(yr,0,1));
if (x<0||x>ovWidth) continue;
ctx.fillText(String(yr), x+3, 10);
ctx.strokeStyle = cssVar("--grid");
ctx.beginPath(); ctx.moveTo(x+0.5,0); ctx.lineTo(x+0.5,base); ctx.stroke();
}
}
function niceTicks(span){
var day=86400000;
var steps = [
[day,"d"], [7*day,"w"], [30*day,"m"], [91*day,"q"], [365*day,"y"], [365*2*day,"y2"], [365*4*day,"y4"]
];
for (var i=0;i<steps.length;i++){
if (span / steps[i][0] <= 10) return steps[i];
}
return steps[steps.length-1];
}
function drawDetail(){
var ctx = dtCtx;
var H = LANES.length*ROW_H;
ctx.clearRect(0,0,dtWidth,H+AXIS_H);
ctx.fillStyle = cssVar("--surface");
ctx.fillRect(0,0,dtWidth,H+AXIS_H);
var searchActive = !!state.search;
// lane separators
ctx.strokeStyle = cssVar("--grid");
ctx.lineWidth = 1;
for (var i=0;i<=LANES.length;i++){
var y = i*ROW_H+0.5;
ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(dtWidth,y); ctx.stroke();
}
// points
LANES.forEach(function(l,li){
if (!state.activeLanes.has(l.key)) return;
var arr = state.byLane[l.key];
var cy = li*ROW_H + ROW_H/2;
var hex = l._hex;
for (var i=0;i<arr.length;i++){
var p = arr[i];
if (!state.activeTypes.has(p.type)) continue;
if (!peopleOk(p)) continue;
var x = scaleDetail(p.ms);
if (x < -10 || x > dtWidth+10) continue;
var match = matchesSearch(p);
var y = cy + p.jitter;
var isMatch = !searchActive || match;
ctx.globalAlpha = isMatch ? 0.72 : 0.10;
ctx.fillStyle = hex;
ctx.beginPath();
ctx.arc(x, y, isMatch && searchActive ? 4 : 3, 0, Math.PI*2);
ctx.fill();
if (searchActive && match){
ctx.globalAlpha = 1;
ctx.strokeStyle = cssVar("--ring");
ctx.lineWidth = 1.3;
ctx.stroke();
}
}
});
ctx.globalAlpha = 1;
// axis
ctx.fillStyle = cssVar("--surface");
ctx.fillRect(0,H,dtWidth,AXIS_H);
ctx.strokeStyle = cssVar("--baseline");
ctx.beginPath(); ctx.moveTo(0,H+0.5); ctx.lineTo(dtWidth,H+0.5); ctx.stroke();
var span = state.t1-state.t0;
var step = niceTicks(span)[0];
var d = new Date(state.t0);
var startMs = Math.floor(state.t0/step)*step;
ctx.font = "11px 'IBM Plex Mono', monospace";
ctx.fillStyle = cssVar("--ink-3");
for (var t=startMs; t<=state.t1; t+=step){
var x = scaleDetail(t);
if (x<0||x>dtWidth) continue;
ctx.strokeStyle = cssVar("--grid");
ctx.beginPath(); ctx.moveTo(x+0.5,0); ctx.lineTo(x+0.5,H); ctx.stroke();
var label = step >= 350*86400000 ? String(new Date(t).getUTCFullYear()) : monthLabel(t);
if (step < 20*86400000) label = fmtDate(t).slice(0,5);
ctx.fillText(label, x+3, H+18);
}
}
/* ---------------- interaction ---------------- */
function bindChartInteraction(){
var dragState = null;
dtCanvas.addEventListener("mousedown", function(e){
dragState = { x: e.clientX, t0: state.t0, t1: state.t1 };
dtCanvas.classList.add("dragging");
});
window.addEventListener("mousemove", function(e){
if (dragState){
var dx = e.clientX - dragState.x;
var dt = -(dx/dtWidth)*(dragState.t1-dragState.t0);
var span = dragState.t1-dragState.t0;
var nt0 = dragState.t0+dt, nt1 = dragState.t1+dt;
if (nt0 < state.fullMin){ nt0=state.fullMin; nt1=nt0+span; }
if (nt1 > state.fullMax){ nt1=state.fullMax; nt0=nt1-span; }
state.t0=nt0; state.t1=nt1;
drawDetail(); positionBrush();
} else {
handleHover(e);
}
});
window.addEventListener("mouseup", function(){ dragState=null; dtCanvas.classList.remove("dragging"); });
dtCanvas.addEventListener("wheel", function(e){
e.preventDefault();
var rect = dtCanvas.getBoundingClientRect();
var mx = e.clientX-rect.left;
var anchor = invDetail(mx);
var factor = Math.exp(e.deltaY*0.0012);
var span = (state.t1-state.t0)*factor;
var minSpan = 6*3600000, maxSpan = state.fullMax-state.fullMin;
span = Math.max(minSpan, Math.min(maxSpan, span));
var ratio = (anchor-state.t0)/(state.t1-state.t0);
var nt0 = anchor - ratio*span, nt1 = anchor + (1-ratio)*span;
if (nt0<state.fullMin){ nt1+= state.fullMin-nt0; nt0=state.fullMin; }
if (nt1>state.fullMax){ nt0-= nt1-state.fullMax; nt1=state.fullMax; }
state.t0=Math.max(state.fullMin,nt0); state.t1=Math.min(state.fullMax,nt1);
drawDetail(); positionBrush();
}, { passive:false });
dtCanvas.addEventListener("mouseleave", function(){ clearHover(); });
function handleHover(e){
var rect = dtCanvas.getBoundingClientRect();
var mx = e.clientX-rect.left, my = e.clientY-rect.top;
var li = Math.floor(my/ROW_H);
if (li<0||li>=LANES.length || !state.activeLanes.has(LANES[li].key)){ clearHover(); return; }
var arr = state.byLane[LANES[li].key];
if (!arr.length){ clearHover(); return; }
var targetMs = invDetail(mx);
var lo=0, hi=arr.length-1;
while (lo<hi){ var mid=(lo+hi)>>1; if (arr[mid].ms<targetMs) lo=mid+1; else hi=mid; }
var best=null, bestD=Infinity;
for (var k=Math.max(0,lo-3); k<=Math.min(arr.length-1,lo+3); k++){
var p = arr[k];
var x = scaleDetail(p.ms);
var y = li*ROW_H+ROW_H/2+p.jitter;
var d = Math.hypot(mx-x, my-y);
if (d<bestD){ bestD=d; best=p; }
}
if (best && bestD<=16){ showHover(best, li); }
else { clearHover(); }
}
function showHover(p, li){
state.hover = p;
var x = scaleDetail(p.ms), y = li*ROW_H+ROW_H/2+p.jitter;
var ring = document.getElementById("hoverRing");
ring.style.display="block";
ring.style.left = x+"px"; ring.style.top = y+"px";
ring.style.width = "12px"; ring.style.height="12px";
var lane = LANES[laneIndex[p.lane]];
var tt = document.getElementById("tooltip");
tt.style.display = "block";
tt.innerHTML = "";
var row1 = document.createElement("div"); row1.className = "row1";
var dotEl = document.createElement("span"); dotEl.className = "dot"; dotEl.style.background = lane.color;
var dateEl = document.createElement("span"); dateEl.className = "date"; dateEl.textContent = fmtDate(p.ms);
var laneEl = document.createElement("span"); laneEl.className = "lane"; laneEl.textContent = "· " + lane.full;
row1.appendChild(dotEl); row1.appendChild(dateEl); row1.appendChild(laneEl);
var idEl = document.createElement("div"); idEl.className = "id"; idEl.textContent = p.id;
var textEl = document.createElement("div"); textEl.className = "text"; textEl.textContent = p.text;
tt.appendChild(row1); tt.appendChild(idEl); tt.appendChild(textEl);
var left = x+16, top = y-10;
if (left + 330 > dtWidth) left = x-330-8;
tt.style.left = left+"px";
tt.style.top = Math.max(0,top)+"px";
}
function clearHover(){
state.hover = null;
document.getElementById("hoverRing").style.display="none";
document.getElementById("tooltip").style.display="none";
}
// overview brush
var brush = document.getElementById("brush");
var ovState = null;
ovCanvas.addEventListener("mousedown", function(e){
var rect = ovCanvas.getBoundingClientRect();
var mx = e.clientX-rect.left;
var bx0 = scaleFull(state.t0), bx1 = scaleFull(state.t1);
var mode;
if (Math.abs(mx-bx0)<7) mode="left";
else if (Math.abs(mx-bx1)<7) mode="right";
else if (mx>bx0 && mx<bx1) mode="move";
else { mode="move"; var span=state.t1-state.t0; var center = state.fullMin+(mx/ovWidth)*(state.fullMax-state.fullMin);
state.t0=center-span/2; state.t1=center+span/2;
clampT(); drawDetail(); positionBrush();
}
ovState = { mode:mode, startX:mx, t0:state.t0, t1:state.t1 };
});
window.addEventListener("mousemove", function(e){
if (!ovState) return;
var rect = ovCanvas.getBoundingClientRect();
var mx = e.clientX-rect.left;
var dt = (mx-ovState.startX)/ovWidth*(state.fullMax-state.fullMin);
if (ovState.mode==="move"){ state.t0=ovState.t0+dt; state.t1=ovState.t1+dt; }
else if (ovState.mode==="left"){ state.t0=Math.min(ovState.t1-6*3600000, ovState.t0+dt); }
else if (ovState.mode==="right"){ state.t1=Math.max(ovState.t0+6*3600000, ovState.t1+dt); }
clampT();
drawDetail(); positionBrush();
});
window.addEventListener("mouseup", function(){ ovState=null; });
function clampT(){
if (state.t0<state.fullMin){ var span=state.t1-state.t0; state.t0=state.fullMin; if(ovState&&ovState.mode==="move") state.t1=state.t0+span; }
if (state.t1>state.fullMax){ var span2=state.t1-state.t0; state.t1=state.fullMax; if(ovState&&ovState.mode==="move") state.t0=state.t1-span2; }
}
}
function positionBrush(){
var brush = document.getElementById("brush");
var x0 = scaleFull(state.t0), x1 = scaleFull(state.t1);
brush.style.left = Math.max(0,x0)+"px";
brush.style.width = Math.max(2,x1-x0)+"px";
}
/* ---------------- table view ---------------- */
function allRows(){
var rows = state.points.concat(state.undated);
return rows.filter(function(r){
if (!state.activeLanes.has(r.lane)) return false;
if (!state.activeTypes.has(r.type)) return false;
if (!peopleOk(r)) return false;
if (state.onlyUndated && r.ms!=null) return false;
return matchesSearch(r);
});
}
function renderTable(reset){
if (reset) state.pageSize = 200;
var rows = allRows();
rows.sort(function(a,b){
if (state.sortKey==="date"){
var am=a.ms, bm=b.ms;
if (am==null && bm==null) return a.order-b.order;
if (am==null) return 1;
if (bm==null) return -1;
return state.sortDir==="asc" ? am-bm : bm-am;
} else if (state.sortKey==="type"){
if (a.type<b.type) return state.sortDir==="asc" ? -1:1;
if (a.type>b.type) return state.sortDir==="asc" ? 1:-1;
return (a.ms||0)-(b.ms||0);
} else {
var al = LANES[laneIndex[a.lane]].label, bl = LANES[laneIndex[b.lane]].label;
if (al<bl) return state.sortDir==="asc" ? -1:1;
if (al>bl) return state.sortDir==="asc" ? 1:-1;
return (a.ms||0)-(b.ms||0);
}
});
var slice = rows.slice(0, state.pageSize);
var tbody = document.getElementById("tbody");
tbody.innerHTML = "";
var frag = document.createDocumentFragment();
slice.forEach(function(r){
var lane = LANES[laneIndex[r.lane]];
var tr = document.createElement("tr");
var tdDate = document.createElement("td");
tdDate.className = "date";
tdDate.textContent = r.ms!=null ? fmtDate(r.ms) : "—";
tr.appendChild(tdDate);
var tdLane = document.createElement("td");
tdLane.className = "lane";
var dot = document.createElement("span"); dot.className="dot"; dot.style.background = lane.color;
tdLane.appendChild(dot);
tdLane.appendChild(document.createTextNode(lane.label));
tr.appendChild(tdLane);
var tdType = document.createElement("td"); tdType.className="type"; tdType.textContent = r.type;
tr.appendChild(tdType);
var tdId = document.createElement("td"); tdId.className="idcol"; tdId.textContent = r.id;
tr.appendChild(tdId);
var tdText = document.createElement("td"); tdText.className="text"; tdText.textContent = r.text;
tr.appendChild(tdText);
var tdPeople = document.createElement("td"); tdPeople.className="people";
tdPeople.textContent = (r.people && r.people.length) ? r.people.join(", ") : "—";
tr.appendChild(tdPeople);
var tdFile = document.createElement("td"); tdFile.className="file"; tdFile.textContent = r.file;
tr.appendChild(tdFile);
frag.appendChild(tr);
});
tbody.appendChild(frag);
document.getElementById("tableCount").textContent =
"Mostrando " + Math.min(state.pageSize, rows.length).toLocaleString("pt-BR") + " de " + rows.length.toLocaleString("pt-BR");
document.getElementById("loadMore").style.display = rows.length>state.pageSize ? "inline-block" : "none";
document.querySelectorAll("thead th[data-sort]").forEach(function(th){
var arrow = th.querySelector(".arrow");
if (th.dataset.sort===state.sortKey) arrow.textContent = state.sortDir==="asc" ? "↑" : "↓";
else arrow.textContent = "";
});
var total = state.points.length+state.undated.length;
document.getElementById("chartCount").textContent =
(state.search ? "Buscando “"+state.search+"” — " : "") +
rows.length.toLocaleString("pt-BR") + " de " + total.toLocaleString("pt-BR") + " documentos visíveis nos filtros atuais.";
}
document.getElementById("loadMore").addEventListener("click", function(){
state.pageSize += 200;
renderTable(false);
});
document.querySelectorAll("thead th[data-sort]").forEach(function(th){
th.addEventListener("click", function(){
var key = th.dataset.sort;
if (state.sortKey===key) state.sortDir = state.sortDir==="asc" ? "desc":"asc";
else { state.sortKey=key; state.sortDir="asc"; }
renderTable(false);
});
});
})();
</script>
</body>
</html>