animated current price
This commit is contained in:
parent
a09e4f1208
commit
952a3bb7f3
4 changed files with 28 additions and 23 deletions
|
|
@ -412,7 +412,7 @@ class ConfluenceChart {
|
|||
this.bars = bars;
|
||||
const candleData = bars.map(this.toCandle);
|
||||
this.candles.setData(candleData);
|
||||
this.updateCurrentPrice(bars[bars.length - 1]?.c, false);
|
||||
this.updateCurrentPrice(bars[bars.length - 1]?.c);
|
||||
this.volume.setData(bars.map(ConfluenceChart.toVolume));
|
||||
this.futureSpace.setData(this.futureTimes(bars));
|
||||
// Anchored by time, not by logical index. A logical index addresses the
|
||||
|
|
@ -484,32 +484,35 @@ class ConfluenceChart {
|
|||
this.renderComments();
|
||||
}
|
||||
|
||||
setCurrentPriceAnimation(enabled) { this.animateCurrentPrice = Boolean(enabled); }
|
||||
setCurrentPriceAnimation(enabled) {
|
||||
this.animateCurrentPrice = Boolean(enabled);
|
||||
if (!this.currentPricePulse) return;
|
||||
const active = this.animateCurrentPrice && this.lastCurrentPrice != null;
|
||||
this.currentPricePulse.hidden = !active;
|
||||
this.currentPricePulse.classList.toggle('active', active);
|
||||
}
|
||||
|
||||
updateCurrentPrice(price, pulse = true) {
|
||||
updateCurrentPrice(price) {
|
||||
if (price == null) return;
|
||||
const options = {
|
||||
price,
|
||||
color: '#b7771d',
|
||||
lineWidth: 3,
|
||||
lineStyle: LightweightCharts.LineStyle.Solid,
|
||||
lineWidth: 1,
|
||||
lineStyle: LightweightCharts.LineStyle.Dotted,
|
||||
axisLabelVisible: true,
|
||||
title: '',
|
||||
};
|
||||
if (this.currentPriceLine) this.currentPriceLine.applyOptions(options);
|
||||
else this.currentPriceLine = this.candles.createPriceLine(options);
|
||||
|
||||
const changed = this.lastCurrentPrice != null && price !== this.lastCurrentPrice;
|
||||
this.lastCurrentPrice = price;
|
||||
if (!pulse || !changed || !this.animateCurrentPrice || !this.currentPricePulse) return;
|
||||
if (!this.currentPricePulse) return;
|
||||
requestAnimationFrame(() => {
|
||||
const y = this.candles.priceToCoordinate(price);
|
||||
if (y == null) return;
|
||||
this.currentPricePulse.hidden = false;
|
||||
this.currentPricePulse.style.top = `${Math.round(y)}px`;
|
||||
this.currentPricePulse.classList.remove('active');
|
||||
void this.currentPricePulse.offsetWidth;
|
||||
this.currentPricePulse.classList.add('active');
|
||||
this.currentPricePulse.hidden = !this.animateCurrentPrice;
|
||||
this.currentPricePulse.classList.toggle('active', this.animateCurrentPrice);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,12 +55,12 @@
|
|||
</div>
|
||||
<label class="score-hidden"><input type="checkbox" v-model="prefs.hidden_levels_score">Hidden levels still count toward confluence</label>
|
||||
</details>
|
||||
<details class="sidebar-section tools-section" open>
|
||||
<summary>Tools</summary>
|
||||
<details class="tool-config">
|
||||
<details class="sidebar-section config-section">
|
||||
<summary>Config</summary>
|
||||
<label><input type="checkbox" v-model="animateCurrentPrice"> Animate current price</label>
|
||||
</details>
|
||||
<details class="sidebar-section tools-section" open>
|
||||
<summary>Tools</summary>
|
||||
<div class="tool tool-mark" :class="{armed: armedTool === 'symbol'}">
|
||||
<div class="tool-head symbol-head" title="Choose a mark and click the chart, or drag a mark button onto it. Marks pin to that bar and price and can be dragged later with their grip.">
|
||||
<button class="symbol-arm" @click="toggleSymbolPanel" :aria-expanded="symbolPanelOpen" :aria-pressed="armedTool === 'symbol'">
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ button { border:1px solid var(--line); background:transparent; color:var(--muted
|
|||
.timeframes { display:flex; flex-wrap:wrap; justify-content:flex-end; }.timeframes button+button { border-left:0; }
|
||||
.drawing-tools { min-height:38px; padding:5px 12px; display:flex; align-items:center; gap:9px; border-bottom:1px solid var(--line); color:var(--muted); font-size:10px; }.drawing-tools button,.drawing-tools select,.drawing-tools .line-name { padding:4px 8px; font-size:10px; }.drawing-tools select,.drawing-tools .line-name { background:var(--panel); color:var(--fg); border:1px solid var(--line); }.drawing-tools .line-name { width:130px; font:inherit; }.drawing-tools label { display:flex; gap:4px; align-items:center; }.drawing-tools input { accent-color:var(--accent); }
|
||||
#chart { position:relative; height:calc(100vh - 190px); min-height:420px; }.chart-preview,.chart-projections,.chart-handles { position:absolute; inset:0; width:100%; height:100%; overflow:hidden; pointer-events:none; }.chart-preview,.chart-projections { z-index:4; }.chart-projections line { stroke-linecap:round; }.chart-handles { z-index:6; }.chart-preview line[hidden],.chart-anchor[hidden],.chart-line-hit[hidden],.chart-line-focus[hidden] { display:none; }.chart-line-focus { stroke-linecap:round; pointer-events:none; animation:line-focus .75s ease-out forwards; }.chart-line-hit { stroke:transparent; stroke-width:16px; pointer-events:stroke; cursor:move; touch-action:none; }.chart-anchor { stroke:var(--panel); stroke-width:2px; cursor:grab; pointer-events:all; touch-action:none; }.chart-anchor:active { cursor:grabbing; }.chart-tooltip { position:absolute; z-index:5; padding:4px 7px; border:1px solid var(--line); background:var(--panel); color:var(--fg); font-size:10px; white-space:pre-line; pointer-events:none; }.chart-tooltip[hidden] { display:none; }
|
||||
.current-price-pulse { position:absolute; left:0; right:0; z-index:5; height:2px; transform:translateY(-1px); opacity:0; background:repeating-linear-gradient(90deg,#b7771d 0 6px,transparent 6px 12px); pointer-events:none; }.current-price-pulse.active { animation:current-price-ripple .65s ease-out; }@keyframes current-price-ripple { 0% { opacity:.95; background-position-x:0; } 100% { opacity:0; background-position-x:36px; } }
|
||||
.current-price-pulse { position:absolute; left:0; right:0; z-index:5; height:3px; transform:translateY(-1.5px); overflow:hidden; opacity:0; background:repeating-linear-gradient(110deg,transparent 0 8px,color-mix(in srgb,var(--accent) 55%,white) 8px 13px,transparent 13px 19px); mask-image:linear-gradient(90deg,transparent 0,#000 8%,#000 100%); pointer-events:none; }.current-price-pulse::after { content:''; position:absolute; top:-2px; bottom:-2px; left:-24%; width:24%; background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--accent) 35%,white),transparent); opacity:.8; }.current-price-pulse.active { opacity:.9; animation:current-price-flow 1.5s linear infinite; }.current-price-pulse.active::after { animation:current-price-scan 2.8s linear infinite; }@keyframes current-price-flow { to { background-position-x:57px; } }@keyframes current-price-scan { to { transform:translateX(520%); } }
|
||||
@media (prefers-reduced-motion:reduce) { .current-price-pulse.active { animation:none; } }
|
||||
@keyframes line-focus { 0% { opacity:.85; stroke-width:var(--line-focus-start-width,12px); } 80% { opacity:.85; } 100% { opacity:0; stroke-width:var(--line-focus-width,2px); } }
|
||||
.chart-context-menu { position:absolute; z-index:8; width:165px; padding:4px; border:1px solid var(--line); background:var(--panel); box-shadow:0 5px 18px color-mix(in srgb,var(--fg) 15%,transparent); pointer-events:auto; }.chart-context-menu[hidden] { display:none; }.chart-context-menu button { width:100%; padding:6px 8px; text-align:left; color:var(--fg); font-size:10px; }
|
||||
.statusbar { min-height:34px; display:flex; align-items:center; gap:24px; padding:6px 13px; border-top:1px solid var(--line); color:var(--muted); font-size:10px; }.statusbar b { color:var(--fg); text-transform:uppercase; }.data-freshness { margin-left:auto; white-space:nowrap; }.diag-capture { padding:3px 7px; border-color:var(--accent); color:var(--accent); font-size:9px; white-space:nowrap; }.diag-capture:disabled { opacity:.55; cursor:wait; }
|
||||
|
|
@ -51,7 +52,7 @@ aside { padding:16px; }h2 { margin:0 0 12px; color:var(--muted); font-size:11px;
|
|||
.tool-body input[type=color] { padding:2px; height:26px; }
|
||||
.symbol-palette { display:grid; grid-template-columns:repeat(7,1fr); gap:3px; }.symbol-palette button { display:grid; place-items:center; min-width:0; height:30px; padding:0; font-size:14px; }.symbol-palette button.active { color:var(--bg); }
|
||||
.tool-body .check { display:flex; align-items:center; gap:5px; padding-bottom:5px; }
|
||||
.tool-config { margin-top:8px; padding:7px 9px; border:1px solid var(--line); border-radius:7px; }.tool-config>summary { margin:0; color:var(--muted); font-size:10px; text-transform:uppercase; cursor:pointer; }.tool-config label { display:flex; align-items:center; gap:6px; margin-top:8px; color:var(--muted); font-size:10px; }.tool-config input { accent-color:var(--accent); }
|
||||
.config-section label { display:flex; align-items:center; gap:6px; color:var(--muted); font-size:10px; }.config-section input { accent-color:var(--accent); }
|
||||
.tool-body .check input { width:auto; }
|
||||
.price-row { grid-template-columns:1fr auto; }
|
||||
.price-row button { font-size:11px; padding:5px 12px; align-self:end; }
|
||||
|
|
|
|||
|
|
@ -184,21 +184,22 @@ test('the quote, current-price marker, and tool order retain their trading conte
|
|||
await withChart(async page => {
|
||||
const state = await page.evaluate(() => ({
|
||||
quote: document.querySelector('.quote-change').textContent.trim(),
|
||||
tools: [...document.querySelectorAll('.tools-section > .tool-config, .tools-section > .tool')]
|
||||
.map(node => node.matches('.tool-config')
|
||||
? node.querySelector('summary').textContent.trim()
|
||||
: node.querySelector('.tool-head').textContent.trim().replace(/\s+/g, ' ')),
|
||||
sections: [...document.querySelectorAll('aside > .sidebar-section > summary')]
|
||||
.map(node => node.textContent.trim().replace(/\s+/g, ' ')),
|
||||
tools: [...document.querySelectorAll('.tools-section > .tool')]
|
||||
.map(node => node.querySelector('.tool-head').textContent.trim().replace(/\s+/g, ' ')),
|
||||
candleOptions: window.__chart.candles.options(),
|
||||
markerOptions: window.__chart.currentPriceLine?.options(),
|
||||
pulseExists: Boolean(document.querySelector('.current-price-pulse')),
|
||||
configOpen: document.querySelector('.tool-config').open,
|
||||
configOpen: document.querySelector('.config-section').open,
|
||||
}));
|
||||
|
||||
assert.match(state.quote, /^[+-]?\d+\.\d{2} \([+-]?\d+\.\d{2}%\)$/);
|
||||
assert.deepEqual(state.tools, ['Config', 'Mark', 'Price level', 'Trendline', 'Comment']);
|
||||
assert.deepEqual(state.sections.slice(0, 3), ['Layers', 'Config', 'Tools']);
|
||||
assert.deepEqual(state.tools, ['Mark', 'Price level', 'Trendline', 'Comment']);
|
||||
assert.equal(state.candleOptions.lastValueVisible, false);
|
||||
assert.equal(state.candleOptions.priceLineVisible, false);
|
||||
assert.equal(state.markerOptions.lineWidth, 3);
|
||||
assert.equal(state.markerOptions.lineWidth, 1);
|
||||
assert.equal(state.markerOptions.axisLabelVisible, true);
|
||||
assert.equal(state.pulseExists, true);
|
||||
assert.equal(state.configOpen, false);
|
||||
|
|
|
|||
Loading…
Reference in a new issue