diff --git a/README.md b/README.md index ef5cad6..f10d6cc 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Este projeto troca aquele gráfico por uma timeline de verdade: index.html página estática (self-contained: HTML + CSS + JS, sem build step) docs.json um registro por documento (id, instituição, data, texto completo, tipo, pessoas) people.json catálogo de pessoas identificadas: {key, count, role} -pipeline/ scripts e dados intermediários da extração (ver abaixo) +pipeline/ scripts e dados intermediários da extração (referência, não é preciso rodar nada) ``` ## Como os dados foram montados @@ -67,18 +67,6 @@ pipeline/ scripts e dados intermediários da extração (ver abaixo) classificações de papel aproximadas. Trate como um ponto de partida para busca, não como fonte definitiva de identidade ou qualificação processual. -## Rodando localmente - -Não há build step — é HTML/CSS/JS puro. Basta servir a pasta com qualquer servidor estático: - -```bash -python3 -m http.server 8000 -# abrir http://localhost:8000/ -``` - -Para regerar `docs.json` e `people.json` a partir dos dados originais (`gitgraph.md`, -`TIMELINE.md`, ambos fora deste repositório), ver os scripts em `pipeline/`. - ## Projeto relacionado [**vorcaros**](https://rafapolo.github.io/vorcaros/) — visualização em grafo da rede societária diff --git a/index.html b/index.html index 776dcca..a67087e 100644 --- a/index.html +++ b/index.html @@ -280,6 +280,20 @@ @media (max-width: 640px){ .lane-sidebar{ width:92px; } .lane-row .lbl b{ font-size:11.5px; } + + .controls{ flex-direction:column; align-items:stretch; } + .search{ flex:none; max-width:none; } + .dropdown{ width:100%; } + .dropdown-btn{ width:100%; justify-content:space-between; } + .dropdown-panel, .dropdown-panel-wide{ position:static; width:100%; margin-top:6px; box-shadow:none; } + .legend{ width:100%; } + .spacer{ display:none; } + .presets{ width:100%; } + .presets button{ flex:1 1 auto; } + .segmented{ width:100%; } + .segmented button{ flex:1; } + + .stat{ min-width:0; flex-basis:calc(50% - 5px); } } @@ -1054,6 +1068,60 @@ dtCanvas.addEventListener("mouseleave", function(){ clearHover(); }); + // touch: one-finger drag pans, two-finger pinch zooms, a tap (no drag) shows the tooltip + var touchState = null; + function touchDist(touches){ + return Math.hypot(touches[0].clientX-touches[1].clientX, touches[0].clientY-touches[1].clientY); + } + function touchMidX(touches){ return (touches[0].clientX+touches[1].clientX)/2; } + + dtCanvas.addEventListener("touchstart", function(e){ + if (e.touches.length === 1){ + var t = e.touches[0]; + touchState = { mode:"pan", x:t.clientX, y:t.clientY, t0:state.t0, t1:state.t1, moved:false, time:Date.now() }; + } else if (e.touches.length === 2){ + var rect = dtCanvas.getBoundingClientRect(); + touchState = { mode:"pinch", dist:touchDist(e.touches), anchorMs: invDetail(touchMidX(e.touches)-rect.left), t0:state.t0, t1:state.t1 }; + } + }, { passive:true }); + + dtCanvas.addEventListener("touchmove", function(e){ + if (!touchState) return; + e.preventDefault(); + if (touchState.mode==="pan" && e.touches.length===1){ + var t = e.touches[0]; + var dx = t.clientX - touchState.x; + if (Math.abs(dx) > 4 || Math.abs(t.clientY-touchState.y) > 4) touchState.moved = true; + var dt = -(dx/dtWidth)*(touchState.t1-touchState.t0); + var span = touchState.t1-touchState.t0; + var nt0 = touchState.t0+dt, nt1 = touchState.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 if (touchState.mode==="pinch" && e.touches.length===2){ + var newDist = touchDist(e.touches); + var factor = touchState.dist / Math.max(newDist,1); + var span = (touchState.t1-touchState.t0)*factor; + var minSpan = 6*3600000, maxSpan = state.fullMax-state.fullMin; + span = Math.max(minSpan, Math.min(maxSpan, span)); + var anchor = touchState.anchorMs; + var ratio = (anchor-touchState.t0)/(touchState.t1-touchState.t0); + var nt0p = anchor - ratio*span, nt1p = anchor + (1-ratio)*span; + if (nt0pstate.fullMax){ nt0p-= nt1p-state.fullMax; nt1p=state.fullMax; } + state.t0=Math.max(state.fullMin,nt0p); state.t1=Math.min(state.fullMax,nt1p); + drawDetail(); positionBrush(); + } + }, { passive:false }); + + dtCanvas.addEventListener("touchend", function(){ + if (touchState && touchState.mode==="pan" && !touchState.moved && (Date.now()-touchState.time)<400){ + handleHover({ clientX: touchState.x, clientY: touchState.y }); + } + touchState = null; + }); + function handleHover(e){ var rect = dtCanvas.getBoundingClientRect(); var mx = e.clientX-rect.left, my = e.clientY-rect.top; @@ -1062,16 +1130,24 @@ var arr = state.byLane[LANES[li].key]; if (!arr.length){ clearHover(); return; } var targetMs = invDetail(mx); + // same-day clusters run up to 50+ documents in a single lane, so the + // candidate window must be bounded by actual time proximity, not a + // fixed number of array slots either side of the binary-search hit. + var msPerPixel = Math.abs((state.t1-state.t0)/dtWidth); + var timeRadius = 18 * msPerPixel; var lo=0, hi=arr.length-1; while (lo>1; if (arr[mid].ms=0 && arr[k].ms>=targetMs-timeRadius; k--){ consider(arr[k]); } if (best && bestD<=16){ showHover(best, li); } else { clearHover(); } } @@ -1111,36 +1187,57 @@ // 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 EDGE_PX = 7; + function startBrushGesture(mx, edgePx){ 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"; + if (Math.abs(mx-bx0)bx0 && mxstate.fullMax){ var span2=state.t1-state.t0; state.t1=state.fullMax; if(ovState&&ovState.mode==="move") state.t0=state.t1-span2; } + var gs = ovState || ovTouchState; + if (state.t0state.fullMax){ var span2=state.t1-state.t0; state.t1=state.fullMax; if(gs&&gs.mode==="move") state.t0=state.t1-span2; } } }