Drag-to-pan the P/L chart price range
Hold the left mouse button on the chart and drag left/right to slide the underlying-price window; the curve is re-sampled as you drag so it always extends to the edges. Fit / switching symbols / clear-all re-centre and reset zoom. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -116,7 +116,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="card-body p-0">
|
||||||
<div id="plChart" style="min-height:380px;" role="img" aria-label="Profit and loss diagram"></div>
|
<div id="plChart" :style="'min-height:380px;user-select:none;cursor:' + (_drag ? 'grabbing' : 'grab')"
|
||||||
|
@mousedown="startPan($event)" @mousemove.window="onPan($event)" @mouseup.window="endPan()" @mouseleave.window="endPan()"
|
||||||
|
role="img" aria-label="Profit and loss diagram (drag left/right to pan the price range)"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -315,7 +317,8 @@
|
|||||||
// state
|
// state
|
||||||
symbol: '', symbols: [], spot: 0, legs: [],
|
symbol: '', symbols: [], spot: 0, legs: [],
|
||||||
dteOffset: 0,
|
dteOffset: 0,
|
||||||
xZoom: 1, lastHalfPct: 0,
|
xZoom: 1, xPan: 0, lastHalfPct: 0,
|
||||||
|
_drag: null,
|
||||||
refreshing: false, showManual: false, toast: '',
|
refreshing: false, showManual: false, toast: '',
|
||||||
manual: { side:'long', type:'call', qty:1, strike:null, expiry:'', entryPrice:null, ivPct:null },
|
manual: { side:'long', type:'call', qty:1, strike:null, expiry:'', entryPrice:null, ivPct:null },
|
||||||
chart: null, _renderTimer: null,
|
chart: null, _renderTimer: null,
|
||||||
@@ -385,7 +388,7 @@
|
|||||||
switchSymbol(sym) {
|
switchSymbol(sym) {
|
||||||
if (!sym || sym === this.symbol) return;
|
if (!sym || sym === this.symbol) return;
|
||||||
StrategyStore.setActive(sym);
|
StrategyStore.setActive(sym);
|
||||||
this.dteOffset = 0;
|
this.dteOffset = 0; this.xPan = 0; this.xZoom = 1;
|
||||||
this.reload();
|
this.reload();
|
||||||
if (this.legs.length > 0) this.reloadMarket(false);
|
if (this.legs.length > 0) this.reloadMarket(false);
|
||||||
},
|
},
|
||||||
@@ -499,7 +502,7 @@
|
|||||||
clearAll() {
|
clearAll() {
|
||||||
if (!confirm('Clear all ' + (this.symbol || '') + ' legs?')) return;
|
if (!confirm('Clear all ' + (this.symbol || '') + ' legs?')) return;
|
||||||
StrategyStore.clear();
|
StrategyStore.clear();
|
||||||
this.dteOffset = 0;
|
this.dteOffset = 0; this.xPan = 0; this.xZoom = 1;
|
||||||
if (this.chart) { this.chart.destroy(); this.chart = null; }
|
if (this.chart) { this.chart.destroy(); this.chart = null; }
|
||||||
this.reload(); // re-renders the chart if another symbol's basket is now active
|
this.reload(); // re-renders the chart if another symbol's basket is now active
|
||||||
},
|
},
|
||||||
@@ -583,7 +586,23 @@
|
|||||||
},
|
},
|
||||||
zoomIn() { this.xZoom = Math.max(0.2, +(this.xZoom / 1.4).toFixed(3)); this.renderChart(); },
|
zoomIn() { this.xZoom = Math.max(0.2, +(this.xZoom / 1.4).toFixed(3)); this.renderChart(); },
|
||||||
zoomOut() { this.xZoom = Math.min(8, +(this.xZoom * 1.4).toFixed(3)); this.renderChart(); },
|
zoomOut() { this.xZoom = Math.min(8, +(this.xZoom * 1.4).toFixed(3)); this.renderChart(); },
|
||||||
zoomFit() { this.xZoom = 1; this.renderChart(); },
|
zoomFit() { this.xZoom = 1; this.xPan = 0; this.renderChart(); },
|
||||||
|
|
||||||
|
// click-and-drag to pan the price window
|
||||||
|
startPan(e) {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
const pw = (this._lastPlotW && this._lastPlotW > 50) ? this._lastPlotW
|
||||||
|
: ((document.getElementById('plChart')?.clientWidth || 600) - 70);
|
||||||
|
this._drag = { x: e.clientX, pan: this.xPan || 0, half: this._lastHalf || ((this.spot || 100) * 0.22), pw };
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
onPan(e) {
|
||||||
|
if (!this._drag) return;
|
||||||
|
const dx = e.clientX - this._drag.x; // drag right -> show lower prices
|
||||||
|
this.xPan = this._drag.pan - (dx / this._drag.pw) * (2 * this._drag.half);
|
||||||
|
this.scheduleRender();
|
||||||
|
},
|
||||||
|
endPan() { if (this._drag) this._drag = null; },
|
||||||
|
|
||||||
renderChart() {
|
renderChart() {
|
||||||
const legs = this.activeLegs, net = this.netCost;
|
const legs = this.activeLegs, net = this.netCost;
|
||||||
@@ -597,8 +616,10 @@
|
|||||||
const span = maxK - minK;
|
const span = maxK - minK;
|
||||||
const baseHalf = Math.max(spot*0.22, span*1.5, (maxK-spot)*1.4, (spot-minK)*1.4, 5);
|
const baseHalf = Math.max(spot*0.22, span*1.5, (maxK-spot)*1.4, (spot-minK)*1.4, 5);
|
||||||
const half = baseHalf * (this.xZoom || 1);
|
const half = baseHalf * (this.xZoom || 1);
|
||||||
const lo = Math.max(0.01, spot - half), hi = spot + half;
|
const center = spot + (this.xPan || 0);
|
||||||
|
const lo = Math.max(0.01, center - half), hi = center + half;
|
||||||
this.lastHalfPct = spot > 0 ? Math.round(half / spot * 100) : 0;
|
this.lastHalfPct = spot > 0 ? Math.round(half / spot * 100) : 0;
|
||||||
|
this._lastHalf = half; this._lastSpot = spot;
|
||||||
const expAt = this.minDTE;
|
const expAt = this.minDTE;
|
||||||
const N = 221;
|
const N = 221;
|
||||||
// x-grid = evenly spaced points + the exact strike kinks (so payoff
|
// x-grid = evenly spaced points + the exact strike kinks (so payoff
|
||||||
@@ -690,6 +711,11 @@
|
|||||||
this.chart = new ApexCharts(document.getElementById('plChart'), opts);
|
this.chart = new ApexCharts(document.getElementById('plChart'), opts);
|
||||||
this.chart.render();
|
this.chart.render();
|
||||||
}
|
}
|
||||||
|
// remember the plot-area width for drag-to-pan math
|
||||||
|
try {
|
||||||
|
const gw = this.chart && this.chart.w && this.chart.w.globals && this.chart.w.globals.gridWidth;
|
||||||
|
this._lastPlotW = (gw && gw > 50) ? gw : ((document.getElementById('plChart')?.clientWidth || 600) - 70);
|
||||||
|
} catch {}
|
||||||
},
|
},
|
||||||
|
|
||||||
fmtMoney,
|
fmtMoney,
|
||||||
|
|||||||
Reference in New Issue
Block a user