feat: toolbar do gráfico com engrenagem, alinhar ao preço e inputs editáveis
- Toolbar flutuante no canto superior esquerdo do canvas - Ícone de mira: centraliza o gráfico no preço atual (reset scroll Y) - Ícone de engrenagem: abre painel de configurações flutuante sobre o gráfico - Painel de config: modo de exibição, passo do preço, modo de fechamento, delta máx - Passo do preço e delta máx agora têm input de texto digitável + botões −/+ - zoomH/zoomV separados: centerTrigger prop para alinhar do App.jsx Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
935ff203d3
commit
1b520f60fd
+122
-2
@@ -36,6 +36,10 @@ export default function App() {
|
|||||||
const [domData, setDomData] = useState(null);
|
const [domData, setDomData] = useState(null);
|
||||||
const [bigTrades, setBigTrades] = useState([]);
|
const [bigTrades, setBigTrades] = useState([]);
|
||||||
|
|
||||||
|
// Chart toolbar
|
||||||
|
const [showChartSettings, setShowChartSettings] = useState(false);
|
||||||
|
const [centerTrigger, setCenterTrigger] = useState(0);
|
||||||
|
|
||||||
// Toasts
|
// Toasts
|
||||||
const [toasts, setToasts] = useState([]);
|
const [toasts, setToasts] = useState([]);
|
||||||
|
|
||||||
@@ -263,6 +267,115 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Chart Toolbar */}
|
||||||
|
<div className="absolute top-2 left-2 z-20 flex items-center gap-1 bg-[#0B0E14]/90 border border-slate-700 rounded-lg px-1.5 py-1">
|
||||||
|
{/* Align to current price */}
|
||||||
|
<button
|
||||||
|
onClick={() => setCenterTrigger(t => t + 1)}
|
||||||
|
title="Alinhar ao preço atual"
|
||||||
|
className="w-7 h-7 flex items-center justify-center text-slate-400 hover:text-[#00E676] hover:bg-slate-800 rounded transition"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
<line x1="12" y1="2" x2="12" y2="6"/>
|
||||||
|
<line x1="12" y1="18" x2="12" y2="22"/>
|
||||||
|
<line x1="2" y1="12" x2="6" y2="12"/>
|
||||||
|
<line x1="18" y1="12" x2="22" y2="12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-4 bg-slate-700"/>
|
||||||
|
|
||||||
|
{/* Settings gear */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowChartSettings(s => !s)}
|
||||||
|
title="Configurações do gráfico"
|
||||||
|
className={`w-7 h-7 flex items-center justify-center rounded transition ${showChartSettings ? 'text-[#00E676] bg-slate-800' : 'text-slate-400 hover:text-white hover:bg-slate-800'}`}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor">
|
||||||
|
<path d="M19.14 12.94c.04-.3.06-.61.06-.94s-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96a6.97 6.97 0 0 0-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87a.48.48 0 0 0 .12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32a.49.49 0 0 0-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Floating Settings Panel */}
|
||||||
|
{showChartSettings && (
|
||||||
|
<div className="absolute top-12 left-2 z-30 bg-[#0F1520] border border-slate-700 rounded-xl shadow-2xl w-64 p-4 space-y-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-xs font-bold text-slate-300 uppercase tracking-wider">Configurações</span>
|
||||||
|
<button onClick={() => setShowChartSettings(false)} className="text-slate-500 hover:text-white text-xs">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* View Mode */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] text-slate-500 font-bold block mb-1">MODO DE EXIBIÇÃO</span>
|
||||||
|
<div className="flex bg-[#151B26] border border-slate-700 rounded-md overflow-hidden">
|
||||||
|
<button onClick={() => setViewMode('bidask')} className={`flex-1 py-1 text-xs font-semibold ${viewMode === 'bidask' ? 'bg-[#00E676] text-slate-900' : 'text-slate-400 hover:bg-slate-800'}`}>Bid x Ask</button>
|
||||||
|
<button onClick={() => setViewMode('delta')} className={`flex-1 py-1 text-xs font-semibold ${viewMode === 'delta' ? 'bg-[#00E676] text-slate-900' : 'text-slate-400 hover:bg-slate-800'}`}>Delta</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Price Step */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] text-slate-500 font-bold block mb-1">PASSO DO PREÇO (ticks)</span>
|
||||||
|
<div className="flex items-center bg-[#151B26] border border-slate-700 rounded-md overflow-hidden">
|
||||||
|
<button onClick={() => setStepMultiplier(s => Math.max(1, s - 25))} className="px-3 py-1.5 text-slate-400 hover:text-white hover:bg-slate-800">−</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={stepMultiplier}
|
||||||
|
onChange={e => setStepMultiplier(Math.max(1, Number(e.target.value) || 1))}
|
||||||
|
className="flex-1 bg-transparent text-center text-sm font-bold text-white outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
/>
|
||||||
|
<button onClick={() => setStepMultiplier(s => s + 25)} className="px-3 py-1.5 text-slate-400 hover:text-white hover:bg-slate-800">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Close Mode */}
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] text-slate-500 font-bold block mb-1">MODO DE FECHAMENTO</span>
|
||||||
|
<select
|
||||||
|
value={closeMode}
|
||||||
|
onChange={e => { setCloseMode(e.target.value); setConfigDirty(true); }}
|
||||||
|
className="w-full bg-[#151B26] border border-slate-700 rounded-md px-2 py-1.5 text-xs text-slate-300 outline-none"
|
||||||
|
>
|
||||||
|
<option value="delta">Delta</option>
|
||||||
|
<option value="volume">Volume</option>
|
||||||
|
<option value="range">Range</option>
|
||||||
|
<option value="time">Tempo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Delta Max */}
|
||||||
|
{closeMode === 'delta' && (
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] text-slate-500 font-bold block mb-1">DELTA MÁX</span>
|
||||||
|
<div className="flex items-center bg-[#151B26] border border-slate-700 rounded-md overflow-hidden">
|
||||||
|
<button onClick={() => { setDeltaMax(d => Math.max(100, d - 100)); setConfigDirty(true); }} className="px-3 py-1.5 text-slate-400 hover:text-white hover:bg-slate-800">−</button>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="100"
|
||||||
|
step="100"
|
||||||
|
value={deltaMax}
|
||||||
|
onChange={e => { setDeltaMax(Math.max(100, Number(e.target.value) || 100)); setConfigDirty(true); }}
|
||||||
|
className="flex-1 bg-transparent text-center text-sm font-bold text-white outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
/>
|
||||||
|
<button onClick={() => { setDeltaMax(d => d + 100); setConfigDirty(true); }} className="px-3 py-1.5 text-slate-400 hover:text-white hover:bg-slate-800">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Apply */}
|
||||||
|
<button
|
||||||
|
onClick={() => { applyConfig(closeMode, deltaMax, volumeMax, timeSeconds, rangePoints); setShowChartSettings(false); }}
|
||||||
|
disabled={!configDirty}
|
||||||
|
className={`w-full py-1.5 rounded-md text-xs font-semibold transition ${configDirty ? 'bg-[#00E676] text-slate-900 hover:bg-[#00c853]' : 'bg-slate-800 text-slate-600 cursor-not-allowed'}`}
|
||||||
|
>
|
||||||
|
Aplicar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<FootprintCanvas
|
<FootprintCanvas
|
||||||
clusters={allClusters}
|
clusters={allClusters}
|
||||||
tickSize={dynamicTickSize}
|
tickSize={dynamicTickSize}
|
||||||
@@ -274,6 +387,7 @@ export default function App() {
|
|||||||
ask={lastAsk}
|
ask={lastAsk}
|
||||||
domData={domData}
|
domData={domData}
|
||||||
bigTrades={bigTrades}
|
bigTrades={bigTrades}
|
||||||
|
centerTrigger={centerTrigger}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -310,8 +424,14 @@ export default function App() {
|
|||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-[10px] text-slate-500 font-bold mb-1">PRICE STEP</span>
|
<span className="text-[10px] text-slate-500 font-bold mb-1">PRICE STEP</span>
|
||||||
<div className="flex items-center bg-[#151B26] border border-slate-700 rounded-md overflow-hidden">
|
<div className="flex items-center bg-[#151B26] border border-slate-700 rounded-md overflow-hidden">
|
||||||
<button onClick={() => setStepMultiplier(s => Math.max(25, s - 25))} className="px-2 py-1 text-slate-400 hover:text-white hover:bg-slate-800">-</button>
|
<button onClick={() => setStepMultiplier(s => Math.max(1, s - 25))} className="px-2 py-1 text-slate-400 hover:text-white hover:bg-slate-800">-</button>
|
||||||
<div className="px-3 py-1 text-sm font-bold text-white min-w-[52px] text-center">{(stepMultiplier * dynamicTickSize).toFixed(2)} pts</div>
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={stepMultiplier}
|
||||||
|
onChange={e => setStepMultiplier(Math.max(1, Number(e.target.value) || 1))}
|
||||||
|
className="px-1 py-1 text-sm font-bold text-white min-w-[52px] text-center bg-transparent outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
/>
|
||||||
<button onClick={() => setStepMultiplier(s => s + 25)} className="px-2 py-1 text-slate-400 hover:text-white hover:bg-slate-800">+</button>
|
<button onClick={() => setStepMultiplier(s => s + 25)} className="px-2 py-1 text-slate-400 hover:text-white hover:bg-slate-800">+</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useRef, useEffect, useState } from 'react';
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function FootprintCanvas({ clusters, tickSize = 1.0, stepMultiplier = 1, viewMode = 'bidask', imbalanceRatio = 300, onStepChange, bid = 0, ask = 0, domData = null, bigTrades = [] }) {
|
export default function FootprintCanvas({ clusters, tickSize = 1.0, stepMultiplier = 1, viewMode = 'bidask', imbalanceRatio = 300, onStepChange, bid = 0, ask = 0, domData = null, bigTrades = [], centerTrigger = 0 }) {
|
||||||
const canvasRef = useRef(null);
|
const canvasRef = useRef(null);
|
||||||
|
|
||||||
// Navigation & Scale State
|
// Navigation & Scale State
|
||||||
@@ -28,6 +28,13 @@ export default function FootprintCanvas({ clusters, tickSize = 1.0, stepMultipli
|
|||||||
const rowHeight = 26 * zoomV; // height of each price cell (vertical only)
|
const rowHeight = 26 * zoomV; // height of each price cell (vertical only)
|
||||||
const axisWidth = 70; // width of the vertical price axis on the right
|
const axisWidth = 70; // width of the vertical price axis on the right
|
||||||
|
|
||||||
|
// Center on current price when triggered from toolbar
|
||||||
|
useEffect(() => {
|
||||||
|
if (centerTrigger > 0) {
|
||||||
|
setScrollOffset(prev => ({ ...prev, y: 0 }));
|
||||||
|
}
|
||||||
|
}, [centerTrigger]);
|
||||||
|
|
||||||
// Auto-scroll to keep latest cluster visible whenever clusters or horizontal zoom changes
|
// Auto-scroll to keep latest cluster visible whenever clusters or horizontal zoom changes
|
||||||
const lastClusterCount = useRef(0);
|
const lastClusterCount = useRef(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user