Polish exit timeline and always show S/R levels.
Keep support/resistance visible even near trade lines, tighten the how-this-exits spine so steps connect, unify step styling, and add space above the panel.
This commit is contained in:
@@ -354,8 +354,8 @@ export function CandlestickChart({
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Left-side role labels only; prices live on the right axis.
|
// Exterior left-side role labels only; prices live on the right axis.
|
||||||
// Priority when levels crowd: Now > Stop > Gate > Entry > S/R.
|
// Priority when levels crowd: Now > Stop > Gate > Entry.
|
||||||
// Best practice: hide Entry when it sits on top of Now — at market, Now
|
// Best practice: hide Entry when it sits on top of Now — at market, Now
|
||||||
// *is* the fill; Entry only matters once price has drifted from the scan.
|
// *is* the fill; Entry only matters once price has drifted from the scan.
|
||||||
type LineLabel = {
|
type LineLabel = {
|
||||||
@@ -385,27 +385,20 @@ export function CandlestickChart({
|
|||||||
&& (entryDriftPx >= 12 || entryDriftR >= 0.25),
|
&& (entryDriftPx >= 12 || entryDriftR >= 0.25),
|
||||||
);
|
);
|
||||||
|
|
||||||
const tradeYs: number[] = [nowY];
|
|
||||||
if (tradeSetup) {
|
|
||||||
tradeYs.push(yScale(tradeSetup.stop_loss), yScale(tradeSetup.target));
|
|
||||||
if (showEntry) tradeYs.push(yScale(tradeSetup.entry_price));
|
|
||||||
}
|
|
||||||
|
|
||||||
// In-plot S/R labels (drawn after candles so they stay readable). Not
|
// In-plot S/R labels (drawn after candles so they stay readable). Not
|
||||||
// exterior margin labels — they ride the line itself, left-aligned.
|
// exterior margin labels — they ride the line itself, left-aligned.
|
||||||
type InPlotLabel = { y: number; text: string; color: string; bg: string };
|
type InPlotLabel = { y: number; text: string; color: string; bg: string };
|
||||||
const inPlotLabels: InPlotLabel[] = [];
|
const inPlotLabels: InPlotLabel[] = [];
|
||||||
|
|
||||||
// Nearest support/resistance only (band if it came from a zone).
|
// Nearest support/resistance only (band if it came from a zone).
|
||||||
// Drop S/R entirely when it sits on a trade line — the trade label wins.
|
// Always draw them — labels live in-plot on the line, so proximity to
|
||||||
|
// Now/Stop no longer forces a hide (that was only to free the left gutter).
|
||||||
markers.forEach((m) => {
|
markers.forEach((m) => {
|
||||||
const isSupport = m.role === 'support';
|
const isSupport = m.role === 'support';
|
||||||
// Support stays cyan; resistance is a soft coral (warmer than neutral ink,
|
// Support stays cyan; resistance is a soft coral (warmer than neutral ink,
|
||||||
// cooler/softer than the stop so the two never read as the same thing).
|
// cooler/softer than the stop so the two never read as the same thing).
|
||||||
const color = isSupport ? '#2f9db2' : '#e89b8c';
|
const color = isSupport ? '#2f9db2' : '#e89b8c';
|
||||||
const yMid = yScale(m.price);
|
const yMid = yScale(m.price);
|
||||||
const collidesTrade = tradeYs.some((ty) => Math.abs(ty - yMid) < LABEL_MIN_GAP_PX);
|
|
||||||
if (collidesTrade) return;
|
|
||||||
|
|
||||||
if (m.high > m.low) {
|
if (m.high > m.low) {
|
||||||
const yTop = yScale(m.high);
|
const yTop = yScale(m.high);
|
||||||
|
|||||||
@@ -16,35 +16,34 @@ export function ExitPlanPanel({ plan, direction }: { plan: ExitPlan; direction:
|
|||||||
const steps = buildSteps(plan, isLong);
|
const steps = buildSteps(plan, isLong);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-5 rounded-xl border border-white/[0.07] bg-white/[0.02] p-4">
|
<div className="mt-8 rounded-xl border border-white/[0.07] bg-white/[0.02] p-4">
|
||||||
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
<div className="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||||
<span className="num text-[10px] uppercase tracking-[0.16em] text-gray-500">how this exits</span>
|
<span className="num text-[10px] uppercase tracking-[0.16em] text-gray-500">how this exits</span>
|
||||||
<span className="text-[11.5px] font-medium text-gray-300">{plan.headline}</span>
|
<span className="text-[11.5px] font-medium text-gray-300">{plan.headline}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ol className="relative mt-4 space-y-0">
|
<ol className="relative mt-4">
|
||||||
{steps.map((step, i) => {
|
{steps.map((step, i) => {
|
||||||
const isLast = i === steps.length - 1;
|
const isLast = i === steps.length - 1;
|
||||||
return (
|
return (
|
||||||
<li key={step.title} className="relative flex gap-3 pb-4 last:pb-0">
|
<li key={step.title} className="relative flex gap-3">
|
||||||
{/* Spine */}
|
{/* Spine column: continuous connector from this node into the next */}
|
||||||
<div className="flex w-5 shrink-0 flex-col items-center">
|
<div className="relative flex w-5 shrink-0 flex-col items-center self-stretch">
|
||||||
<span
|
<span
|
||||||
className={`num mt-0.5 flex h-5 w-5 items-center justify-center rounded-full border text-[10px] font-semibold ${
|
className="num relative z-10 mt-0.5 flex h-5 w-5 items-center justify-center rounded-full border border-blue-400/35 bg-blue-400/10 text-[10px] font-semibold text-blue-200"
|
||||||
step.tone === 'muted'
|
|
||||||
? 'border-white/[0.12] text-gray-500'
|
|
||||||
: 'border-blue-400/35 bg-blue-400/10 text-blue-200'
|
|
||||||
}`}
|
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
{step.icon ?? i + 1}
|
{i + 1}
|
||||||
</span>
|
</span>
|
||||||
{!isLast && (
|
{!isLast && (
|
||||||
<span className="mt-1 w-px flex-1 min-h-[12px] bg-white/[0.08]" aria-hidden />
|
<span
|
||||||
|
className="absolute left-1/2 top-5 bottom-0 w-px -translate-x-1/2 bg-blue-400/25"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1 pt-0.5">
|
<div className={`min-w-0 flex-1 pt-0.5 ${isLast ? 'pb-0' : 'pb-5'}`}>
|
||||||
<p className="text-[12px] font-medium text-gray-200">{step.title}</p>
|
<p className="text-[12px] font-medium text-gray-200">{step.title}</p>
|
||||||
{step.primary && (
|
{step.primary && (
|
||||||
<p className="num mt-0.5 text-[13px] font-semibold tracking-tight text-gray-100">
|
<p className="num mt-0.5 text-[13px] font-semibold tracking-tight text-gray-100">
|
||||||
@@ -73,9 +72,6 @@ interface ExitStep {
|
|||||||
title: string;
|
title: string;
|
||||||
primary?: string;
|
primary?: string;
|
||||||
detail?: string;
|
detail?: string;
|
||||||
/** Optional glyph instead of a step number (e.g. clock for time stop). */
|
|
||||||
icon?: string;
|
|
||||||
tone?: 'default' | 'muted';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSteps(plan: ExitPlan, isLong: boolean): ExitStep[] {
|
function buildSteps(plan: ExitPlan, isLong: boolean): ExitStep[] {
|
||||||
@@ -114,11 +110,12 @@ function buildSteps(plan: ExitPlan, isLong: boolean): ExitStep[] {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Same visual weight as the other steps — time stop is a real exit rule,
|
||||||
|
// not a footnote.
|
||||||
steps.push({
|
steps.push({
|
||||||
title: 'Time stop',
|
title: 'Time stop',
|
||||||
primary: `${plan.maxHoldDays} trading days`,
|
primary: `${plan.maxHoldDays} trading days`,
|
||||||
detail: 'Flat if still open after the max hold.',
|
detail: 'Flat if still open after the max hold.',
|
||||||
tone: 'muted',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return steps;
|
return steps;
|
||||||
|
|||||||
Reference in New Issue
Block a user