Paint session H/L in the candle primitive so they track price zoom.
DOM marks only moved on overlay redraw, so a price-axis drag left them stuck until something else refreshed.
This commit is contained in:
parent
bc9fed764a
commit
72bcd0fa61
2 changed files with 93 additions and 61 deletions
153
static/chart.js
153
static/chart.js
|
|
@ -195,6 +195,87 @@ class FibLevelsPrimitive {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SessionRangePrimitive {
|
||||||
|
constructor() {
|
||||||
|
this._high = null;
|
||||||
|
this._low = null;
|
||||||
|
this._enabled = true;
|
||||||
|
this._series = null;
|
||||||
|
this._requestUpdate = null;
|
||||||
|
this._items = [];
|
||||||
|
this._view = {
|
||||||
|
update: () => this._sync(),
|
||||||
|
renderer: () => this._renderer,
|
||||||
|
};
|
||||||
|
this._renderer = { draw: target => this._draw(target) };
|
||||||
|
}
|
||||||
|
|
||||||
|
attached(param) {
|
||||||
|
this._series = param.series;
|
||||||
|
this._requestUpdate = param.requestUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
detached() {
|
||||||
|
this._series = null;
|
||||||
|
this._requestUpdate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRange(high, low, enabled) {
|
||||||
|
this._high = high;
|
||||||
|
this._low = low;
|
||||||
|
this._enabled = Boolean(enabled);
|
||||||
|
if (this._requestUpdate) this._requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAllViews() {
|
||||||
|
this._view.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
paneViews() {
|
||||||
|
return [this._view];
|
||||||
|
}
|
||||||
|
|
||||||
|
_sync() {
|
||||||
|
this._items = [];
|
||||||
|
if (!this._enabled || !this._series || this._high == null || this._low == null) return;
|
||||||
|
const highY = this._series.priceToCoordinate(this._high);
|
||||||
|
const lowY = this._series.priceToCoordinate(this._low);
|
||||||
|
if (highY != null) this._items.push({ key: 'H', value: this._high, y: highY, color: '#27825c' });
|
||||||
|
if (lowY != null) this._items.push({ key: 'L', value: this._low, y: lowY, color: '#bd4545' });
|
||||||
|
if (this._items.length === 2 && Math.abs(this._items[0].y - this._items[1].y) < 14) {
|
||||||
|
this._items[0].y -= 7;
|
||||||
|
this._items[1].y += 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_draw(target) {
|
||||||
|
if (!this._items.length) return;
|
||||||
|
target.useMediaCoordinateSpace(scope => {
|
||||||
|
const ctx = scope.context;
|
||||||
|
const right = scope.mediaSize.width;
|
||||||
|
const height = scope.mediaSize.height;
|
||||||
|
ctx.save();
|
||||||
|
ctx.font = '9px "IBM Plex Mono", ui-monospace, monospace';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.textAlign = 'right';
|
||||||
|
for (const item of this._items) {
|
||||||
|
if (item.y < 0 || item.y > height) continue;
|
||||||
|
ctx.strokeStyle = item.color;
|
||||||
|
ctx.fillStyle = item.color;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([3, 3]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(right - 22, item.y + 0.5);
|
||||||
|
ctx.lineTo(right, item.y + 0.5);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.fillText(`${item.key} ${Number(item.value).toFixed(2)}`, right - 26, item.y);
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ConfluenceChart {
|
class ConfluenceChart {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.chart = null;
|
this.chart = null;
|
||||||
|
|
@ -274,7 +355,7 @@ class ConfluenceChart {
|
||||||
this.commentNodes = new Map();
|
this.commentNodes = new Map();
|
||||||
this.commentLayer = null;
|
this.commentLayer = null;
|
||||||
this.contextLabelLayer = null;
|
this.contextLabelLayer = null;
|
||||||
this.sessionRangeLayer = null;
|
this.sessionRangePrimitive = null;
|
||||||
this.sessionRangeEnabled = true;
|
this.sessionRangeEnabled = true;
|
||||||
this.sessionT = null;
|
this.sessionT = null;
|
||||||
this.sessionHigh = null;
|
this.sessionHigh = null;
|
||||||
|
|
@ -462,6 +543,8 @@ class ConfluenceChart {
|
||||||
this.candles.attachPrimitive(this.rthPrimitive);
|
this.candles.attachPrimitive(this.rthPrimitive);
|
||||||
this.fibPrimitive = new FibLevelsPrimitive(time => this.coordinateAtTime(time));
|
this.fibPrimitive = new FibLevelsPrimitive(time => this.coordinateAtTime(time));
|
||||||
this.candles.attachPrimitive(this.fibPrimitive);
|
this.candles.attachPrimitive(this.fibPrimitive);
|
||||||
|
this.sessionRangePrimitive = new SessionRangePrimitive();
|
||||||
|
this.candles.attachPrimitive(this.sessionRangePrimitive);
|
||||||
this.futureSpace = this.chart.addSeries(LightweightCharts.LineSeries, {
|
this.futureSpace = this.chart.addSeries(LightweightCharts.LineSeries, {
|
||||||
priceScaleId: 'right',
|
priceScaleId: 'right',
|
||||||
color: 'rgba(0,0,0,0)',
|
color: 'rgba(0,0,0,0)',
|
||||||
|
|
@ -490,9 +573,6 @@ class ConfluenceChart {
|
||||||
this.contextLabelLayer = document.createElement('div');
|
this.contextLabelLayer = document.createElement('div');
|
||||||
this.contextLabelLayer.className = 'chart-context-labels';
|
this.contextLabelLayer.className = 'chart-context-labels';
|
||||||
this.overlayLayer.appendChild(this.contextLabelLayer);
|
this.overlayLayer.appendChild(this.contextLabelLayer);
|
||||||
this.sessionRangeLayer = document.createElement('div');
|
|
||||||
this.sessionRangeLayer.className = 'session-range-marks';
|
|
||||||
this.overlayLayer.appendChild(this.sessionRangeLayer);
|
|
||||||
this.tooltip = document.createElement('div');
|
this.tooltip = document.createElement('div');
|
||||||
this.tooltip.className = 'chart-tooltip';
|
this.tooltip.className = 'chart-tooltip';
|
||||||
this.overlayLayer.appendChild(this.tooltip);
|
this.overlayLayer.appendChild(this.tooltip);
|
||||||
|
|
@ -864,7 +944,7 @@ class ConfluenceChart {
|
||||||
|
|
||||||
setSessionRangeEnabled(enabled) {
|
setSessionRangeEnabled(enabled) {
|
||||||
this.sessionRangeEnabled = Boolean(enabled);
|
this.sessionRangeEnabled = Boolean(enabled);
|
||||||
this.renderSessionRangeMarks();
|
this.pushSessionRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
setSessionRange(range) {
|
setSessionRange(range) {
|
||||||
|
|
@ -878,7 +958,7 @@ class ConfluenceChart {
|
||||||
this.sessionHigh = Math.max(this.sessionHigh ?? range.high, range.high);
|
this.sessionHigh = Math.max(this.sessionHigh ?? range.high, range.high);
|
||||||
this.sessionLow = Math.min(this.sessionLow ?? range.low, range.low);
|
this.sessionLow = Math.min(this.sessionLow ?? range.low, range.low);
|
||||||
}
|
}
|
||||||
this.syncSessionRangeMarks();
|
this.pushSessionRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
expandSessionRange(bar) {
|
expandSessionRange(bar) {
|
||||||
|
|
@ -893,61 +973,15 @@ class ConfluenceChart {
|
||||||
this.sessionLow = bar.l;
|
this.sessionLow = bar.l;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (changed) this.syncSessionRangeMarks();
|
if (changed) this.pushSessionRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSessionRangeMarks() {
|
pushSessionRange() {
|
||||||
if (!this.sessionRangeLayer) return;
|
this.sessionRangePrimitive?.setRange(
|
||||||
const show = this.sessionRangeEnabled && this.bars[0]?.tf && this.bars[0].tf !== '1d'
|
this.sessionHigh,
|
||||||
&& this.sessionHigh != null && this.sessionLow != null;
|
this.sessionLow,
|
||||||
if (!show) {
|
this.sessionRangeEnabled && this.bars[0]?.tf && this.bars[0].tf !== '1d',
|
||||||
this.sessionRangeLayer.replaceChildren();
|
);
|
||||||
return;
|
|
||||||
}
|
|
||||||
const marks = [
|
|
||||||
{ key: 'high', label: 'H', value: this.sessionHigh },
|
|
||||||
{ key: 'low', label: 'L', value: this.sessionLow },
|
|
||||||
].map(mark => {
|
|
||||||
const node = document.createElement('div');
|
|
||||||
node.className = `session-range-mark ${mark.key}`;
|
|
||||||
node.dataset.key = mark.key;
|
|
||||||
const text = document.createElement('span');
|
|
||||||
text.textContent = `${mark.label} ${Number(mark.value).toFixed(2)}`;
|
|
||||||
node.appendChild(text);
|
|
||||||
return node;
|
|
||||||
});
|
|
||||||
this.sessionRangeLayer.replaceChildren(...marks);
|
|
||||||
this.syncSessionRangeMarks();
|
|
||||||
}
|
|
||||||
|
|
||||||
syncSessionRangeMarks() {
|
|
||||||
if (!this.sessionRangeLayer || !this.candles) return;
|
|
||||||
if (!this.sessionRangeLayer.children.length) {
|
|
||||||
if (this.sessionRangeEnabled && this.sessionHigh != null) this.renderSessionRangeMarks();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const height = this.plotCanvas()?.getBoundingClientRect().height || 0;
|
|
||||||
const place = (key, value) => {
|
|
||||||
const node = this.sessionRangeLayer.querySelector(`[data-key="${key}"]`);
|
|
||||||
if (!node) return null;
|
|
||||||
const y = value == null ? null : this.candles.priceToCoordinate(value);
|
|
||||||
const on = y != null && y >= 0 && y <= height;
|
|
||||||
node.hidden = !on;
|
|
||||||
if (on) node.style.top = `${Math.round(y)}px`;
|
|
||||||
const label = node.querySelector('span');
|
|
||||||
if (label && value != null) {
|
|
||||||
label.textContent = `${key === 'high' ? 'H' : 'L'} ${Number(value).toFixed(2)}`;
|
|
||||||
}
|
|
||||||
return on ? y : null;
|
|
||||||
};
|
|
||||||
const highY = place('high', this.sessionHigh);
|
|
||||||
const lowY = place('low', this.sessionLow);
|
|
||||||
if (highY != null && lowY != null && Math.abs(highY - lowY) < 14) {
|
|
||||||
const high = this.sessionRangeLayer.querySelector('[data-key="high"]');
|
|
||||||
const low = this.sessionRangeLayer.querySelector('[data-key="low"]');
|
|
||||||
if (high) high.style.top = `${Math.round(highY - 7)}px`;
|
|
||||||
if (low) low.style.top = `${Math.round(lowY + 7)}px`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extendManualLineSeries(previous, bar) {
|
extendManualLineSeries(previous, bar) {
|
||||||
|
|
@ -1643,7 +1677,6 @@ class ConfluenceChart {
|
||||||
this.renderProjectionDiagnostic();
|
this.renderProjectionDiagnostic();
|
||||||
this.renderComments();
|
this.renderComments();
|
||||||
this.renderContextLabels();
|
this.renderContextLabels();
|
||||||
this.renderSessionRangeMarks();
|
|
||||||
this.syncCurrentPricePulse();
|
this.syncCurrentPricePulse();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,5 @@ aside { padding:16px; }h2 { margin:0 0 12px; color:var(--muted); font-size:11px;
|
||||||
.chart-snap-leader { position:absolute; inset:0; pointer-events:none; z-index:5; overflow:visible; }.chart-snap-leader line { stroke:var(--muted); stroke-width:1; stroke-dasharray:3 3; opacity:.75; }
|
.chart-snap-leader { position:absolute; inset:0; pointer-events:none; z-index:5; overflow:visible; }.chart-snap-leader line { stroke:var(--muted); stroke-width:1; stroke-dasharray:3 3; opacity:.75; }
|
||||||
.chart-overlays { position:absolute; left:0; top:0; pointer-events:none; overflow:visible; z-index:3; }.chart-overlays > * { pointer-events:none; }.chart-overlays .chart-context-menu { pointer-events:auto; }.chart-overlays .chart-comments, .chart-overlays .chart-snap-leader { position:absolute; inset:0; }
|
.chart-overlays { position:absolute; left:0; top:0; pointer-events:none; overflow:visible; z-index:3; }.chart-overlays > * { pointer-events:none; }.chart-overlays .chart-context-menu { pointer-events:auto; }.chart-overlays .chart-comments, .chart-overlays .chart-snap-leader { position:absolute; inset:0; }
|
||||||
.chart-context-labels { position:absolute; inset:0; z-index:4; overflow:visible; pointer-events:none; }.chart-context-label { position:absolute; left:4px; transform:translateY(-50%); max-width:170px; padding:2px 5px; border-left:3px solid var(--muted); border-radius:2px; background:color-mix(in srgb,var(--chart-bg) 92%,transparent); color:var(--fg); font-size:9px; line-height:1.25; white-space:nowrap; box-shadow:0 1px 2px rgba(0,0,0,.12); }
|
.chart-context-labels { position:absolute; inset:0; z-index:4; overflow:visible; pointer-events:none; }.chart-context-label { position:absolute; left:4px; transform:translateY(-50%); max-width:170px; padding:2px 5px; border-left:3px solid var(--muted); border-radius:2px; background:color-mix(in srgb,var(--chart-bg) 92%,transparent); color:var(--fg); font-size:9px; line-height:1.25; white-space:nowrap; box-shadow:0 1px 2px rgba(0,0,0,.12); }
|
||||||
.session-range-marks { position:absolute; inset:0; z-index:5; overflow:visible; pointer-events:none; }.session-range-mark { position:absolute; right:0; transform:translateY(-50%); display:flex; align-items:center; gap:5px; }.session-range-mark span { padding:1px 5px; border-radius:2px; background:color-mix(in srgb,var(--chart-bg) 92%,transparent); font-size:9px; line-height:1.25; white-space:nowrap; }.session-range-mark::after { content:''; width:22px; border-top:1px dashed currentColor; }.session-range-mark.high { color:var(--green); }.session-range-mark.low { color:var(--red); }.session-range-mark[hidden] { display:none; }
|
|
||||||
.event-number { margin-right:7px; padding:1px 5px; border-radius:3px; background:color-mix(in srgb,var(--accent) 22%,transparent); color:var(--fg); font-weight:600; }
|
.event-number { margin-right:7px; padding:1px 5px; border-radius:3px; background:color-mix(in srgb,var(--accent) 22%,transparent); color:var(--fg); font-weight:600; }
|
||||||
.cluster-row { display:grid; grid-template-columns:auto auto auto minmax(0,1fr); gap:8px; align-items:baseline; padding:3px 7px; border-left:3px solid var(--green); font-size:10px; white-space:nowrap; }.cluster-row.resistance { border-left-color:var(--red); }.cluster-row + .cluster-row { border-top:1px solid var(--line); }.cluster-band { font-size:11px; color:var(--fg); }.cluster-score { font-weight:600; color:var(--accent); }.cluster-dist { color:var(--muted); }.cluster-members { color:var(--muted); overflow:hidden; text-overflow:ellipsis; }
|
.cluster-row { display:grid; grid-template-columns:auto auto auto minmax(0,1fr); gap:8px; align-items:baseline; padding:3px 7px; border-left:3px solid var(--green); font-size:10px; white-space:nowrap; }.cluster-row.resistance { border-left-color:var(--red); }.cluster-row + .cluster-row { border-top:1px solid var(--line); }.cluster-band { font-size:11px; color:var(--fg); }.cluster-score { font-weight:600; color:var(--accent); }.cluster-dist { color:var(--muted); }.cluster-members { color:var(--muted); overflow:hidden; text-overflow:ellipsis; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue