From 402f9354ac35beb40c8024f48058cd7551069e6e Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Sat, 26 Oct 2024 20:32:03 -0700 Subject: [PATCH 01/89] Update handler.ts Use existing line/histogram logic to implement area charts --- src/general/handler.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/general/handler.ts b/src/general/handler.ts index d3666e4..aadfb44 100644 --- a/src/general/handler.ts +++ b/src/general/handler.ts @@ -3,6 +3,7 @@ import { CrosshairMode, DeepPartial, HistogramStyleOptions, + AreaStyleOptions, IChartApi, ISeriesApi, LineStyleOptions, @@ -196,7 +197,15 @@ export class Handler { series: line, } } - + createAreaSeries(name: string, options: DeepPartial) { + const line = this.chart.addAreaSeries({ ...options }); + this._seriesList.push(line); + this.legend.makeSeriesRow(name, line); + return { + name: name, + series: line, + }; + } createToolBox() { this.toolBox = new ToolBox(this.id, this.chart, this.series, this.commandFunctions); this.div.appendChild(this.toolBox.div); From 042dcfc092a848d2c625df4c331a8ac42bff5430 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Sat, 26 Oct 2024 20:36:38 -0700 Subject: [PATCH 02/89] Update abstract.py Add methods to plot area chart. --- lightweight_charts/abstract.py | 66 ++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 0091d3f..5a8dd63 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -529,6 +529,57 @@ def scale(self, scale_margin_top: float = 0.0, scale_margin_bottom: float = 0.0) }})''') + +class Area(SeriesCommon): + def __init__(self, chart, name, top_color, bottom_color, invert = False, color = '#FFFFFF', style='solid', width=1, price_line=False, price_label=False, price_scale_id=None, crosshair_marker=True): + + super().__init__(chart, name) + self.color = color + self.topColor = top_color + self.bottomColor = bottom_color + + self.run_script(f''' + {self.id} = {self._chart.id}.createAreaSeries( + "{name}", + {{ + topColor: '{top_color}', + bottomColor: '{bottom_color}', + invertFilledArea: {jbool(invert)}, + color: '{color}', + lineStyle: {as_enum(style, LINE_STYLE)}, + lineWidth: {width}, + lastValueVisible: {jbool(price_label)}, + priceLineVisible: {jbool(price_line)}, + crosshairMarkerVisible: {jbool(crosshair_marker)}, + priceScaleId: {f'"{price_scale_id}"' if price_scale_id else 'undefined'} + {"""autoscaleInfoProvider: () => ({ + priceRange: { + minValue: 1_000_000_000, + maxValue: 0, + }, + }), + """ if chart._scale_candles_only else ''} + }} + ) + null''') + def delete(self): + """ + Irreversibly deletes the line, as well as the object that contains the line. + """ + self._chart._lines.remove(self) if self in self._chart._lines else None + self.run_script(f''' + {self.id}legendItem = {self._chart.id}.legend._lines.find((line) => line.series == {self.id}.series) + {self._chart.id}.legend._lines = {self._chart.id}.legend._lines.filter((item) => item != {self.id}legendItem) + + if ({self.id}legendItem) {{ + {self._chart.id}.legend.div.removeChild({self.id}legendItem.row) + }} + + {self._chart.id}.chart.removeSeries({self.id}.series) + delete {self.id}legendItem + delete {self.id} + ''') + class Candlestick(SeriesCommon): def __init__(self, chart: 'AbstractChart'): super().__init__(chart) @@ -701,7 +752,7 @@ def __init__(self, window: Window, width: float = 1.0, height: float = 1.0, self._height = height self.events: Events = Events(self) - from lightweight_charts.polygon import PolygonAPI + from .polygon import PolygonAPI self.polygon: PolygonAPI = PolygonAPI(self) self.run_script( @@ -741,7 +792,18 @@ def create_histogram( return Histogram( self, name, color, price_line, price_label, scale_margin_top, scale_margin_bottom) - + def create_area( + self, name: str = '', top_color: str ='rgba(0, 100, 0, 0.5)', + bottom_color: str ='rgba(138, 3, 3, 0.5)',invert: bool = False, color: str ='#FFFFFF', style: LINE_STYLE = 'solid', width: int = 2, + price_line: bool = True, price_label: bool = True, price_scale_id: Optional[str] = None + ) -> Area: + """ + Creates and returns an Area object. + """ + return Area(self, name, top_color, bottom_color, invert, color, style, + width, price_line, price_label, price_scale_id) + + def lines(self) -> List[Line]: """ Returns all lines for the chart. From 4b574a59976dcd69deff8b7430c3e3d2a3a0dc66 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Sun, 27 Oct 2024 04:32:32 -0700 Subject: [PATCH 03/89] Update abstract.py Fix: Area line coloring Feat: Implement bar series --- lightweight_charts/abstract.py | 79 ++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 5a8dd63..71ba09c 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -531,10 +531,10 @@ def scale(self, scale_margin_top: float = 0.0, scale_margin_bottom: float = 0.0) class Area(SeriesCommon): - def __init__(self, chart, name, top_color, bottom_color, invert = False, color = '#FFFFFF', style='solid', width=1, price_line=False, price_label=False, price_scale_id=None, crosshair_marker=True): + def __init__(self, chart, name, top_color, bottom_color, invert = False, line_color = '#FFFFFF', style='solid', width=1, price_line=True, price_label=False, price_scale_id=None, crosshair_marker=True): super().__init__(chart, name) - self.color = color + self.color = line_color self.topColor = top_color self.bottomColor = bottom_color @@ -545,7 +545,8 @@ def __init__(self, chart, name, top_color, bottom_color, invert = False, color = topColor: '{top_color}', bottomColor: '{bottom_color}', invertFilledArea: {jbool(invert)}, - color: '{color}', + color: '{line_color}', + lineColor: '{line_color}', lineStyle: {as_enum(style, LINE_STYLE)}, lineWidth: {width}, lastValueVisible: {jbool(price_label)}, @@ -579,7 +580,54 @@ def delete(self): delete {self.id}legendItem delete {self.id} ''') - +class Bar(SeriesCommon): + def __init__(self, chart, name, up_color='#26a69a', down_color='#ef5350', open_visible=True, thin_bars=True, price_line=True, price_label=False, price_scale_id=None): + super().__init__(chart, name) + self.up_color = up_color + self.down_color = down_color + + self.run_script(f''' + {self.id} = {chart.id}.createBarSeries( + "{name}", + {{ + color: '{up_color}', + upColor: '{up_color}', + downColor: '{down_color}', + openVisible: {jbool(open_visible)}, + thinBars: {jbool(thin_bars)}, + lastValueVisible: {jbool(price_label)}, + priceLineVisible: {jbool(price_line)}, + priceScaleId: {f'"{price_scale_id}"' if price_scale_id else 'undefined'} + }} + )''') + def set(self, df: Optional[pd.DataFrame] = None): + if df is None or df.empty: + self.run_script(f'{self.id}.series.setData([])') + self.candle_data = pd.DataFrame() + return + df = self._df_datetime_format(df) + self.data = df.copy() + self._last_bar = df.iloc[-1] + self.run_script(f'{self.id}.series.setData({js_data(df)})') + + + def delete(self): + """ + Irreversibly deletes the bar series. + """ + self.run_script(f''' + {self.id}legendItem = {self._chart.id}.legend._lines.find((line) => line.series == {self.id}.series) + {self._chart.id}.legend._lines = {self._chart.id}.legend._lines.filter((item) => item != {self.id}legendItem) + + if ({self.id}legendItem) {{ + {self._chart.id}.legend.div.removeChild({self.id}legendItem.row) + }} + + {self._chart.id}.chart.removeSeries({self.id}.series) + delete {self.id}legendItem + delete {self.id} + ''') + class Candlestick(SeriesCommon): def __init__(self, chart: 'AbstractChart'): super().__init__(chart) @@ -792,18 +840,33 @@ def create_histogram( return Histogram( self, name, color, price_line, price_label, scale_margin_top, scale_margin_bottom) + def create_area( self, name: str = '', top_color: str ='rgba(0, 100, 0, 0.5)', - bottom_color: str ='rgba(138, 3, 3, 0.5)',invert: bool = False, color: str ='#FFFFFF', style: LINE_STYLE = 'solid', width: int = 2, + bottom_color: str ='rgba(138, 3, 3, 0.5)',invert: bool = False, color: str ='rgba(0,0,255,1)', style: LINE_STYLE = 'solid', width: int = 2, price_line: bool = True, price_label: bool = True, price_scale_id: Optional[str] = None ) -> Area: """ Creates and returns an Area object. """ - return Area(self, name, top_color, bottom_color, invert, color, style, - width, price_line, price_label, price_scale_id) - + self._lines.append(Area(self, name, top_color, bottom_color, invert, color, style, + width, price_line, price_label, price_scale_id)) + return self._lines[-1] + def create_bar( + self, name: str = '', up_color: str = '#26a69a', down_color: str = '#ef5350', + open_visible: bool = True, thin_bars: bool = True, + price_line: bool = True, price_label: bool = True, + price_scale_id: Optional[str] = None + ) -> Bar: + """ + Creates and returns a Bar object. + """ + return Bar( + self, name, up_color, down_color, open_visible, thin_bars, + price_line, price_label, price_scale_id) + + def lines(self) -> List[Line]: """ Returns all lines for the chart. From 5e52329ed082b1388c3bd1264e362067b2d9d964 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Sun, 27 Oct 2024 04:33:56 -0700 Subject: [PATCH 04/89] Update handler.ts Feat: Implement bar series --- src/general/handler.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/general/handler.ts b/src/general/handler.ts index aadfb44..adca7ab 100644 --- a/src/general/handler.ts +++ b/src/general/handler.ts @@ -1,9 +1,10 @@ import { + AreaStyleOptions, + BarStyleOptions, ColorType, CrosshairMode, DeepPartial, HistogramStyleOptions, - AreaStyleOptions, IChartApi, ISeriesApi, LineStyleOptions, @@ -206,6 +207,18 @@ export class Handler { series: line, }; } + + createBarSeries(name: string, options: DeepPartial) { + const line = this.chart.addBarSeries({ ...options }); + this._seriesList.push(line); + this.legend.makeSeriesRow(name, line); + return { + name: name, + series: line, + }; + } + + createToolBox() { this.toolBox = new ToolBox(this.id, this.chart, this.series, this.commandFunctions); this.div.appendChild(this.toolBox.div); From 49d49abe36566129cf839691f6b8ec03d5498cfa Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Sun, 27 Oct 2024 04:35:24 -0700 Subject: [PATCH 05/89] Update bundle.js Feat: Implement Area and Bar series types --- lightweight_charts/js/bundle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightweight_charts/js/bundle.js b/lightweight_charts/js/bundle.js index bd59e73..6d6c9fe 100644 --- a/lightweight_charts/js/bundle.js +++ b/lightweight_charts/js/bundle.js @@ -1 +1 @@ -var Lib=function(t,e){"use strict";const i={backgroundColor:"#0c0d0f",hoverBackgroundColor:"#3c434c",clickBackgroundColor:"#50565E",activeBackgroundColor:"rgba(0, 122, 255, 0.7)",mutedBackgroundColor:"rgba(0, 122, 255, 0.3)",borderColor:"#3C434C",color:"#d8d9db",activeColor:"#ececed"};function s(){window.pane={...i},window.containerDiv=document.getElementById("container")||document.createElement("div"),window.setCursor=t=>{t&&(window.cursor=t),document.body.style.cursor=window.cursor},window.cursor="default",window.textBoxFocused=!1}class o{handler;div;seriesContainer;ohlcEnabled=!1;percentEnabled=!1;linesEnabled=!1;colorBasedOnCandle=!1;text;candle;_lines=[];constructor(t){this.legendHandler=this.legendHandler.bind(this),this.handler=t,this.ohlcEnabled=!1,this.percentEnabled=!1,this.linesEnabled=!1,this.colorBasedOnCandle=!1,this.div=document.createElement("div"),this.div.classList.add("legend"),this.div.style.maxWidth=100*t.scale.width-8+"vw",this.div.style.display="none";const e=document.createElement("div");e.style.display="flex",e.style.flexDirection="row",this.seriesContainer=document.createElement("div"),this.seriesContainer.classList.add("series-container"),this.text=document.createElement("span"),this.text.style.lineHeight="1.8",this.candle=document.createElement("div"),e.appendChild(this.seriesContainer),this.div.appendChild(this.text),this.div.appendChild(this.candle),this.div.appendChild(e),t.div.appendChild(this.div),t.chart.subscribeCrosshairMove(this.legendHandler)}toJSON(){const{_lines:t,handler:e,...i}=this;return i}makeSeriesRow(t,e){const i="#FFF";let s=`\n \n \`\n `,o=`\n \n `,n=document.createElement("div");n.style.display="flex",n.style.alignItems="center";let r=document.createElement("div"),a=document.createElement("div");a.classList.add("legend-toggle-switch");let l=document.createElementNS("http://www.w3.org/2000/svg","svg");l.setAttribute("width","22"),l.setAttribute("height","16");let h=document.createElementNS("http://www.w3.org/2000/svg","g");h.innerHTML=s;let d=!0;a.addEventListener("click",(()=>{d?(d=!1,h.innerHTML=o,e.applyOptions({visible:!1})):(d=!0,e.applyOptions({visible:!0}),h.innerHTML=s)})),l.appendChild(h),a.appendChild(l),n.appendChild(r),n.appendChild(a),this.seriesContainer.appendChild(n);const c=e.options().color;this._lines.push({name:t,div:r,row:n,toggle:a,series:e,solid:c.startsWith("rgba")?c.replace(/[^,]+(?=\))/,"1"):c})}legendItemFormat(t,e){return t.toFixed(e).toString().padStart(8," ")}shorthandFormat(t){const e=Math.abs(t);return e>=1e6?(t/1e6).toFixed(1)+"M":e>=1e3?(t/1e3).toFixed(1)+"K":t.toString().padStart(8," ")}legendHandler(t,e=!1){if(!this.ohlcEnabled&&!this.linesEnabled&&!this.percentEnabled)return;const i=this.handler.series.options();if(!t.time)return this.candle.style.color="transparent",void(this.candle.innerHTML=this.candle.innerHTML.replace(i.upColor,"").replace(i.downColor,""));let s,o=null;if(e){const e=this.handler.chart.timeScale();let i=e.timeToCoordinate(t.time);i&&(o=e.coordinateToLogical(i.valueOf())),o&&(s=this.handler.series.dataByIndex(o.valueOf()))}else s=t.seriesData.get(this.handler.series);this.candle.style.color="";let n='';if(s){if(this.ohlcEnabled&&(n+=`O ${this.legendItemFormat(s.open,this.handler.precision)} `,n+=`| H ${this.legendItemFormat(s.high,this.handler.precision)} `,n+=`| L ${this.legendItemFormat(s.low,this.handler.precision)} `,n+=`| C ${this.legendItemFormat(s.close,this.handler.precision)} `),this.percentEnabled){let t=(s.close-s.open)/s.open*100,e=t>0?i.upColor:i.downColor,o=`${t>=0?"+":""}${t.toFixed(2)} %`;this.colorBasedOnCandle?n+=`| ${o}`:n+="| "+o}if(this.handler.volumeSeries){let e;e=o?this.handler.volumeSeries.dataByIndex(o):t.seriesData.get(this.handler.volumeSeries),e&&(n+=this.ohlcEnabled?`
V ${this.shorthandFormat(e.value)}`:"")}}this.candle.innerHTML=n+"
",this._lines.forEach((i=>{if(!this.linesEnabled)return void(i.row.style.display="none");let s,n;if(i.row.style.display="flex",s=e&&o?i.series.dataByIndex(o):t.seriesData.get(i.series),s?.value){if("Histogram"==i.series.seriesType())n=this.shorthandFormat(s.value);else{const t=i.series.options().priceFormat;n=this.legendItemFormat(s.value,t.precision)}i.div.innerHTML=` ${i.name} : ${n}`}}))}}function n(t){if(void 0===t)throw new Error("Value is undefined");return t}class r{_chart=void 0;_series=void 0;requestUpdate(){this._requestUpdate&&this._requestUpdate()}_requestUpdate;attached({chart:t,series:e,requestUpdate:i}){this._chart=t,this._series=e,this._series.subscribeDataChanged(this._fireDataUpdated),this._requestUpdate=i,this.requestUpdate()}detached(){this._chart=void 0,this._series=void 0,this._requestUpdate=void 0}get chart(){return n(this._chart)}get series(){return n(this._series)}_fireDataUpdated(t){this.dataUpdated&&this.dataUpdated(t)}}const a={lineColor:"#1E80F0",lineStyle:e.LineStyle.Solid,width:4};var l;!function(t){t[t.NONE=0]="NONE",t[t.HOVERING=1]="HOVERING",t[t.DRAGGING=2]="DRAGGING",t[t.DRAGGINGP1=3]="DRAGGINGP1",t[t.DRAGGINGP2=4]="DRAGGINGP2",t[t.DRAGGINGP3=5]="DRAGGINGP3",t[t.DRAGGINGP4=6]="DRAGGINGP4"}(l||(l={}));class h extends r{_paneViews=[];_options;_points=[];_state=l.NONE;_startDragPoint=null;_latestHoverPoint=null;static _mouseIsDown=!1;static hoveredObject=null;static lastHoveredObject=null;_listeners=[];constructor(t){super(),this._options={...a,...t}}updateAllViews(){this._paneViews.forEach((t=>t.update()))}paneViews(){return this._paneViews}applyOptions(t){this._options={...this._options,...t},this.requestUpdate()}updatePoints(...t){for(let e=0;ei.name===t&&i.listener===e));this._listeners.splice(this._listeners.indexOf(i),1)}_handleHoverInteraction(t){if(this._latestHoverPoint=t.point,h._mouseIsDown)this._handleDragInteraction(t);else if(this._mouseIsOverDrawing(t)){if(this._state!=l.NONE)return;this._moveToState(l.HOVERING),h.hoveredObject=h.lastHoveredObject=this}else{if(this._state==l.NONE)return;this._moveToState(l.NONE),h.hoveredObject===this&&(h.hoveredObject=null)}}static _eventToPoint(t,e){if(!e||!t.point||!t.logical)return null;const i=e.coordinateToPrice(t.point.y);return null==i?null:{time:t.time||null,logical:t.logical,price:i.valueOf()}}static _getDiff(t,e){return{logical:t.logical-e.logical,price:t.price-e.price}}_addDiffToPoint(t,e,i){t&&(t.logical=t.logical+e,t.price=t.price+i,t.time=this.series.dataByIndex(t.logical)?.time||null)}_handleMouseDownInteraction=()=>{h._mouseIsDown=!0,this._onMouseDown()};_handleMouseUpInteraction=()=>{h._mouseIsDown=!1,this._moveToState(l.HOVERING)};_handleDragInteraction(t){if(this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP1&&this._state!=l.DRAGGINGP2&&this._state!=l.DRAGGINGP3&&this._state!=l.DRAGGINGP4)return;const e=h._eventToPoint(t,this.series);if(!e)return;this._startDragPoint=this._startDragPoint||e;const i=h._getDiff(e,this._startDragPoint);this._onDrag(i),this.requestUpdate(),this._startDragPoint=e}}class d{_options;constructor(t){this._options=t}}class c extends d{_p1;_p2;_hovered;constructor(t,e,i,s){super(i),this._p1=t,this._p2=e,this._hovered=s}_getScaledCoordinates(t){return null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y?null:{x1:Math.round(this._p1.x*t.horizontalPixelRatio),y1:Math.round(this._p1.y*t.verticalPixelRatio),x2:Math.round(this._p2.x*t.horizontalPixelRatio),y2:Math.round(this._p2.y*t.verticalPixelRatio)}}_drawEndCircle(t,e,i){t.context.fillStyle="#000",t.context.beginPath(),t.context.arc(e,i,9,0,2*Math.PI),t.context.stroke(),t.context.fill()}}function p(t,i){const s={[e.LineStyle.Solid]:[],[e.LineStyle.Dotted]:[t.lineWidth,t.lineWidth],[e.LineStyle.Dashed]:[2*t.lineWidth,2*t.lineWidth],[e.LineStyle.LargeDashed]:[6*t.lineWidth,6*t.lineWidth],[e.LineStyle.SparseDotted]:[t.lineWidth,4*t.lineWidth]}[i];t.setLineDash(s)}class u extends d{_point={x:null,y:null};constructor(t,e){super(e),this._point=t}draw(t){t.useBitmapCoordinateSpace((t=>{if(null==this._point.y)return;const e=t.context,i=Math.round(this._point.y*t.verticalPixelRatio),s=this._point.x?this._point.x*t.horizontalPixelRatio:0;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.beginPath(),e.moveTo(s,i),e.lineTo(t.bitmapSize.width,i),e.stroke()}))}}class _{_source;constructor(t){this._source=t}}class m extends _{_p1={x:null,y:null};_p2={x:null,y:null};_source;constructor(t){super(t),this._source=t}update(){if(!this._source.p1||!this._source.p2)return;const t=this._source.series,e=t.priceToCoordinate(this._source.p1.price),i=t.priceToCoordinate(this._source.p2.price),s=this._getX(this._source.p1),o=this._getX(this._source.p2);this._p1={x:s,y:e},this._p2={x:o,y:i}}_getX(t){return this._source.chart.timeScale().logicalToCoordinate(t.logical)}}class v extends _{_source;_point={x:null,y:null};constructor(t){super(t),this._source=t}update(){const t=this._source._point,e=this._source.chart.timeScale(),i=this._source.series;"RayLine"==this._source._type&&(this._point.x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical)),this._point.y=i.priceToCoordinate(t.price)}renderer(){return new u(this._point,this._source._options)}}class g{_source;_y=null;_price=null;constructor(t){this._source=t}update(){if(!this._source.series||!this._source._point)return;this._y=this._source.series.priceToCoordinate(this._source._point.price);const t=this._source.series.options().priceFormat.precision;this._price=this._source._point.price.toFixed(t).toString()}visible(){return!0}tickVisible(){return!0}coordinate(){return this._y??0}text(){return this._source._options.text||this._price||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class w extends h{_type="HorizontalLine";_paneViews;_point;_callbackName;_priceAxisViews;_startDragPoint=null;constructor(t,e,i=null){super(e),this._point=t,this._point.time=null,this._paneViews=[new v(this)],this._priceAxisViews=[new g(this)],this._callbackName=i}get points(){return[this._point]}updatePoints(...t){for(const e of t)e&&(this._point.price=e.price);this.requestUpdate()}updateAllViews(){this._paneViews.forEach((t=>t.update())),this._priceAxisViews.forEach((t=>t.update()))}priceAxisViews(){return this._priceAxisViews}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._addDiffToPoint(this._point,0,t.price),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.series.priceToCoordinate(this._point.price);return!!i&&Math.abs(i-t.point.y){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class y{_chart;_series;_finishDrawingCallback=null;_drawings=[];_activeDrawing=null;_isDrawing=!1;_drawingType=null;constructor(t,e,i=null){this._chart=t,this._series=e,this._finishDrawingCallback=i,this._chart.subscribeClick(this._clickHandler),this._chart.subscribeCrosshairMove(this._moveHandler)}_clickHandler=t=>this._onClick(t);_moveHandler=t=>this._onMouseMove(t);beginDrawing(t){this._drawingType=t,this._isDrawing=!0}stopDrawing(){this._isDrawing=!1,this._activeDrawing=null}get drawings(){return this._drawings}addNewDrawing(t){this._series.attachPrimitive(t),this._drawings.push(t)}delete(t){if(null==t)return;const e=this._drawings.indexOf(t);-1!=e&&(this._drawings.splice(e,1),t.detach())}clearDrawings(){for(const t of this._drawings)t.detach();this._drawings=[]}repositionOnTime(){for(const t of this.drawings){const e=[];for(const i of t.points){if(!i){e.push(i);continue}const t=i.time?this._chart.timeScale().coordinateToLogical(this._chart.timeScale().timeToCoordinate(i.time)||0):i.logical;e.push({time:i.time,logical:t,price:i.price})}t.updatePoints(...e)}}_onClick(t){if(!this._isDrawing)return;const e=h._eventToPoint(t,this._series);if(e)if(null==this._activeDrawing){if(null==this._drawingType)return;this._activeDrawing=new this._drawingType(e,e),this._series.attachPrimitive(this._activeDrawing),this._drawingType==w&&this._onClick(t)}else{if(this._drawings.push(this._activeDrawing),this.stopDrawing(),!this._finishDrawingCallback)return;this._finishDrawingCallback()}}_onMouseMove(t){if(!t)return;for(const e of this._drawings)e._handleHoverInteraction(t);if(!this._isDrawing||!this._activeDrawing)return;const e=h._eventToPoint(t,this._series);e&&this._activeDrawing.updatePoints(null,e)}}class b extends c{constructor(t,e,i,s){super(t,e,i,s)}draw(t){t.useBitmapCoordinateSpace((t=>{if(null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y)return;const e=t.context,i=this._getScaledCoordinates(t);i&&(e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.beginPath(),e.moveTo(i.x1,i.y1),e.lineTo(i.x2,i.y2),e.stroke(),this._hovered&&(this._drawEndCircle(t,i.x1,i.y1),this._drawEndCircle(t,i.x2,i.y2)))}))}}class x extends m{constructor(t){super(t)}renderer(){return new b(this._p1,this._p2,this._source._options,this._source.hovered)}}class C extends h{_paneViews=[];_hovered=!1;constructor(t,e,i){super(),this.points.push(t),this.points.push(e),this._options={...a,...i}}setFirstPoint(t){this.updatePoints(t)}setSecondPoint(t){this.updatePoints(null,t)}get p1(){return this.points[0]}get p2(){return this.points[1]}get hovered(){return this._hovered}}class f extends C{_type="TrendLine";constructor(t,e,i){super(t,e,i),this._paneViews=[new x(this)]}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._hovered=!1,this.requestUpdate(),this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this.requestUpdate(),this._subscribe("mousedown",this._handleMouseDownInteraction),this._unsubscribe("mouseup",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGINGP1:case l.DRAGGINGP2:case l.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP1||this._addDiffToPoint(this.p1,t.logical,t.price),this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP2||this._addDiffToPoint(this.p2,t.logical,t.price)}_onMouseDown(){this._startDragPoint=null;const t=this._latestHoverPoint;if(!t)return;const e=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(e.x&&i.x&&e.y&&i.y))return this._moveToState(l.DRAGGING);Math.abs(t.x-e.x)<10&&Math.abs(t.y-e.y)<10?this._moveToState(l.DRAGGINGP1):Math.abs(t.x-i.x)<10&&Math.abs(t.y-i.y)<10?this._moveToState(l.DRAGGINGP2):this._moveToState(l.DRAGGING)}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this._paneViews[0]._p1.x,s=this._paneViews[0]._p1.y,o=this._paneViews[0]._p2.x,n=this._paneViews[0]._p2.y;if(!(i&&o&&s&&n))return!1;const r=t.point.x,a=t.point.y;if(r<=Math.min(i,o)-e||r>=Math.max(i,o)+e)return!1;return Math.abs((n-s)*r-(o-i)*a+o*s-n*i)/Math.sqrt((n-s)**2+(o-i)**2)<=e}}class D extends c{constructor(t,e,i,s){super(t,e,i,s)}draw(t){t.useBitmapCoordinateSpace((t=>{const e=t.context,i=this._getScaledCoordinates(t);if(!i)return;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.fillStyle=this._options.fillColor;const s=Math.min(i.x1,i.x2),o=Math.min(i.y1,i.y2),n=Math.abs(i.x1-i.x2),r=Math.abs(i.y1-i.y2);e.strokeRect(s,o,n,r),e.fillRect(s,o,n,r),this._hovered&&(this._drawEndCircle(t,s,o),this._drawEndCircle(t,s+n,o),this._drawEndCircle(t,s+n,o+r),this._drawEndCircle(t,s,o+r))}))}}class E extends m{constructor(t){super(t)}renderer(){return new D(this._p1,this._p2,this._source._options,this._source.hovered)}}const k={fillEnabled:!0,fillColor:"rgba(255, 255, 255, 0.2)",...a};class L extends C{_type="Box";constructor(t,e,i){super(t,e,i),this._options={...k,...i},this._paneViews=[new E(this)]}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._hovered=!1,this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this._unsubscribe("mouseup",this._handleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGINGP1:case l.DRAGGINGP2:case l.DRAGGINGP3:case l.DRAGGINGP4:case l.DRAGGING:document.body.style.cursor="grabbing",document.body.addEventListener("mouseup",this._handleMouseUpInteraction),this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP1||this._addDiffToPoint(this.p1,t.logical,t.price),this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP2||this._addDiffToPoint(this.p2,t.logical,t.price),this._state!=l.DRAGGING&&(this._state==l.DRAGGINGP3&&(this._addDiffToPoint(this.p1,t.logical,0),this._addDiffToPoint(this.p2,0,t.price)),this._state==l.DRAGGINGP4&&(this._addDiffToPoint(this.p1,0,t.price),this._addDiffToPoint(this.p2,t.logical,0)))}_onMouseDown(){this._startDragPoint=null;const t=this._latestHoverPoint,e=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(e.x&&i.x&&e.y&&i.y))return this._moveToState(l.DRAGGING);const s=10;Math.abs(t.x-e.x)l-p&&rh-p&&ai.appendChild(this.makeColorBox(t))));let s=document.createElement("div");s.style.backgroundColor=window.pane.borderColor,s.style.height="1px",s.style.width="130px";let o=document.createElement("div");o.style.margin="10px";let n=document.createElement("div");n.style.color="lightgray",n.style.fontSize="12px",n.innerText="Opacity",this._opacityLabel=document.createElement("div"),this._opacityLabel.style.color="lightgray",this._opacityLabel.style.fontSize="12px",this._opacitySlider=document.createElement("input"),this._opacitySlider.type="range",this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%",this._opacitySlider.oninput=()=>{this._opacityLabel.innerText=this._opacitySlider.value+"%",this.opacity=parseInt(this._opacitySlider.value)/100,this.updateColor()},o.appendChild(n),o.appendChild(this._opacitySlider),o.appendChild(this._opacityLabel),this._div.appendChild(i),this._div.appendChild(s),this._div.appendChild(o),window.containerDiv.appendChild(this._div)}_updateOpacitySlider(){this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%"}makeColorBox(t){const e=document.createElement("div");e.style.width="18px",e.style.height="18px",e.style.borderRadius="3px",e.style.margin="3px",e.style.boxSizing="border-box",e.style.backgroundColor=t,e.addEventListener("mouseover",(()=>e.style.border="2px solid lightgray")),e.addEventListener("mouseout",(()=>e.style.border="none"));const i=S.extractRGBA(t);return e.addEventListener("click",(()=>{this.rgba=i,this.updateColor()})),e}static extractRGBA(t){const e=document.createElement("div");e.style.color=t,document.body.appendChild(e);const i=getComputedStyle(e).color;document.body.removeChild(e);const s=i.match(/\d+/g)?.map(Number);if(!s)return[];let o=i.includes("rgba")?parseFloat(i.split(",")[3]):1;return[s[0],s[1],s[2],o]}updateColor(){if(!h.lastHoveredObject||!this.rgba)return;const t=`rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`;h.lastHoveredObject.applyOptions({[this.colorOption]:t}),this.saveDrawings()}openMenu(t){h.lastHoveredObject&&(this.rgba=S.extractRGBA(h.lastHoveredObject._options[this.colorOption]),this.opacity=this.rgba[3],this._updateOpacitySlider(),this._div.style.top=t.top-30+"px",this._div.style.left=t.right+"px",this._div.style.display="flex",setTimeout((()=>document.addEventListener("mousedown",(t=>{this._div.contains(t.target)||this.closeMenu()}))),10))}closeMenu(){document.body.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}class G{static _styles=[{name:"Solid",var:e.LineStyle.Solid},{name:"Dotted",var:e.LineStyle.Dotted},{name:"Dashed",var:e.LineStyle.Dashed},{name:"Large Dashed",var:e.LineStyle.LargeDashed},{name:"Sparse Dotted",var:e.LineStyle.SparseDotted}];_div;_saveDrawings;constructor(t){this._saveDrawings=t,this._div=document.createElement("div"),this._div.classList.add("context-menu"),G._styles.forEach((t=>{this._div.appendChild(this._makeTextBox(t.name,t.var))})),window.containerDiv.appendChild(this._div)}_makeTextBox(t,e){const i=document.createElement("span");return i.classList.add("context-menu-item"),i.innerText=t,i.addEventListener("click",(()=>{h.lastHoveredObject?.applyOptions({lineStyle:e}),this._saveDrawings()})),i}openMenu(t){this._div.style.top=t.top-30+"px",this._div.style.left=t.right+"px",this._div.style.display="block",setTimeout((()=>document.addEventListener("mousedown",(t=>{this._div.contains(t.target)||this.closeMenu()}))),10)}closeMenu(){document.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}function T(t){const e=[];for(const i of t)0==e.length?e.push(i.toUpperCase()):i==i.toUpperCase()?e.push(" "+i):e.push(i);return e.join("")}class I{saveDrawings;drawingTool;div;hoverItem;items=[];constructor(t,e){this.saveDrawings=t,this.drawingTool=e,this._onRightClick=this._onRightClick.bind(this),this.div=document.createElement("div"),this.div.classList.add("context-menu"),document.body.appendChild(this.div),this.hoverItem=null,document.body.addEventListener("contextmenu",this._onRightClick)}_handleClick=t=>this._onClick(t);_onClick(t){t.target&&(this.div.contains(t.target)||(this.div.style.display="none",document.body.removeEventListener("click",this._handleClick)))}_onRightClick(t){if(!h.hoveredObject)return;for(const t of this.items)this.div.removeChild(t);this.items=[];for(const t of Object.keys(h.hoveredObject._options)){let e;if(t.toLowerCase().includes("color"))e=new S(this.saveDrawings,t);else{if("lineStyle"!==t)continue;e=new G(this.saveDrawings)}let i=t=>e.openMenu(t);this.menuItem(T(t),i,(()=>{document.removeEventListener("click",e.closeMenu),e._div.style.display="none"}))}this.separator(),this.menuItem("Delete Drawing",(()=>this.drawingTool.delete(h.lastHoveredObject))),t.preventDefault(),this.div.style.left=t.clientX+"px",this.div.style.top=t.clientY+"px",this.div.style.display="block",document.body.addEventListener("click",this._handleClick)}menuItem(t,e,i=null){const s=document.createElement("span");s.classList.add("context-menu-item"),this.div.appendChild(s);const o=document.createElement("span");if(o.innerText=t,o.style.pointerEvents="none",s.appendChild(o),i){let t=document.createElement("span");t.innerText="►",t.style.fontSize="8px",t.style.pointerEvents="none",s.appendChild(t)}if(s.addEventListener("mouseover",(()=>{this.hoverItem&&this.hoverItem.closeAction&&this.hoverItem.closeAction(),this.hoverItem={elem:o,action:e,closeAction:i}})),i){let t;s.addEventListener("mouseover",(()=>t=setTimeout((()=>e(s.getBoundingClientRect())),100))),s.addEventListener("mouseout",(()=>clearTimeout(t)))}else s.addEventListener("click",(t=>{e(t),this.div.style.display="none"}));this.items.push(s)}separator(){const t=document.createElement("div");t.style.width="90%",t.style.height="1px",t.style.margin="3px 0px",t.style.backgroundColor=window.pane.borderColor,this.div.appendChild(t),this.items.push(t)}}class M extends w{_type="RayLine";constructor(t,e){super({...t},e),this._point.time=t.time}updatePoints(...t){for(const e of t)e&&(this._point=e);this.requestUpdate()}_onDrag(t){this._addDiffToPoint(this._point,t.logical,t.price),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.series.priceToCoordinate(this._point.price),s=this._point.time?this.chart.timeScale().timeToCoordinate(this._point.time):null;return!(!i||!s)&&(Math.abs(i-t.point.y)s-e)}}class N extends d{_point={x:null,y:null};constructor(t,e){super(e),this._point=t}draw(t){t.useBitmapCoordinateSpace((t=>{if(null==this._point.x)return;const e=t.context,i=this._point.x*t.horizontalPixelRatio;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.beginPath(),e.moveTo(i,0),e.lineTo(i,t.bitmapSize.height),e.stroke()}))}}class R extends _{_source;_point={x:null,y:null};constructor(t){super(t),this._source=t}update(){const t=this._source._point,e=this._source.chart.timeScale(),i=this._source.series;this._point.x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical),this._point.y=i.priceToCoordinate(t.price)}renderer(){return new N(this._point,this._source._options)}}class A{_source;_x=null;constructor(t){this._source=t}update(){if(!this._source.chart||!this._source._point)return;const t=this._source._point,e=this._source.chart.timeScale();this._x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical)}visible(){return!!this._source._options.text}tickVisible(){return!0}coordinate(){return this._x??0}text(){return this._source._options.text||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class B extends h{_type="VerticalLine";_paneViews;_timeAxisViews;_point;_callbackName;_startDragPoint=null;constructor(t,e,i=null){super(e),this._point=t,this._paneViews=[new R(this)],this._callbackName=i,this._timeAxisViews=[new A(this)]}updateAllViews(){this._paneViews.forEach((t=>t.update())),this._timeAxisViews.forEach((t=>t.update()))}timeAxisViews(){return this._timeAxisViews}updatePoints(...t){for(const e of t)e&&(!e.time&&e.logical&&(e.time=this.series.dataByIndex(e.logical)?.time||null),this._point=e);this.requestUpdate()}get points(){return[this._point]}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._addDiffToPoint(this._point,t.logical,0),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.chart.timeScale();let s;return s=this._point.time?i.timeToCoordinate(this._point.time):i.logicalToCoordinate(this._point.logical),!!s&&Math.abs(s-t.point.x){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class P{static TREND_SVG='';static HORZ_SVG='';static RAY_SVG='';static BOX_SVG='';static VERT_SVG=P.RAY_SVG;div;activeIcon=null;buttons=[];_commandFunctions;_handlerID;_drawingTool;constructor(t,e,i,s){this._handlerID=t,this._commandFunctions=s,this._drawingTool=new y(e,i,(()=>this.removeActiveAndSave())),this.div=this._makeToolBox(),new I(this.saveDrawings,this._drawingTool),s.push((t=>{if((t.metaKey||t.ctrlKey)&&"KeyZ"===t.code){const t=this._drawingTool.drawings.pop();return t&&this._drawingTool.delete(t),!0}return!1}))}toJSON(){const{...t}=this;return t}_makeToolBox(){let t=document.createElement("div");t.classList.add("toolbox"),this.buttons.push(this._makeToolBoxElement(f,"KeyT",P.TREND_SVG)),this.buttons.push(this._makeToolBoxElement(w,"KeyH",P.HORZ_SVG)),this.buttons.push(this._makeToolBoxElement(M,"KeyR",P.RAY_SVG)),this.buttons.push(this._makeToolBoxElement(L,"KeyB",P.BOX_SVG)),this.buttons.push(this._makeToolBoxElement(B,"KeyV",P.VERT_SVG,!0));for(const e of this.buttons)t.appendChild(e);return t}_makeToolBoxElement(t,e,i,s=!1){const o=document.createElement("div");o.classList.add("toolbox-button");const n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("width","29"),n.setAttribute("height","29");const r=document.createElementNS("http://www.w3.org/2000/svg","g");r.innerHTML=i,r.setAttribute("fill",window.pane.color),n.appendChild(r),o.appendChild(n);const a={div:o,group:r,type:t};return o.addEventListener("click",(()=>this._onIconClick(a))),this._commandFunctions.push((t=>this._handlerID===window.handlerInFocus&&(!(!t.altKey||t.code!==e)&&(t.preventDefault(),this._onIconClick(a),!0)))),1==s&&(n.style.transform="rotate(90deg)",n.style.transformBox="fill-box",n.style.transformOrigin="center"),o}_onIconClick(t){this.activeIcon&&(this.activeIcon.div.classList.remove("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.stopDrawing(),this.activeIcon===t)?this.activeIcon=null:(this.activeIcon=t,this.activeIcon.div.classList.add("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.beginDrawing(this.activeIcon.type))}removeActiveAndSave=()=>{window.setCursor("default"),this.activeIcon&&this.activeIcon.div.classList.remove("active-toolbox-button"),this.activeIcon=null,this.saveDrawings()};addNewDrawing(t){this._drawingTool.addNewDrawing(t)}clearDrawings(){this._drawingTool.clearDrawings()}saveDrawings=()=>{const t=[];for(const e of this._drawingTool.drawings)t.push({type:e._type,points:e.points,options:e._options});const e=JSON.stringify(t);window.callbackFunction(`save_drawings${this._handlerID}_~_${e}`)};loadDrawings(t){t.forEach((t=>{switch(t.type){case"Box":this._drawingTool.addNewDrawing(new L(t.points[0],t.points[1],t.options));break;case"TrendLine":this._drawingTool.addNewDrawing(new f(t.points[0],t.points[1],t.options));break;case"HorizontalLine":this._drawingTool.addNewDrawing(new w(t.points[0],t.options));break;case"RayLine":this._drawingTool.addNewDrawing(new M(t.points[0],t.options));break;case"VerticalLine":this._drawingTool.addNewDrawing(new B(t.points[0],t.options))}}))}}class O{makeButton;callbackName;div;isOpen=!1;widget;constructor(t,e,i,s,o,n){this.makeButton=t,this.callbackName=e,this.div=document.createElement("div"),this.div.classList.add("topbar-menu"),this.widget=this.makeButton(s+" ↓",null,o,!0,n),this.updateMenuItems(i),this.widget.elem.addEventListener("click",(()=>{if(this.isOpen=!this.isOpen,!this.isOpen)return void(this.div.style.display="none");let t=this.widget.elem.getBoundingClientRect();this.div.style.display="flex",this.div.style.flexDirection="column";let e=t.x+t.width/2;this.div.style.left=e-this.div.clientWidth/2+"px",this.div.style.top=t.y+t.height+"px"})),document.body.appendChild(this.div)}updateMenuItems(t){this.div.innerHTML="",t.forEach((t=>{let e=this.makeButton(t,null,!1,!1);e.elem.addEventListener("click",(()=>{this._clickHandler(e.elem.innerText)})),e.elem.style.margin="4px 4px",e.elem.style.padding="2px 2px",this.div.appendChild(e.elem)})),this.widget.elem.innerText=t[0]+" ↓"}_clickHandler(t){this.widget.elem.innerText=t+" ↓",window.callbackFunction(`${this.callbackName}_~_${t}`),this.div.style.display="none",this.isOpen=!1}}class V{_handler;_div;left;right;constructor(t){this._handler=t,this._div=document.createElement("div"),this._div.classList.add("topbar");const e=t=>{const e=document.createElement("div");return e.classList.add("topbar-container"),e.style.justifyContent=t,this._div.appendChild(e),e};this.left=e("flex-start"),this.right=e("flex-end")}makeSwitcher(t,e,i,s="left"){const o=document.createElement("div");let n;o.style.margin="4px 12px";const r={elem:o,callbackName:i,intervalElements:t.map((t=>{const i=document.createElement("button");i.classList.add("topbar-button"),i.classList.add("switcher-button"),i.style.margin="0px 2px",i.innerText=t,t==e&&(n=i,i.classList.add("active-switcher-button"));const s=V.getClientWidth(i);return i.style.minWidth=s+1+"px",i.addEventListener("click",(()=>r.onItemClicked(i))),o.appendChild(i),i})),onItemClicked:t=>{t!=n&&(n.classList.remove("active-switcher-button"),t.classList.add("active-switcher-button"),n=t,window.callbackFunction(`${r.callbackName}_~_${t.innerText}`))}};return this.appendWidget(o,s,!0),r}makeTextBoxWidget(t,e="left",i=null){if(i){const s=document.createElement("input");return s.classList.add("topbar-textbox-input"),s.value=t,s.style.width=`${s.value.length+2}ch`,s.addEventListener("focus",(()=>{window.textBoxFocused=!0})),s.addEventListener("input",(t=>{t.preventDefault(),s.style.width=`${s.value.length+2}ch`})),s.addEventListener("keydown",(t=>{"Enter"==t.key&&(t.preventDefault(),s.blur())})),s.addEventListener("blur",(()=>{window.callbackFunction(`${i}_~_${s.value}`),window.textBoxFocused=!1})),this.appendWidget(s,e,!0),s}{const i=document.createElement("div");return i.classList.add("topbar-textbox"),i.innerText=t,this.appendWidget(i,e,!0),i}}makeMenu(t,e,i,s,o){return new O(this.makeButton.bind(this),s,t,e,i,o)}makeButton(t,e,i,s=!0,o="left",n=!1){let r=document.createElement("button");r.classList.add("topbar-button"),r.innerText=t,document.body.appendChild(r),r.style.minWidth=r.clientWidth+1+"px",document.body.removeChild(r);let a={elem:r,callbackName:e};if(e){let t;if(n){let e=!1;t=()=>{e=!e,window.callbackFunction(`${a.callbackName}_~_${e}`),r.style.backgroundColor=e?"var(--active-bg-color)":"",r.style.color=e?"var(--active-color)":""}}else t=()=>window.callbackFunction(`${a.callbackName}_~_${r.innerText}`);r.addEventListener("click",t)}return s&&this.appendWidget(r,o,i),a}makeSeparator(t="left"){const e=document.createElement("div");e.classList.add("topbar-seperator");("left"==t?this.left:this.right).appendChild(e)}appendWidget(t,e,i){const s="left"==e?this.left:this.right;i?("left"==e&&s.appendChild(t),this.makeSeparator(e),"right"==e&&s.appendChild(t)):s.appendChild(t),this._handler.reSize()}static getClientWidth(t){document.body.appendChild(t);const e=t.clientWidth;return document.body.removeChild(t),e}}s();return t.Box=L,t.Handler=class{id;commandFunctions=[];wrapper;div;chart;scale;precision=2;series;volumeSeries;legend;_topBar;toolBox;spinner;_seriesList=[];constructor(t,e,i,s,n){this.reSize=this.reSize.bind(this),this.id=t,this.scale={width:e,height:i},this.wrapper=document.createElement("div"),this.wrapper.classList.add("handler"),this.wrapper.style.float=s,this.div=document.createElement("div"),this.div.style.position="relative",this.wrapper.appendChild(this.div),window.containerDiv.append(this.wrapper),this.chart=this._createChart(),this.series=this.createCandlestickSeries(),this.volumeSeries=this.createVolumeSeries(),this.legend=new o(this),document.addEventListener("keydown",(t=>{for(let e=0;ewindow.handlerInFocus=this.id)),this.reSize(),n&&window.addEventListener("resize",(()=>this.reSize()))}reSize(){let t=0!==this.scale.height&&this._topBar?._div.offsetHeight||0;this.chart.resize(window.innerWidth*this.scale.width,window.innerHeight*this.scale.height-t),this.wrapper.style.width=100*this.scale.width+"%",this.wrapper.style.height=100*this.scale.height+"%",0===this.scale.height||0===this.scale.width?this.toolBox&&(this.toolBox.div.style.display="none"):this.toolBox&&(this.toolBox.div.style.display="flex")}_createChart(){return e.createChart(this.div,{width:window.innerWidth*this.scale.width,height:window.innerHeight*this.scale.height,layout:{textColor:window.pane.color,background:{color:"#000000",type:e.ColorType.Solid},fontSize:12},rightPriceScale:{scaleMargins:{top:.3,bottom:.25}},timeScale:{timeVisible:!0,secondsVisible:!1},crosshair:{mode:e.CrosshairMode.Normal,vertLine:{labelBackgroundColor:"rgb(46, 46, 46)"},horzLine:{labelBackgroundColor:"rgb(55, 55, 55)"}},grid:{vertLines:{color:"rgba(29, 30, 38, 5)"},horzLines:{color:"rgba(29, 30, 58, 5)"}},handleScroll:{vertTouchDrag:!0}})}createCandlestickSeries(){const t="rgba(39, 157, 130, 100)",e="rgba(200, 97, 100, 100)",i=this.chart.addCandlestickSeries({upColor:t,borderUpColor:t,wickUpColor:t,downColor:e,borderDownColor:e,wickDownColor:e});return i.priceScale().applyOptions({scaleMargins:{top:.2,bottom:.2}}),i}createVolumeSeries(){const t=this.chart.addHistogramSeries({color:"#26a69a",priceFormat:{type:"volume"},priceScaleId:"volume_scale"});return t.priceScale().applyOptions({scaleMargins:{top:.8,bottom:0}}),t}createLineSeries(t,e){const i=this.chart.addLineSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createHistogramSeries(t,e){const i=this.chart.addHistogramSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createToolBox(){this.toolBox=new P(this.id,this.chart,this.series,this.commandFunctions),this.div.appendChild(this.toolBox.div)}createTopBar(){return this._topBar=new V(this),this.wrapper.prepend(this._topBar._div),this._topBar}toJSON(){const{chart:t,...e}=this;return e}static syncCharts(t,e,i=!1){function s(t,e){e?(t.chart.setCrosshairPosition(e.value||e.close,e.time,t.series),t.legend.legendHandler(e,!0)):t.chart.clearCrosshairPosition()}function o(t,e){return e.time&&e.seriesData.get(t)||null}const n=t.chart.timeScale(),r=e.chart.timeScale(),a=t=>{t&&n.setVisibleLogicalRange(t)},l=t=>{t&&r.setVisibleLogicalRange(t)},h=i=>{s(e,o(t.series,i))},d=i=>{s(t,o(e.series,i))};let c=e;function p(t,e,s,o,n,r){t.wrapper.addEventListener("mouseover",(()=>{c!==t&&(c=t,e.chart.unsubscribeCrosshairMove(s),t.chart.subscribeCrosshairMove(o),i||(e.chart.timeScale().unsubscribeVisibleLogicalRangeChange(n),t.chart.timeScale().subscribeVisibleLogicalRangeChange(r)))}))}p(e,t,h,d,l,a),p(t,e,d,h,a,l),e.chart.subscribeCrosshairMove(d);const u=r.getVisibleLogicalRange();u&&n.setVisibleLogicalRange(u),i||e.chart.timeScale().subscribeVisibleLogicalRangeChange(a)}static makeSearchBox(t){const e=document.createElement("div");e.classList.add("searchbox"),e.style.display="none";const i=document.createElement("div");i.innerHTML='';const s=document.createElement("input");return s.type="text",e.appendChild(i),e.appendChild(s),t.div.appendChild(e),t.commandFunctions.push((i=>window.handlerInFocus===t.id&&!window.textBoxFocused&&("none"===e.style.display?!!/^[a-zA-Z0-9]$/.test(i.key)&&(e.style.display="flex",s.focus(),!0):("Enter"===i.key||"Escape"===i.key)&&("Enter"===i.key&&window.callbackFunction(`search${t.id}_~_${s.value}`),e.style.display="none",s.value="",!0)))),s.addEventListener("input",(()=>s.value=s.value.toUpperCase())),{window:e,box:s}}static makeSpinner(t){t.spinner=document.createElement("div"),t.spinner.classList.add("spinner"),t.wrapper.appendChild(t.spinner);let e=0;!function i(){t.spinner&&(e+=10,t.spinner.style.transform=`translate(-50%, -50%) rotate(${e}deg)`,requestAnimationFrame(i))}()}static _styleMap={"--bg-color":"backgroundColor","--hover-bg-color":"hoverBackgroundColor","--click-bg-color":"clickBackgroundColor","--active-bg-color":"activeBackgroundColor","--muted-bg-color":"mutedBackgroundColor","--border-color":"borderColor","--color":"color","--active-color":"activeColor"};static setRootStyles(t){const e=document.documentElement.style;for(const[i,s]of Object.entries(this._styleMap))e.setProperty(i,t[s])}},t.HorizontalLine=w,t.Legend=o,t.RayLine=M,t.Table=class{_div;callbackName;borderColor;borderWidth;table;rows={};headings;widths;alignments;footer;header;constructor(t,e,i,s,o,n,r=!1,a,l,h,d,c){this._div=document.createElement("div"),this.callbackName=null,this.borderColor=l,this.borderWidth=h,r?(this._div.style.position="absolute",this._div.style.cursor="move"):(this._div.style.position="relative",this._div.style.float=n),this._div.style.zIndex="2000",this.reSize(t,e),this._div.style.display="flex",this._div.style.flexDirection="column",this._div.style.borderRadius="5px",this._div.style.color="white",this._div.style.fontSize="12px",this._div.style.fontVariantNumeric="tabular-nums",this.table=document.createElement("table"),this.table.style.width="100%",this.table.style.borderCollapse="collapse",this._div.style.overflow="hidden",this.headings=i,this.widths=s.map((t=>100*t+"%")),this.alignments=o;let p=this.table.createTHead().insertRow();for(let t=0;t0?c[t]:a,e.style.color=d[t],p.appendChild(e)}let u,_,m=document.createElement("div");if(m.style.overflowY="auto",m.style.overflowX="hidden",m.style.backgroundColor=a,m.appendChild(this.table),this._div.appendChild(m),window.containerDiv.appendChild(this._div),!r)return;let v=t=>{this._div.style.left=t.clientX-u+"px",this._div.style.top=t.clientY-_+"px"},g=()=>{document.removeEventListener("mousemove",v),document.removeEventListener("mouseup",g)};this._div.addEventListener("mousedown",(t=>{u=t.clientX-this._div.offsetLeft,_=t.clientY-this._div.offsetTop,document.addEventListener("mousemove",v),document.addEventListener("mouseup",g)}))}divToButton(t,e){t.addEventListener("mouseover",(()=>t.style.backgroundColor="rgba(60, 60, 60, 0.6)")),t.addEventListener("mouseout",(()=>t.style.backgroundColor="transparent")),t.addEventListener("mousedown",(()=>t.style.backgroundColor="rgba(60, 60, 60)")),t.addEventListener("click",(()=>window.callbackFunction(e))),t.addEventListener("mouseup",(()=>t.style.backgroundColor="rgba(60, 60, 60, 0.6)"))}newRow(t,e=!1){let i=this.table.insertRow();i.style.cursor="default";for(let s=0;s{t&&(window.cursor=t),document.body.style.cursor=window.cursor},t}({},LightweightCharts); +var Lib=function(t,e){"use strict";const i={backgroundColor:"#0c0d0f",hoverBackgroundColor:"#3c434c",clickBackgroundColor:"#50565E",activeBackgroundColor:"rgba(0, 122, 255, 0.7)",mutedBackgroundColor:"rgba(0, 122, 255, 0.3)",borderColor:"#3C434C",color:"#d8d9db",activeColor:"#ececed"};function s(){window.pane={...i},window.containerDiv=document.getElementById("container")||document.createElement("div"),window.setCursor=t=>{t&&(window.cursor=t),document.body.style.cursor=window.cursor},window.cursor="default",window.textBoxFocused=!1}class o{handler;div;seriesContainer;ohlcEnabled=!1;percentEnabled=!1;linesEnabled=!1;colorBasedOnCandle=!1;text;candle;_lines=[];constructor(t){this.legendHandler=this.legendHandler.bind(this),this.handler=t,this.ohlcEnabled=!1,this.percentEnabled=!1,this.linesEnabled=!1,this.colorBasedOnCandle=!1,this.div=document.createElement("div"),this.div.classList.add("legend"),this.div.style.maxWidth=100*t.scale.width-8+"vw",this.div.style.display="none";const e=document.createElement("div");e.style.display="flex",e.style.flexDirection="row",this.seriesContainer=document.createElement("div"),this.seriesContainer.classList.add("series-container"),this.text=document.createElement("span"),this.text.style.lineHeight="1.8",this.candle=document.createElement("div"),e.appendChild(this.seriesContainer),this.div.appendChild(this.text),this.div.appendChild(this.candle),this.div.appendChild(e),t.div.appendChild(this.div),t.chart.subscribeCrosshairMove(this.legendHandler)}toJSON(){const{_lines:t,handler:e,...i}=this;return i}makeSeriesRow(t,e){const i="#FFF";let s=`\n \n \`\n `,o=`\n \n `,n=document.createElement("div");n.style.display="flex",n.style.alignItems="center";let r=document.createElement("div"),a=document.createElement("div");a.classList.add("legend-toggle-switch");let l=document.createElementNS("http://www.w3.org/2000/svg","svg");l.setAttribute("width","22"),l.setAttribute("height","16");let h=document.createElementNS("http://www.w3.org/2000/svg","g");h.innerHTML=s;let d=!0;a.addEventListener("click",(()=>{d?(d=!1,h.innerHTML=o,e.applyOptions({visible:!1})):(d=!0,e.applyOptions({visible:!0}),h.innerHTML=s)})),l.appendChild(h),a.appendChild(l),n.appendChild(r),n.appendChild(a),this.seriesContainer.appendChild(n);const c=e.options().color;this._lines.push({name:t,div:r,row:n,toggle:a,series:e,solid:c.startsWith("rgba")?c.replace(/[^,]+(?=\))/,"1"):c})}legendItemFormat(t,e){return t.toFixed(e).toString().padStart(8," ")}shorthandFormat(t){const e=Math.abs(t);return e>=1e6?(t/1e6).toFixed(1)+"M":e>=1e3?(t/1e3).toFixed(1)+"K":t.toString().padStart(8," ")}legendHandler(t,e=!1){if(!this.ohlcEnabled&&!this.linesEnabled&&!this.percentEnabled)return;const i=this.handler.series.options();if(!t.time)return this.candle.style.color="transparent",void(this.candle.innerHTML=this.candle.innerHTML.replace(i.upColor,"").replace(i.downColor,""));let s,o=null;if(e){const e=this.handler.chart.timeScale();let i=e.timeToCoordinate(t.time);i&&(o=e.coordinateToLogical(i.valueOf())),o&&(s=this.handler.series.dataByIndex(o.valueOf()))}else s=t.seriesData.get(this.handler.series);this.candle.style.color="";let n='';if(s){if(this.ohlcEnabled&&(n+=`O ${this.legendItemFormat(s.open,this.handler.precision)} `,n+=`| H ${this.legendItemFormat(s.high,this.handler.precision)} `,n+=`| L ${this.legendItemFormat(s.low,this.handler.precision)} `,n+=`| C ${this.legendItemFormat(s.close,this.handler.precision)} `),this.percentEnabled){let t=(s.close-s.open)/s.open*100,e=t>0?i.upColor:i.downColor,o=`${t>=0?"+":""}${t.toFixed(2)} %`;this.colorBasedOnCandle?n+=`| ${o}`:n+="| "+o}if(this.handler.volumeSeries){let e;e=o?this.handler.volumeSeries.dataByIndex(o):t.seriesData.get(this.handler.volumeSeries),e&&(n+=this.ohlcEnabled?`
V ${this.shorthandFormat(e.value)}`:"")}}this.candle.innerHTML=n+"
",this._lines.forEach((i=>{if(!this.linesEnabled)return void(i.row.style.display="none");let s,n;if(i.row.style.display="flex",s=e&&o?i.series.dataByIndex(o):t.seriesData.get(i.series),s?.value){if("Histogram"==i.series.seriesType())n=this.shorthandFormat(s.value);else{const t=i.series.options().priceFormat;n=this.legendItemFormat(s.value,t.precision)}i.div.innerHTML=` ${i.name} : ${n}`}}))}}function n(t){if(void 0===t)throw new Error("Value is undefined");return t}class r{_chart=void 0;_series=void 0;requestUpdate(){this._requestUpdate&&this._requestUpdate()}_requestUpdate;attached({chart:t,series:e,requestUpdate:i}){this._chart=t,this._series=e,this._series.subscribeDataChanged(this._fireDataUpdated),this._requestUpdate=i,this.requestUpdate()}detached(){this._chart=void 0,this._series=void 0,this._requestUpdate=void 0}get chart(){return n(this._chart)}get series(){return n(this._series)}_fireDataUpdated(t){this.dataUpdated&&this.dataUpdated(t)}}const a={lineColor:"#1E80F0",lineStyle:e.LineStyle.Solid,width:4};var l;!function(t){t[t.NONE=0]="NONE",t[t.HOVERING=1]="HOVERING",t[t.DRAGGING=2]="DRAGGING",t[t.DRAGGINGP1=3]="DRAGGINGP1",t[t.DRAGGINGP2=4]="DRAGGINGP2",t[t.DRAGGINGP3=5]="DRAGGINGP3",t[t.DRAGGINGP4=6]="DRAGGINGP4"}(l||(l={}));class h extends r{_paneViews=[];_options;_points=[];_state=l.NONE;_startDragPoint=null;_latestHoverPoint=null;static _mouseIsDown=!1;static hoveredObject=null;static lastHoveredObject=null;_listeners=[];constructor(t){super(),this._options={...a,...t}}updateAllViews(){this._paneViews.forEach((t=>t.update()))}paneViews(){return this._paneViews}applyOptions(t){this._options={...this._options,...t},this.requestUpdate()}updatePoints(...t){for(let e=0;ei.name===t&&i.listener===e));this._listeners.splice(this._listeners.indexOf(i),1)}_handleHoverInteraction(t){if(this._latestHoverPoint=t.point,h._mouseIsDown)this._handleDragInteraction(t);else if(this._mouseIsOverDrawing(t)){if(this._state!=l.NONE)return;this._moveToState(l.HOVERING),h.hoveredObject=h.lastHoveredObject=this}else{if(this._state==l.NONE)return;this._moveToState(l.NONE),h.hoveredObject===this&&(h.hoveredObject=null)}}static _eventToPoint(t,e){if(!e||!t.point||!t.logical)return null;const i=e.coordinateToPrice(t.point.y);return null==i?null:{time:t.time||null,logical:t.logical,price:i.valueOf()}}static _getDiff(t,e){return{logical:t.logical-e.logical,price:t.price-e.price}}_addDiffToPoint(t,e,i){t&&(t.logical=t.logical+e,t.price=t.price+i,t.time=this.series.dataByIndex(t.logical)?.time||null)}_handleMouseDownInteraction=()=>{h._mouseIsDown=!0,this._onMouseDown()};_handleMouseUpInteraction=()=>{h._mouseIsDown=!1,this._moveToState(l.HOVERING)};_handleDragInteraction(t){if(this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP1&&this._state!=l.DRAGGINGP2&&this._state!=l.DRAGGINGP3&&this._state!=l.DRAGGINGP4)return;const e=h._eventToPoint(t,this.series);if(!e)return;this._startDragPoint=this._startDragPoint||e;const i=h._getDiff(e,this._startDragPoint);this._onDrag(i),this.requestUpdate(),this._startDragPoint=e}}class d{_options;constructor(t){this._options=t}}class c extends d{_p1;_p2;_hovered;constructor(t,e,i,s){super(i),this._p1=t,this._p2=e,this._hovered=s}_getScaledCoordinates(t){return null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y?null:{x1:Math.round(this._p1.x*t.horizontalPixelRatio),y1:Math.round(this._p1.y*t.verticalPixelRatio),x2:Math.round(this._p2.x*t.horizontalPixelRatio),y2:Math.round(this._p2.y*t.verticalPixelRatio)}}_drawEndCircle(t,e,i){t.context.fillStyle="#000",t.context.beginPath(),t.context.arc(e,i,9,0,2*Math.PI),t.context.stroke(),t.context.fill()}}function p(t,i){const s={[e.LineStyle.Solid]:[],[e.LineStyle.Dotted]:[t.lineWidth,t.lineWidth],[e.LineStyle.Dashed]:[2*t.lineWidth,2*t.lineWidth],[e.LineStyle.LargeDashed]:[6*t.lineWidth,6*t.lineWidth],[e.LineStyle.SparseDotted]:[t.lineWidth,4*t.lineWidth]}[i];t.setLineDash(s)}class u extends d{_point={x:null,y:null};constructor(t,e){super(e),this._point=t}draw(t){t.useBitmapCoordinateSpace((t=>{if(null==this._point.y)return;const e=t.context,i=Math.round(this._point.y*t.verticalPixelRatio),s=this._point.x?this._point.x*t.horizontalPixelRatio:0;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.beginPath(),e.moveTo(s,i),e.lineTo(t.bitmapSize.width,i),e.stroke()}))}}class _{_source;constructor(t){this._source=t}}class m extends _{_p1={x:null,y:null};_p2={x:null,y:null};_source;constructor(t){super(t),this._source=t}update(){if(!this._source.p1||!this._source.p2)return;const t=this._source.series,e=t.priceToCoordinate(this._source.p1.price),i=t.priceToCoordinate(this._source.p2.price),s=this._getX(this._source.p1),o=this._getX(this._source.p2);this._p1={x:s,y:e},this._p2={x:o,y:i}}_getX(t){return this._source.chart.timeScale().logicalToCoordinate(t.logical)}}class v extends _{_source;_point={x:null,y:null};constructor(t){super(t),this._source=t}update(){const t=this._source._point,e=this._source.chart.timeScale(),i=this._source.series;"RayLine"==this._source._type&&(this._point.x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical)),this._point.y=i.priceToCoordinate(t.price)}renderer(){return new u(this._point,this._source._options)}}class g{_source;_y=null;_price=null;constructor(t){this._source=t}update(){if(!this._source.series||!this._source._point)return;this._y=this._source.series.priceToCoordinate(this._source._point.price);const t=this._source.series.options().priceFormat.precision;this._price=this._source._point.price.toFixed(t).toString()}visible(){return!0}tickVisible(){return!0}coordinate(){return this._y??0}text(){return this._source._options.text||this._price||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class w extends h{_type="HorizontalLine";_paneViews;_point;_callbackName;_priceAxisViews;_startDragPoint=null;constructor(t,e,i=null){super(e),this._point=t,this._point.time=null,this._paneViews=[new v(this)],this._priceAxisViews=[new g(this)],this._callbackName=i}get points(){return[this._point]}updatePoints(...t){for(const e of t)e&&(this._point.price=e.price);this.requestUpdate()}updateAllViews(){this._paneViews.forEach((t=>t.update())),this._priceAxisViews.forEach((t=>t.update()))}priceAxisViews(){return this._priceAxisViews}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._addDiffToPoint(this._point,0,t.price),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.series.priceToCoordinate(this._point.price);return!!i&&Math.abs(i-t.point.y){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class y{_chart;_series;_finishDrawingCallback=null;_drawings=[];_activeDrawing=null;_isDrawing=!1;_drawingType=null;constructor(t,e,i=null){this._chart=t,this._series=e,this._finishDrawingCallback=i,this._chart.subscribeClick(this._clickHandler),this._chart.subscribeCrosshairMove(this._moveHandler)}_clickHandler=t=>this._onClick(t);_moveHandler=t=>this._onMouseMove(t);beginDrawing(t){this._drawingType=t,this._isDrawing=!0}stopDrawing(){this._isDrawing=!1,this._activeDrawing=null}get drawings(){return this._drawings}addNewDrawing(t){this._series.attachPrimitive(t),this._drawings.push(t)}delete(t){if(null==t)return;const e=this._drawings.indexOf(t);-1!=e&&(this._drawings.splice(e,1),t.detach())}clearDrawings(){for(const t of this._drawings)t.detach();this._drawings=[]}repositionOnTime(){for(const t of this.drawings){const e=[];for(const i of t.points){if(!i){e.push(i);continue}const t=i.time?this._chart.timeScale().coordinateToLogical(this._chart.timeScale().timeToCoordinate(i.time)||0):i.logical;e.push({time:i.time,logical:t,price:i.price})}t.updatePoints(...e)}}_onClick(t){if(!this._isDrawing)return;const e=h._eventToPoint(t,this._series);if(e)if(null==this._activeDrawing){if(null==this._drawingType)return;this._activeDrawing=new this._drawingType(e,e),this._series.attachPrimitive(this._activeDrawing),this._drawingType==w&&this._onClick(t)}else{if(this._drawings.push(this._activeDrawing),this.stopDrawing(),!this._finishDrawingCallback)return;this._finishDrawingCallback()}}_onMouseMove(t){if(!t)return;for(const e of this._drawings)e._handleHoverInteraction(t);if(!this._isDrawing||!this._activeDrawing)return;const e=h._eventToPoint(t,this._series);e&&this._activeDrawing.updatePoints(null,e)}}class b extends c{constructor(t,e,i,s){super(t,e,i,s)}draw(t){t.useBitmapCoordinateSpace((t=>{if(null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y)return;const e=t.context,i=this._getScaledCoordinates(t);i&&(e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.beginPath(),e.moveTo(i.x1,i.y1),e.lineTo(i.x2,i.y2),e.stroke(),this._hovered&&(this._drawEndCircle(t,i.x1,i.y1),this._drawEndCircle(t,i.x2,i.y2)))}))}}class x extends m{constructor(t){super(t)}renderer(){return new b(this._p1,this._p2,this._source._options,this._source.hovered)}}class C extends h{_paneViews=[];_hovered=!1;constructor(t,e,i){super(),this.points.push(t),this.points.push(e),this._options={...a,...i}}setFirstPoint(t){this.updatePoints(t)}setSecondPoint(t){this.updatePoints(null,t)}get p1(){return this.points[0]}get p2(){return this.points[1]}get hovered(){return this._hovered}}class f extends C{_type="TrendLine";constructor(t,e,i){super(t,e,i),this._paneViews=[new x(this)]}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._hovered=!1,this.requestUpdate(),this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this.requestUpdate(),this._subscribe("mousedown",this._handleMouseDownInteraction),this._unsubscribe("mouseup",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGINGP1:case l.DRAGGINGP2:case l.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP1||this._addDiffToPoint(this.p1,t.logical,t.price),this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP2||this._addDiffToPoint(this.p2,t.logical,t.price)}_onMouseDown(){this._startDragPoint=null;const t=this._latestHoverPoint;if(!t)return;const e=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(e.x&&i.x&&e.y&&i.y))return this._moveToState(l.DRAGGING);Math.abs(t.x-e.x)<10&&Math.abs(t.y-e.y)<10?this._moveToState(l.DRAGGINGP1):Math.abs(t.x-i.x)<10&&Math.abs(t.y-i.y)<10?this._moveToState(l.DRAGGINGP2):this._moveToState(l.DRAGGING)}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this._paneViews[0]._p1.x,s=this._paneViews[0]._p1.y,o=this._paneViews[0]._p2.x,n=this._paneViews[0]._p2.y;if(!(i&&o&&s&&n))return!1;const r=t.point.x,a=t.point.y;if(r<=Math.min(i,o)-e||r>=Math.max(i,o)+e)return!1;return Math.abs((n-s)*r-(o-i)*a+o*s-n*i)/Math.sqrt((n-s)**2+(o-i)**2)<=e}}class D extends c{constructor(t,e,i,s){super(t,e,i,s)}draw(t){t.useBitmapCoordinateSpace((t=>{const e=t.context,i=this._getScaledCoordinates(t);if(!i)return;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.fillStyle=this._options.fillColor;const s=Math.min(i.x1,i.x2),o=Math.min(i.y1,i.y2),n=Math.abs(i.x1-i.x2),r=Math.abs(i.y1-i.y2);e.strokeRect(s,o,n,r),e.fillRect(s,o,n,r),this._hovered&&(this._drawEndCircle(t,s,o),this._drawEndCircle(t,s+n,o),this._drawEndCircle(t,s+n,o+r),this._drawEndCircle(t,s,o+r))}))}}class k extends m{constructor(t){super(t)}renderer(){return new D(this._p1,this._p2,this._source._options,this._source.hovered)}}const E={fillEnabled:!0,fillColor:"rgba(255, 255, 255, 0.2)",...a};class L extends C{_type="Box";constructor(t,e,i){super(t,e,i),this._options={...E,...i},this._paneViews=[new k(this)]}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._hovered=!1,this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this._unsubscribe("mouseup",this._handleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGINGP1:case l.DRAGGINGP2:case l.DRAGGINGP3:case l.DRAGGINGP4:case l.DRAGGING:document.body.style.cursor="grabbing",document.body.addEventListener("mouseup",this._handleMouseUpInteraction),this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP1||this._addDiffToPoint(this.p1,t.logical,t.price),this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP2||this._addDiffToPoint(this.p2,t.logical,t.price),this._state!=l.DRAGGING&&(this._state==l.DRAGGINGP3&&(this._addDiffToPoint(this.p1,t.logical,0),this._addDiffToPoint(this.p2,0,t.price)),this._state==l.DRAGGINGP4&&(this._addDiffToPoint(this.p1,0,t.price),this._addDiffToPoint(this.p2,t.logical,0)))}_onMouseDown(){this._startDragPoint=null;const t=this._latestHoverPoint,e=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(e.x&&i.x&&e.y&&i.y))return this._moveToState(l.DRAGGING);const s=10;Math.abs(t.x-e.x)l-p&&rh-p&&ai.appendChild(this.makeColorBox(t))));let s=document.createElement("div");s.style.backgroundColor=window.pane.borderColor,s.style.height="1px",s.style.width="130px";let o=document.createElement("div");o.style.margin="10px";let n=document.createElement("div");n.style.color="lightgray",n.style.fontSize="12px",n.innerText="Opacity",this._opacityLabel=document.createElement("div"),this._opacityLabel.style.color="lightgray",this._opacityLabel.style.fontSize="12px",this._opacitySlider=document.createElement("input"),this._opacitySlider.type="range",this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%",this._opacitySlider.oninput=()=>{this._opacityLabel.innerText=this._opacitySlider.value+"%",this.opacity=parseInt(this._opacitySlider.value)/100,this.updateColor()},o.appendChild(n),o.appendChild(this._opacitySlider),o.appendChild(this._opacityLabel),this._div.appendChild(i),this._div.appendChild(s),this._div.appendChild(o),window.containerDiv.appendChild(this._div)}_updateOpacitySlider(){this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%"}makeColorBox(t){const e=document.createElement("div");e.style.width="18px",e.style.height="18px",e.style.borderRadius="3px",e.style.margin="3px",e.style.boxSizing="border-box",e.style.backgroundColor=t,e.addEventListener("mouseover",(()=>e.style.border="2px solid lightgray")),e.addEventListener("mouseout",(()=>e.style.border="none"));const i=S.extractRGBA(t);return e.addEventListener("click",(()=>{this.rgba=i,this.updateColor()})),e}static extractRGBA(t){const e=document.createElement("div");e.style.color=t,document.body.appendChild(e);const i=getComputedStyle(e).color;document.body.removeChild(e);const s=i.match(/\d+/g)?.map(Number);if(!s)return[];let o=i.includes("rgba")?parseFloat(i.split(",")[3]):1;return[s[0],s[1],s[2],o]}updateColor(){if(!h.lastHoveredObject||!this.rgba)return;const t=`rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`;h.lastHoveredObject.applyOptions({[this.colorOption]:t}),this.saveDrawings()}openMenu(t){h.lastHoveredObject&&(this.rgba=S.extractRGBA(h.lastHoveredObject._options[this.colorOption]),this.opacity=this.rgba[3],this._updateOpacitySlider(),this._div.style.top=t.top-30+"px",this._div.style.left=t.right+"px",this._div.style.display="flex",setTimeout((()=>document.addEventListener("mousedown",(t=>{this._div.contains(t.target)||this.closeMenu()}))),10))}closeMenu(){document.body.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}class G{static _styles=[{name:"Solid",var:e.LineStyle.Solid},{name:"Dotted",var:e.LineStyle.Dotted},{name:"Dashed",var:e.LineStyle.Dashed},{name:"Large Dashed",var:e.LineStyle.LargeDashed},{name:"Sparse Dotted",var:e.LineStyle.SparseDotted}];_div;_saveDrawings;constructor(t){this._saveDrawings=t,this._div=document.createElement("div"),this._div.classList.add("context-menu"),G._styles.forEach((t=>{this._div.appendChild(this._makeTextBox(t.name,t.var))})),window.containerDiv.appendChild(this._div)}_makeTextBox(t,e){const i=document.createElement("span");return i.classList.add("context-menu-item"),i.innerText=t,i.addEventListener("click",(()=>{h.lastHoveredObject?.applyOptions({lineStyle:e}),this._saveDrawings()})),i}openMenu(t){this._div.style.top=t.top-30+"px",this._div.style.left=t.right+"px",this._div.style.display="block",setTimeout((()=>document.addEventListener("mousedown",(t=>{this._div.contains(t.target)||this.closeMenu()}))),10)}closeMenu(){document.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}function T(t){const e=[];for(const i of t)0==e.length?e.push(i.toUpperCase()):i==i.toUpperCase()?e.push(" "+i):e.push(i);return e.join("")}class I{saveDrawings;drawingTool;div;hoverItem;items=[];constructor(t,e){this.saveDrawings=t,this.drawingTool=e,this._onRightClick=this._onRightClick.bind(this),this.div=document.createElement("div"),this.div.classList.add("context-menu"),document.body.appendChild(this.div),this.hoverItem=null,document.body.addEventListener("contextmenu",this._onRightClick)}_handleClick=t=>this._onClick(t);_onClick(t){t.target&&(this.div.contains(t.target)||(this.div.style.display="none",document.body.removeEventListener("click",this._handleClick)))}_onRightClick(t){if(!h.hoveredObject)return;for(const t of this.items)this.div.removeChild(t);this.items=[];for(const t of Object.keys(h.hoveredObject._options)){let e;if(t.toLowerCase().includes("color"))e=new S(this.saveDrawings,t);else{if("lineStyle"!==t)continue;e=new G(this.saveDrawings)}let i=t=>e.openMenu(t);this.menuItem(T(t),i,(()=>{document.removeEventListener("click",e.closeMenu),e._div.style.display="none"}))}this.separator(),this.menuItem("Delete Drawing",(()=>this.drawingTool.delete(h.lastHoveredObject))),t.preventDefault(),this.div.style.left=t.clientX+"px",this.div.style.top=t.clientY+"px",this.div.style.display="block",document.body.addEventListener("click",this._handleClick)}menuItem(t,e,i=null){const s=document.createElement("span");s.classList.add("context-menu-item"),this.div.appendChild(s);const o=document.createElement("span");if(o.innerText=t,o.style.pointerEvents="none",s.appendChild(o),i){let t=document.createElement("span");t.innerText="►",t.style.fontSize="8px",t.style.pointerEvents="none",s.appendChild(t)}if(s.addEventListener("mouseover",(()=>{this.hoverItem&&this.hoverItem.closeAction&&this.hoverItem.closeAction(),this.hoverItem={elem:o,action:e,closeAction:i}})),i){let t;s.addEventListener("mouseover",(()=>t=setTimeout((()=>e(s.getBoundingClientRect())),100))),s.addEventListener("mouseout",(()=>clearTimeout(t)))}else s.addEventListener("click",(t=>{e(t),this.div.style.display="none"}));this.items.push(s)}separator(){const t=document.createElement("div");t.style.width="90%",t.style.height="1px",t.style.margin="3px 0px",t.style.backgroundColor=window.pane.borderColor,this.div.appendChild(t),this.items.push(t)}}class M extends w{_type="RayLine";constructor(t,e){super({...t},e),this._point.time=t.time}updatePoints(...t){for(const e of t)e&&(this._point=e);this.requestUpdate()}_onDrag(t){this._addDiffToPoint(this._point,t.logical,t.price),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.series.priceToCoordinate(this._point.price),s=this._point.time?this.chart.timeScale().timeToCoordinate(this._point.time):null;return!(!i||!s)&&(Math.abs(i-t.point.y)s-e)}}class N extends d{_point={x:null,y:null};constructor(t,e){super(e),this._point=t}draw(t){t.useBitmapCoordinateSpace((t=>{if(null==this._point.x)return;const e=t.context,i=this._point.x*t.horizontalPixelRatio;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.beginPath(),e.moveTo(i,0),e.lineTo(i,t.bitmapSize.height),e.stroke()}))}}class R extends _{_source;_point={x:null,y:null};constructor(t){super(t),this._source=t}update(){const t=this._source._point,e=this._source.chart.timeScale(),i=this._source.series;this._point.x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical),this._point.y=i.priceToCoordinate(t.price)}renderer(){return new N(this._point,this._source._options)}}class A{_source;_x=null;constructor(t){this._source=t}update(){if(!this._source.chart||!this._source._point)return;const t=this._source._point,e=this._source.chart.timeScale();this._x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical)}visible(){return!!this._source._options.text}tickVisible(){return!0}coordinate(){return this._x??0}text(){return this._source._options.text||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class B extends h{_type="VerticalLine";_paneViews;_timeAxisViews;_point;_callbackName;_startDragPoint=null;constructor(t,e,i=null){super(e),this._point=t,this._paneViews=[new R(this)],this._callbackName=i,this._timeAxisViews=[new A(this)]}updateAllViews(){this._paneViews.forEach((t=>t.update())),this._timeAxisViews.forEach((t=>t.update()))}timeAxisViews(){return this._timeAxisViews}updatePoints(...t){for(const e of t)e&&(!e.time&&e.logical&&(e.time=this.series.dataByIndex(e.logical)?.time||null),this._point=e);this.requestUpdate()}get points(){return[this._point]}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._addDiffToPoint(this._point,t.logical,0),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.chart.timeScale();let s;return s=this._point.time?i.timeToCoordinate(this._point.time):i.logicalToCoordinate(this._point.logical),!!s&&Math.abs(s-t.point.x){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class P{static TREND_SVG='';static HORZ_SVG='';static RAY_SVG='';static BOX_SVG='';static VERT_SVG=P.RAY_SVG;div;activeIcon=null;buttons=[];_commandFunctions;_handlerID;_drawingTool;constructor(t,e,i,s){this._handlerID=t,this._commandFunctions=s,this._drawingTool=new y(e,i,(()=>this.removeActiveAndSave())),this.div=this._makeToolBox(),new I(this.saveDrawings,this._drawingTool),s.push((t=>{if((t.metaKey||t.ctrlKey)&&"KeyZ"===t.code){const t=this._drawingTool.drawings.pop();return t&&this._drawingTool.delete(t),!0}return!1}))}toJSON(){const{...t}=this;return t}_makeToolBox(){let t=document.createElement("div");t.classList.add("toolbox"),this.buttons.push(this._makeToolBoxElement(f,"KeyT",P.TREND_SVG)),this.buttons.push(this._makeToolBoxElement(w,"KeyH",P.HORZ_SVG)),this.buttons.push(this._makeToolBoxElement(M,"KeyR",P.RAY_SVG)),this.buttons.push(this._makeToolBoxElement(L,"KeyB",P.BOX_SVG)),this.buttons.push(this._makeToolBoxElement(B,"KeyV",P.VERT_SVG,!0));for(const e of this.buttons)t.appendChild(e);return t}_makeToolBoxElement(t,e,i,s=!1){const o=document.createElement("div");o.classList.add("toolbox-button");const n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("width","29"),n.setAttribute("height","29");const r=document.createElementNS("http://www.w3.org/2000/svg","g");r.innerHTML=i,r.setAttribute("fill",window.pane.color),n.appendChild(r),o.appendChild(n);const a={div:o,group:r,type:t};return o.addEventListener("click",(()=>this._onIconClick(a))),this._commandFunctions.push((t=>this._handlerID===window.handlerInFocus&&(!(!t.altKey||t.code!==e)&&(t.preventDefault(),this._onIconClick(a),!0)))),1==s&&(n.style.transform="rotate(90deg)",n.style.transformBox="fill-box",n.style.transformOrigin="center"),o}_onIconClick(t){this.activeIcon&&(this.activeIcon.div.classList.remove("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.stopDrawing(),this.activeIcon===t)?this.activeIcon=null:(this.activeIcon=t,this.activeIcon.div.classList.add("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.beginDrawing(this.activeIcon.type))}removeActiveAndSave=()=>{window.setCursor("default"),this.activeIcon&&this.activeIcon.div.classList.remove("active-toolbox-button"),this.activeIcon=null,this.saveDrawings()};addNewDrawing(t){this._drawingTool.addNewDrawing(t)}clearDrawings(){this._drawingTool.clearDrawings()}saveDrawings=()=>{const t=[];for(const e of this._drawingTool.drawings)t.push({type:e._type,points:e.points,options:e._options});const e=JSON.stringify(t);window.callbackFunction(`save_drawings${this._handlerID}_~_${e}`)};loadDrawings(t){t.forEach((t=>{switch(t.type){case"Box":this._drawingTool.addNewDrawing(new L(t.points[0],t.points[1],t.options));break;case"TrendLine":this._drawingTool.addNewDrawing(new f(t.points[0],t.points[1],t.options));break;case"HorizontalLine":this._drawingTool.addNewDrawing(new w(t.points[0],t.options));break;case"RayLine":this._drawingTool.addNewDrawing(new M(t.points[0],t.options));break;case"VerticalLine":this._drawingTool.addNewDrawing(new B(t.points[0],t.options))}}))}}class O{makeButton;callbackName;div;isOpen=!1;widget;constructor(t,e,i,s,o,n){this.makeButton=t,this.callbackName=e,this.div=document.createElement("div"),this.div.classList.add("topbar-menu"),this.widget=this.makeButton(s+" ↓",null,o,!0,n),this.updateMenuItems(i),this.widget.elem.addEventListener("click",(()=>{if(this.isOpen=!this.isOpen,!this.isOpen)return void(this.div.style.display="none");let t=this.widget.elem.getBoundingClientRect();this.div.style.display="flex",this.div.style.flexDirection="column";let e=t.x+t.width/2;this.div.style.left=e-this.div.clientWidth/2+"px",this.div.style.top=t.y+t.height+"px"})),document.body.appendChild(this.div)}updateMenuItems(t){this.div.innerHTML="",t.forEach((t=>{let e=this.makeButton(t,null,!1,!1);e.elem.addEventListener("click",(()=>{this._clickHandler(e.elem.innerText)})),e.elem.style.margin="4px 4px",e.elem.style.padding="2px 2px",this.div.appendChild(e.elem)})),this.widget.elem.innerText=t[0]+" ↓"}_clickHandler(t){this.widget.elem.innerText=t+" ↓",window.callbackFunction(`${this.callbackName}_~_${t}`),this.div.style.display="none",this.isOpen=!1}}class V{_handler;_div;left;right;constructor(t){this._handler=t,this._div=document.createElement("div"),this._div.classList.add("topbar");const e=t=>{const e=document.createElement("div");return e.classList.add("topbar-container"),e.style.justifyContent=t,this._div.appendChild(e),e};this.left=e("flex-start"),this.right=e("flex-end")}makeSwitcher(t,e,i,s="left"){const o=document.createElement("div");let n;o.style.margin="4px 12px";const r={elem:o,callbackName:i,intervalElements:t.map((t=>{const i=document.createElement("button");i.classList.add("topbar-button"),i.classList.add("switcher-button"),i.style.margin="0px 2px",i.innerText=t,t==e&&(n=i,i.classList.add("active-switcher-button"));const s=V.getClientWidth(i);return i.style.minWidth=s+1+"px",i.addEventListener("click",(()=>r.onItemClicked(i))),o.appendChild(i),i})),onItemClicked:t=>{t!=n&&(n.classList.remove("active-switcher-button"),t.classList.add("active-switcher-button"),n=t,window.callbackFunction(`${r.callbackName}_~_${t.innerText}`))}};return this.appendWidget(o,s,!0),r}makeTextBoxWidget(t,e="left",i=null){if(i){const s=document.createElement("input");return s.classList.add("topbar-textbox-input"),s.value=t,s.style.width=`${s.value.length+2}ch`,s.addEventListener("focus",(()=>{window.textBoxFocused=!0})),s.addEventListener("input",(t=>{t.preventDefault(),s.style.width=`${s.value.length+2}ch`})),s.addEventListener("keydown",(t=>{"Enter"==t.key&&(t.preventDefault(),s.blur())})),s.addEventListener("blur",(()=>{window.callbackFunction(`${i}_~_${s.value}`),window.textBoxFocused=!1})),this.appendWidget(s,e,!0),s}{const i=document.createElement("div");return i.classList.add("topbar-textbox"),i.innerText=t,this.appendWidget(i,e,!0),i}}makeMenu(t,e,i,s,o){return new O(this.makeButton.bind(this),s,t,e,i,o)}makeButton(t,e,i,s=!0,o="left",n=!1){let r=document.createElement("button");r.classList.add("topbar-button"),r.innerText=t,document.body.appendChild(r),r.style.minWidth=r.clientWidth+1+"px",document.body.removeChild(r);let a={elem:r,callbackName:e};if(e){let t;if(n){let e=!1;t=()=>{e=!e,window.callbackFunction(`${a.callbackName}_~_${e}`),r.style.backgroundColor=e?"var(--active-bg-color)":"",r.style.color=e?"var(--active-color)":""}}else t=()=>window.callbackFunction(`${a.callbackName}_~_${r.innerText}`);r.addEventListener("click",t)}return s&&this.appendWidget(r,o,i),a}makeSeparator(t="left"){const e=document.createElement("div");e.classList.add("topbar-seperator");("left"==t?this.left:this.right).appendChild(e)}appendWidget(t,e,i){const s="left"==e?this.left:this.right;i?("left"==e&&s.appendChild(t),this.makeSeparator(e),"right"==e&&s.appendChild(t)):s.appendChild(t),this._handler.reSize()}static getClientWidth(t){document.body.appendChild(t);const e=t.clientWidth;return document.body.removeChild(t),e}}s();return t.Box=L,t.Handler=class{id;commandFunctions=[];wrapper;div;chart;scale;precision=2;series;volumeSeries;legend;_topBar;toolBox;spinner;_seriesList=[];constructor(t,e,i,s,n){this.reSize=this.reSize.bind(this),this.id=t,this.scale={width:e,height:i},this.wrapper=document.createElement("div"),this.wrapper.classList.add("handler"),this.wrapper.style.float=s,this.div=document.createElement("div"),this.div.style.position="relative",this.wrapper.appendChild(this.div),window.containerDiv.append(this.wrapper),this.chart=this._createChart(),this.series=this.createCandlestickSeries(),this.volumeSeries=this.createVolumeSeries(),this.legend=new o(this),document.addEventListener("keydown",(t=>{for(let e=0;ewindow.handlerInFocus=this.id)),this.reSize(),n&&window.addEventListener("resize",(()=>this.reSize()))}reSize(){let t=0!==this.scale.height&&this._topBar?._div.offsetHeight||0;this.chart.resize(window.innerWidth*this.scale.width,window.innerHeight*this.scale.height-t),this.wrapper.style.width=100*this.scale.width+"%",this.wrapper.style.height=100*this.scale.height+"%",0===this.scale.height||0===this.scale.width?this.toolBox&&(this.toolBox.div.style.display="none"):this.toolBox&&(this.toolBox.div.style.display="flex")}_createChart(){return e.createChart(this.div,{width:window.innerWidth*this.scale.width,height:window.innerHeight*this.scale.height,layout:{textColor:window.pane.color,background:{color:"#000000",type:e.ColorType.Solid},fontSize:12},rightPriceScale:{scaleMargins:{top:.3,bottom:.25}},timeScale:{timeVisible:!0,secondsVisible:!1},crosshair:{mode:e.CrosshairMode.Normal,vertLine:{labelBackgroundColor:"rgb(46, 46, 46)"},horzLine:{labelBackgroundColor:"rgb(55, 55, 55)"}},grid:{vertLines:{color:"rgba(29, 30, 38, 5)"},horzLines:{color:"rgba(29, 30, 58, 5)"}},handleScroll:{vertTouchDrag:!0}})}createCandlestickSeries(){const t="rgba(39, 157, 130, 100)",e="rgba(200, 97, 100, 100)",i=this.chart.addCandlestickSeries({upColor:t,borderUpColor:t,wickUpColor:t,downColor:e,borderDownColor:e,wickDownColor:e});return i.priceScale().applyOptions({scaleMargins:{top:.2,bottom:.2}}),i}createVolumeSeries(){const t=this.chart.addHistogramSeries({color:"#26a69a",priceFormat:{type:"volume"},priceScaleId:"volume_scale"});return t.priceScale().applyOptions({scaleMargins:{top:.8,bottom:0}}),t}createLineSeries(t,e){const i=this.chart.addLineSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createHistogramSeries(t,e){const i=this.chart.addHistogramSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createAreaSeries(t,e){const i=this.chart.addAreaSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createBarSeries(t,e){const i=this.chart.addBarSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createToolBox(){this.toolBox=new P(this.id,this.chart,this.series,this.commandFunctions),this.div.appendChild(this.toolBox.div)}createTopBar(){return this._topBar=new V(this),this.wrapper.prepend(this._topBar._div),this._topBar}toJSON(){const{chart:t,...e}=this;return e}static syncCharts(t,e,i=!1){function s(t,e){e?(t.chart.setCrosshairPosition(e.value||e.close,e.time,t.series),t.legend.legendHandler(e,!0)):t.chart.clearCrosshairPosition()}function o(t,e){return e.time&&e.seriesData.get(t)||null}const n=t.chart.timeScale(),r=e.chart.timeScale(),a=t=>{t&&n.setVisibleLogicalRange(t)},l=t=>{t&&r.setVisibleLogicalRange(t)},h=i=>{s(e,o(t.series,i))},d=i=>{s(t,o(e.series,i))};let c=e;function p(t,e,s,o,n,r){t.wrapper.addEventListener("mouseover",(()=>{c!==t&&(c=t,e.chart.unsubscribeCrosshairMove(s),t.chart.subscribeCrosshairMove(o),i||(e.chart.timeScale().unsubscribeVisibleLogicalRangeChange(n),t.chart.timeScale().subscribeVisibleLogicalRangeChange(r)))}))}p(e,t,h,d,l,a),p(t,e,d,h,a,l),e.chart.subscribeCrosshairMove(d);const u=r.getVisibleLogicalRange();u&&n.setVisibleLogicalRange(u),i||e.chart.timeScale().subscribeVisibleLogicalRangeChange(a)}static makeSearchBox(t){const e=document.createElement("div");e.classList.add("searchbox"),e.style.display="none";const i=document.createElement("div");i.innerHTML='';const s=document.createElement("input");return s.type="text",e.appendChild(i),e.appendChild(s),t.div.appendChild(e),t.commandFunctions.push((i=>window.handlerInFocus===t.id&&!window.textBoxFocused&&("none"===e.style.display?!!/^[a-zA-Z0-9]$/.test(i.key)&&(e.style.display="flex",s.focus(),!0):("Enter"===i.key||"Escape"===i.key)&&("Enter"===i.key&&window.callbackFunction(`search${t.id}_~_${s.value}`),e.style.display="none",s.value="",!0)))),s.addEventListener("input",(()=>s.value=s.value.toUpperCase())),{window:e,box:s}}static makeSpinner(t){t.spinner=document.createElement("div"),t.spinner.classList.add("spinner"),t.wrapper.appendChild(t.spinner);let e=0;!function i(){t.spinner&&(e+=10,t.spinner.style.transform=`translate(-50%, -50%) rotate(${e}deg)`,requestAnimationFrame(i))}()}static _styleMap={"--bg-color":"backgroundColor","--hover-bg-color":"hoverBackgroundColor","--click-bg-color":"clickBackgroundColor","--active-bg-color":"activeBackgroundColor","--muted-bg-color":"mutedBackgroundColor","--border-color":"borderColor","--color":"color","--active-color":"activeColor"};static setRootStyles(t){const e=document.documentElement.style;for(const[i,s]of Object.entries(this._styleMap))e.setProperty(i,t[s])}},t.HorizontalLine=w,t.Legend=o,t.RayLine=M,t.Table=class{_div;callbackName;borderColor;borderWidth;table;rows={};headings;widths;alignments;footer;header;constructor(t,e,i,s,o,n,r=!1,a,l,h,d,c){this._div=document.createElement("div"),this.callbackName=null,this.borderColor=l,this.borderWidth=h,r?(this._div.style.position="absolute",this._div.style.cursor="move"):(this._div.style.position="relative",this._div.style.float=n),this._div.style.zIndex="2000",this.reSize(t,e),this._div.style.display="flex",this._div.style.flexDirection="column",this._div.style.borderRadius="5px",this._div.style.color="white",this._div.style.fontSize="12px",this._div.style.fontVariantNumeric="tabular-nums",this.table=document.createElement("table"),this.table.style.width="100%",this.table.style.borderCollapse="collapse",this._div.style.overflow="hidden",this.headings=i,this.widths=s.map((t=>100*t+"%")),this.alignments=o;let p=this.table.createTHead().insertRow();for(let t=0;t0?c[t]:a,e.style.color=d[t],p.appendChild(e)}let u,_,m=document.createElement("div");if(m.style.overflowY="auto",m.style.overflowX="hidden",m.style.backgroundColor=a,m.appendChild(this.table),this._div.appendChild(m),window.containerDiv.appendChild(this._div),!r)return;let v=t=>{this._div.style.left=t.clientX-u+"px",this._div.style.top=t.clientY-_+"px"},g=()=>{document.removeEventListener("mousemove",v),document.removeEventListener("mouseup",g)};this._div.addEventListener("mousedown",(t=>{u=t.clientX-this._div.offsetLeft,_=t.clientY-this._div.offsetTop,document.addEventListener("mousemove",v),document.addEventListener("mouseup",g)}))}divToButton(t,e){t.addEventListener("mouseover",(()=>t.style.backgroundColor="rgba(60, 60, 60, 0.6)")),t.addEventListener("mouseout",(()=>t.style.backgroundColor="transparent")),t.addEventListener("mousedown",(()=>t.style.backgroundColor="rgba(60, 60, 60)")),t.addEventListener("click",(()=>window.callbackFunction(e))),t.addEventListener("mouseup",(()=>t.style.backgroundColor="rgba(60, 60, 60, 0.6)"))}newRow(t,e=!1){let i=this.table.insertRow();i.style.cursor="default";for(let s=0;s{t&&(window.cursor=t),document.body.style.cursor=window.cursor},t}({},LightweightCharts); From 899ddb1821b3fc83f014172031d0e615bf925328 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Sun, 27 Oct 2024 04:40:49 -0700 Subject: [PATCH 06/89] Update abstract.py formatting --- lightweight_charts/abstract.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 71ba09c..750aa1b 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -800,7 +800,7 @@ def __init__(self, window: Window, width: float = 1.0, height: float = 1.0, self._height = height self.events: Events = Events(self) - from .polygon import PolygonAPI + from lightweight_charts.polygon import PolygonAPI self.polygon: PolygonAPI = PolygonAPI(self) self.run_script( @@ -843,8 +843,9 @@ def create_histogram( def create_area( self, name: str = '', top_color: str ='rgba(0, 100, 0, 0.5)', - bottom_color: str ='rgba(138, 3, 3, 0.5)',invert: bool = False, color: str ='rgba(0,0,255,1)', style: LINE_STYLE = 'solid', width: int = 2, - price_line: bool = True, price_label: bool = True, price_scale_id: Optional[str] = None + bottom_color: str ='rgba(138, 3, 3, 0.5)',invert: bool = False, color: str ='rgba(0,0,255,1)', + style: LINE_STYLE = 'solid', width: int = 2, price_line: bool = True, price_label: bool = True, + price_scale_id: Optional[str] = None ) -> Area: """ Creates and returns an Area object. From a9811271c43e923cf3b61190618651b5ea661b70 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:31:31 -0700 Subject: [PATCH 07/89] Update abstract.py Fix: Bar update method --- lightweight_charts/abstract.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 750aa1b..31a0e71 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -580,6 +580,7 @@ def delete(self): delete {self.id}legendItem delete {self.id} ''') + class Bar(SeriesCommon): def __init__(self, chart, name, up_color='#26a69a', down_color='#ef5350', open_visible=True, thin_bars=True, price_line=True, price_label=False, price_scale_id=None): super().__init__(chart, name) @@ -610,7 +611,20 @@ def set(self, df: Optional[pd.DataFrame] = None): self._last_bar = df.iloc[-1] self.run_script(f'{self.id}.series.setData({js_data(df)})') + def update(self, series: pd.Series, _from_tick=False): + """ + Updates the data from a bar; + if series['time'] is the same time as the last bar, the last bar will be overwritten.\n + :param series: labels: date/time, open, high, low, close, volume (if using volume). + """ + series = self._series_datetime_format(series) if not _from_tick else series + if series['time'] != self._last_bar['time']: + self.data.loc[self.data.index[-1]] = self._last_bar + self.data = pd.concat([self.data, series.to_frame().T], ignore_index=True) + self._chart.events.new_bar._emit(self) + self._last_bar = series + self.run_script(f'{self.id}.series.update({js_data(series)})') def delete(self): """ Irreversibly deletes the bar series. From 6720a48142bd3ef6591b216edb8813ef45ec7ce7 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Tue, 29 Oct 2024 21:32:09 -0700 Subject: [PATCH 08/89] Added LegendGroup Add functionality to group lines under a single legend item following example shown by @esteban2006 https://github.com/louisnw01/lightweight-charts-python/issues/452#issuecomment-2355647342 --- src/general/legend.ts | 398 +++++++++++++++++++++++++----------------- 1 file changed, 240 insertions(+), 158 deletions(-) diff --git a/src/general/legend.ts b/src/general/legend.ts index 1099728..2be2486 100644 --- a/src/general/legend.ts +++ b/src/general/legend.ts @@ -1,20 +1,63 @@ import { ISeriesApi, LineData, Logical, MouseEventParams, PriceFormatBuiltIn, SeriesType } from "lightweight-charts"; import { Handler } from "./handler"; - +// Interfaces for the legend elements interface LineElement { name: string; div: HTMLDivElement; row: HTMLDivElement; - toggle: HTMLDivElement, - series: ISeriesApi, + toggle: HTMLDivElement; + series: ISeriesApi; solid: string; } +interface LegendGroup { + name: string; + seriesList: ISeriesApi[]; + div: HTMLDivElement; + row: HTMLDivElement; + toggle: HTMLDivElement; + solidColors: string[]; + names: string[]; +} +// Define the SVG path data +const openEye = ` + + + + +`; + +const closedEye = ` + + + + + +`; export class Legend { private handler: Handler; public div: HTMLDivElement; - public seriesContainer: HTMLDivElement + public seriesContainer: HTMLDivElement; private ohlcEnabled: boolean = false; private percentEnabled: boolean = false; @@ -24,132 +67,157 @@ export class Legend { private text: HTMLSpanElement; private candle: HTMLDivElement; public _lines: LineElement[] = []; - + public _groups: LegendGroup[] = []; constructor(handler: Handler) { - this.legendHandler = this.legendHandler.bind(this) - this.handler = handler; - this.ohlcEnabled = false; - this.percentEnabled = false - this.linesEnabled = false - this.colorBasedOnCandle = false - this.div = document.createElement('div'); - this.div.classList.add("legend") - this.div.style.maxWidth = `${(handler.scale.width * 100) - 8}vw` - this.div.style.display = 'none'; + this.div.classList.add("legend"); + this.seriesContainer = document.createElement("div"); + this.text = document.createElement('span'); + this.candle = document.createElement('div'); + this.setupLegend(); + this.legendHandler = this.legendHandler.bind(this); + handler.chart.subscribeCrosshairMove(this.legendHandler); + } + + private setupLegend() { + this.div.style.maxWidth = `${(this.handler.scale.width * 100) - 8}vw`; + this.div.style.display = 'none'; + const seriesWrapper = document.createElement('div'); seriesWrapper.style.display = 'flex'; seriesWrapper.style.flexDirection = 'row'; - this.seriesContainer = document.createElement("div"); + this.seriesContainer.classList.add("series-container"); + this.text.style.lineHeight = '1.8'; - this.text = document.createElement('span') - this.text.style.lineHeight = '1.8' - this.candle = document.createElement('div') - seriesWrapper.appendChild(this.seriesContainer); - this.div.appendChild(this.text) - this.div.appendChild(this.candle) - this.div.appendChild(seriesWrapper) - handler.div.appendChild(this.div) - - // this.makeSeriesRows(handler); + this.div.appendChild(this.text); + this.div.appendChild(this.candle); + this.div.appendChild(seriesWrapper); + this.handler.div.appendChild(this.div); + } - handler.chart.subscribeCrosshairMove(this.legendHandler) + legendItemFormat(num: number, decimal: number) { + return num.toFixed(decimal).toString().padStart(8, ' '); } - toJSON() { - // Exclude the chart attribute from serialization - const {_lines, handler, ...serialized} = this; - return serialized; + shorthandFormat(num: number) { + const absNum = Math.abs(num); + return absNum >= 1000000 ? (num / 1000000).toFixed(1) + 'M' : + absNum >= 1000 ? (num / 1000).toFixed(1) + 'K' : + num.toString().padStart(8, ' '); } - // makeSeriesRows(handler: Handler) { - // if (this.linesEnabled) handler._seriesList.forEach(s => this.makeSeriesRow(s)) - // } - - makeSeriesRow(name: string, series: ISeriesApi) { - const strokeColor = '#FFF'; - let openEye = ` - - \` - ` - let closedEye = ` - - ` - - let row = document.createElement('div') - row.style.display = 'flex' - row.style.alignItems = 'center' - let div = document.createElement('div') - let toggle = document.createElement('div') + makeSeriesRow(name: string, series: ISeriesApi): HTMLDivElement { + const row = document.createElement('div'); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + + const div = document.createElement('div'); + div.innerText = name; + + const toggle = document.createElement('div'); toggle.classList.add('legend-toggle-switch'); - - - let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.setAttribute("width", "22"); - svg.setAttribute("height", "16"); - - let group = document.createElementNS("http://www.w3.org/2000/svg", "g"); - group.innerHTML = openEye - - let on = true + + const color = (series.options() as any).color || 'rgba(255,0,0,1)'; // Use a default color + const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; + + const onIcon = this.createSvgIcon(openEye); + const offIcon = this.createSvgIcon(closedEye); + toggle.appendChild(onIcon.cloneNode(true)); // Clone nodes to avoid duplication + + let visible = true; toggle.addEventListener('click', () => { - if (on) { - on = false - group.innerHTML = closedEye - series.applyOptions({ - visible: false - }) - } else { - on = true - series.applyOptions({ - visible: true - }) - group.innerHTML = openEye - } - }) - - svg.appendChild(group) - toggle.appendChild(svg); - row.appendChild(div) - row.appendChild(toggle) - this.seriesContainer.appendChild(row) - - const color = series.options().color; + visible = !visible; + series.applyOptions({ visible }); + toggle.innerHTML = ''; // Clear current icon + toggle.appendChild(visible ? onIcon.cloneNode(true) : offIcon.cloneNode(true)); + }); + + row.appendChild(div); + row.appendChild(toggle); + this._lines.push({ - name: name, - div: div, - row: row, - toggle: toggle, - series: series, - solid: color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color + name, + div, + row, + toggle, + series, + solid: solidColor, }); - } + this.seriesContainer.appendChild(row); - legendItemFormat(num: number, decimal: number) { return num.toFixed(decimal).toString().padStart(8, ' ') } + return row; + } + makeSeriesGroup(groupName: string, names: string[], seriesList: ISeriesApi[], solidColors: string[]) { + const row = document.createElement('div'); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + + const div = document.createElement('div'); + div.style.color = '#FFF'; // Keep group name text in white + div.innerText = `${groupName}:`; + + const toggle = document.createElement('div'); + toggle.classList.add('legend-toggle-switch'); + + const onIcon = this.createSvgIcon(openEye); + const offIcon = this.createSvgIcon(closedEye); + toggle.appendChild(onIcon.cloneNode(true)); // Default to visible + + let visible = true; + toggle.addEventListener('click', () => { + visible = !visible; + seriesList.forEach(series => series.applyOptions({ visible })); + toggle.innerHTML = ''; // Clear toggle before appending new icon + toggle.appendChild(visible ? onIcon.cloneNode(true) : offIcon.cloneNode(true)); + }); + + // Build the legend text with only colored squares and regular-weight line names + let legendText = `${groupName}:`; + names.forEach((name, index) => { + const color = solidColors[index]; + legendText += ` ${name}: -`; + }); + + div.innerHTML = legendText; // Set HTML content to maintain colored squares and regular font for line names + + this._groups.push({ + name: groupName, + seriesList, + div, + row, + toggle, + solidColors, + names, + }); + + row.appendChild(div); + row.appendChild(toggle); + this.seriesContainer.appendChild(row); + return row; + } + - shorthandFormat(num: number) { - const absNum = Math.abs(num) - if (absNum >= 1000000) { - return (num / 1000000).toFixed(1) + 'M'; - } else if (absNum >= 1000) { - return (num / 1000).toFixed(1) + 'K'; - } - return num.toString().padStart(8, ' '); + private createSvgIcon(svgContent: string): SVGElement { + const tempContainer = document.createElement('div'); + tempContainer.innerHTML = svgContent.trim(); + const svgElement = tempContainer.querySelector('svg'); + return svgElement as SVGElement; } + - legendHandler(param: MouseEventParams, usingPoint= false) { + legendHandler(param: MouseEventParams, usingPoint = false) { if (!this.ohlcEnabled && !this.linesEnabled && !this.percentEnabled) return; - const options: any = this.handler.series.options() + const options: any = this.handler.series.options(); if (!param.time) { - this.candle.style.color = 'transparent' - this.candle.innerHTML = this.candle.innerHTML.replace(options['upColor'], '').replace(options['downColor'], '') - return + this.candle.style.color = 'transparent'; + this.candle.innerHTML = this.candle.innerHTML.replace(options['upColor'], '').replace(options['downColor'], ''); + return; } let data: any; @@ -157,76 +225,90 @@ export class Legend { if (usingPoint) { const timeScale = this.handler.chart.timeScale(); - let coordinate = timeScale.timeToCoordinate(param.time) - if (coordinate) - logical = timeScale.coordinateToLogical(coordinate.valueOf()) - if (logical) - data = this.handler.series.dataByIndex(logical.valueOf()) - } - else { + const coordinate = timeScale.timeToCoordinate(param.time); + if (coordinate) logical = timeScale.coordinateToLogical(coordinate.valueOf()); + if (logical) data = this.handler.series.dataByIndex(logical.valueOf()); + } else { data = param.seriesData.get(this.handler.series); } - this.candle.style.color = '' - let str = '' + let str = ''; if (data) { + // OHLC Data if (this.ohlcEnabled) { - str += `O ${this.legendItemFormat(data.open, this.handler.precision)} ` - str += `| H ${this.legendItemFormat(data.high, this.handler.precision)} ` - str += `| L ${this.legendItemFormat(data.low, this.handler.precision)} ` - str += `| C ${this.legendItemFormat(data.close, this.handler.precision)} ` + str += `O ${this.legendItemFormat(data.open, this.handler.precision)} `; + str += `| H ${this.legendItemFormat(data.high, this.handler.precision)} `; + str += `| L ${this.legendItemFormat(data.low, this.handler.precision)} `; + str += `| C ${this.legendItemFormat(data.close, this.handler.precision)} `; } + // Percentage Movement if (this.percentEnabled) { - let percentMove = ((data.close - data.open) / data.open) * 100 - let color = percentMove > 0 ? options['upColor'] : options['downColor'] - let percentStr = `${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %` - - if (this.colorBasedOnCandle) { - str += `| ${percentStr}` - } else { - str += '| ' + percentStr - } - } - - if (this.handler.volumeSeries) { - let volumeData: any; - if (logical) { - volumeData = this.handler.volumeSeries.dataByIndex(logical) - } - else { - volumeData = param.seriesData.get(this.handler.volumeSeries) - } - if (volumeData) { - str += this.ohlcEnabled ? `
V ${this.shorthandFormat(volumeData.value)}` : '' - } + const percentMove = ((data.close - data.open) / data.open) * 100; + const color = percentMove > 0 ? options['upColor'] : options['downColor']; + const percentStr = `${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %`; + str += this.colorBasedOnCandle ? `| ${percentStr}` : `| ${percentStr}`; } } - this.candle.innerHTML = str + '
' + this.candle.innerHTML = str + '
'; - this._lines.forEach((e) => { - if (!this.linesEnabled) { - e.row.style.display = 'none' - return - } - e.row.style.display = 'flex' + this.updateGroupLegend(param, logical, usingPoint); + this.updateSeriesLegend(param, logical, usingPoint); + } - let data - if (usingPoint && logical) { - data = e.series.dataByIndex(logical) as LineData + private updateGroupLegend(param: MouseEventParams, logical: Logical | null, usingPoint: boolean) { + this._groups.forEach((group) => { + if (!this.linesEnabled) { + group.row.style.display = 'none'; + return; } - else { - data = param.seriesData.get(e.series) as LineData + group.row.style.display = 'flex'; + + let legendText = `${group.name}:`; + group.seriesList.forEach((series, index) => { + const data = usingPoint && logical + ? series.dataByIndex(logical) as LineData + : param.seriesData.get(series) as LineData; + + if (!data?.value) return; + + const priceFormat = series.options().priceFormat; + const price = 'precision' in priceFormat + ? this.legendItemFormat(data.value, (priceFormat as PriceFormatBuiltIn).precision) + : this.legendItemFormat(data.value, 2); // Default precision + + const color = group.solidColors ? group.solidColors[index] : 'inherit'; + const name = group.names[index]; + + // Include `price` in legendText + legendText += ` ${name}: ${price}`; + }); + + group.div.innerHTML = legendText; + }); + } + private updateSeriesLegend(param: MouseEventParams, logical: Logical | null, usingPoint: boolean) { + this._lines.forEach((line) => { + if (!this.linesEnabled) { + line.row.style.display = 'none'; + return; } - if (!data?.value) return; - let price; - if (e.series.seriesType() == 'Histogram') { - price = this.shorthandFormat(data.value) + line.row.style.display = 'flex'; + + const data = usingPoint && logical + ? line.series.dataByIndex(logical) as LineData + : param.seriesData.get(line.series) as LineData; + + if (data?.value !== undefined) { + const priceFormat = line.series.options().priceFormat as PriceFormatBuiltIn; + const price = 'precision' in priceFormat + ? this.legendItemFormat(data.value, priceFormat.precision) + : this.legendItemFormat(data.value, 2); + + line.div.innerHTML = ` ${line.name}: ${price}`; } else { - const format = e.series.options().priceFormat as PriceFormatBuiltIn - price = this.legendItemFormat(data.value, format.precision) // couldn't this just be line.options().precision? + line.div.innerHTML = `${line.name}: -`; } - e.div.innerHTML = ` ${e.name} : ${price}` - }) + }); } } From 3ec9692526fcf59d32133bee9f8c0e4b2fc8938c Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Tue, 29 Oct 2024 21:39:22 -0700 Subject: [PATCH 09/89] handle LegendGroups Implement logic to handle LegendGroup creation when creating a line series. --- src/general/handler.ts | 42 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/general/handler.ts b/src/general/handler.ts index adca7ab..8ed085e 100644 --- a/src/general/handler.ts +++ b/src/general/handler.ts @@ -29,7 +29,9 @@ export interface Scale{ height: number, } - +interface MultiLineOptions extends DeepPartial { + group?: string; // Define group as an optional string identifier +} globalParamInit(); declare const window: GlobalParams; @@ -179,16 +181,40 @@ export class Handler { return volumeSeries; } - createLineSeries(name: string, options: DeepPartial) { - const line = this.chart.addLineSeries({...options}); + createLineSeries( + name: string, + options: MultiLineOptions + ): { name: string; series: ISeriesApi } { + const { group, ...lineOptions } = options; + const line = this.chart.addLineSeries(lineOptions); this._seriesList.push(line); - this.legend.makeSeriesRow(name, line) - return { - name: name, - series: line, + + // Get color of the series for legend display + const color = line.options().color || 'rgba(255,0,0,1)'; // Default to red if no color is defined + const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; + + if (!group || group === '') { + // No group: create a standalone series row + this.legend.makeSeriesRow(name, line); + } else { + // Check if the group already exists + const existingGroup = this.legend._groups.find(g => g.name === group); + + if (existingGroup) { + // Group exists: add the new line's name and color to the `names` and `solidColors` arrays + existingGroup.names.push(name); + existingGroup.seriesList.push(line); + existingGroup.solidColors.push(solidColor); + } else { + // Group does not exist: create a new one + this.legend.makeSeriesGroup(group, [name], [line], [solidColor]); + } } + + return { name, series: line }; } - + + createHistogramSeries(name: string, options: DeepPartial) { const line = this.chart.addHistogramSeries({...options}); this._seriesList.push(line); From d57734eef56afbc1812997c2e06ee8c1425ece97 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Tue, 29 Oct 2024 21:48:00 -0700 Subject: [PATCH 10/89] Add group parameter to Line, .create_line Add group parameter (str) to make a line appear in the legend under a LegendGroup --- lightweight_charts/abstract.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 31a0e71..a6b3f70 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -423,16 +423,19 @@ def vertical_span( return VerticalSpan(self, start_time, end_time, color) -class Line(SeriesCommon): - def __init__(self, chart, name, color, style, width, price_line, price_label, price_scale_id=None, crosshair_marker=True): +class Line(SeriesCommon): + def __init__(self, chart, name, color, style, width, price_line, price_label, group, price_scale_id=None, crosshair_marker=True): super().__init__(chart, name) self.color = color + self.group = group # Store group for potential internal use + # Pass group as part of the options if createLineSeries handles removing it self.run_script(f''' {self.id} = {self._chart.id}.createLineSeries( "{name}", {{ + group: '{group}', color: '{color}', lineStyle: {as_enum(style, LINE_STYLE)}, lineWidth: {width}, @@ -832,17 +835,19 @@ def fit(self): """ self.run_script(f'{self.id}.chart.timeScale().fitContent()') - def create_line( + def create_line( self, name: str = '', color: str = 'rgba(214, 237, 255, 0.6)', style: LINE_STYLE = 'solid', width: int = 2, - price_line: bool = True, price_label: bool = True, price_scale_id: Optional[str] = None + price_line: bool = True, price_label: bool = True, group: str = '', + price_scale_id: Optional[str] =None ) -> Line: """ Creates and returns a Line object. """ - self._lines.append(Line(self, name, color, style, width, price_line, price_label, price_scale_id)) + self._lines.append(Line(self, name, color, style, width, price_line, price_label, group, price_scale_id )) return self._lines[-1] + def create_histogram( self, name: str = '', color: str = 'rgba(214, 237, 255, 0.6)', price_line: bool = True, price_label: bool = True, @@ -854,6 +859,7 @@ def create_histogram( return Histogram( self, name, color, price_line, price_label, scale_margin_top, scale_margin_bottom) + def create_area( self, name: str = '', top_color: str ='rgba(0, 100, 0, 0.5)', @@ -868,6 +874,8 @@ def create_area( width, price_line, price_label, price_scale_id)) return self._lines[-1] + + def create_bar( self, name: str = '', up_color: str = '#26a69a', down_color: str = '#ef5350', open_visible: bool = True, thin_bars: bool = True, From 288c2224111e2b4d44e5bfdd45ce640fd26ea8a1 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:43:17 -0700 Subject: [PATCH 11/89] Update bundle.js --- lightweight_charts/js/bundle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightweight_charts/js/bundle.js b/lightweight_charts/js/bundle.js index 6d6c9fe..730a0c7 100644 --- a/lightweight_charts/js/bundle.js +++ b/lightweight_charts/js/bundle.js @@ -1 +1 @@ -var Lib=function(t,e){"use strict";const i={backgroundColor:"#0c0d0f",hoverBackgroundColor:"#3c434c",clickBackgroundColor:"#50565E",activeBackgroundColor:"rgba(0, 122, 255, 0.7)",mutedBackgroundColor:"rgba(0, 122, 255, 0.3)",borderColor:"#3C434C",color:"#d8d9db",activeColor:"#ececed"};function s(){window.pane={...i},window.containerDiv=document.getElementById("container")||document.createElement("div"),window.setCursor=t=>{t&&(window.cursor=t),document.body.style.cursor=window.cursor},window.cursor="default",window.textBoxFocused=!1}class o{handler;div;seriesContainer;ohlcEnabled=!1;percentEnabled=!1;linesEnabled=!1;colorBasedOnCandle=!1;text;candle;_lines=[];constructor(t){this.legendHandler=this.legendHandler.bind(this),this.handler=t,this.ohlcEnabled=!1,this.percentEnabled=!1,this.linesEnabled=!1,this.colorBasedOnCandle=!1,this.div=document.createElement("div"),this.div.classList.add("legend"),this.div.style.maxWidth=100*t.scale.width-8+"vw",this.div.style.display="none";const e=document.createElement("div");e.style.display="flex",e.style.flexDirection="row",this.seriesContainer=document.createElement("div"),this.seriesContainer.classList.add("series-container"),this.text=document.createElement("span"),this.text.style.lineHeight="1.8",this.candle=document.createElement("div"),e.appendChild(this.seriesContainer),this.div.appendChild(this.text),this.div.appendChild(this.candle),this.div.appendChild(e),t.div.appendChild(this.div),t.chart.subscribeCrosshairMove(this.legendHandler)}toJSON(){const{_lines:t,handler:e,...i}=this;return i}makeSeriesRow(t,e){const i="#FFF";let s=`\n \n \`\n `,o=`\n \n `,n=document.createElement("div");n.style.display="flex",n.style.alignItems="center";let r=document.createElement("div"),a=document.createElement("div");a.classList.add("legend-toggle-switch");let l=document.createElementNS("http://www.w3.org/2000/svg","svg");l.setAttribute("width","22"),l.setAttribute("height","16");let h=document.createElementNS("http://www.w3.org/2000/svg","g");h.innerHTML=s;let d=!0;a.addEventListener("click",(()=>{d?(d=!1,h.innerHTML=o,e.applyOptions({visible:!1})):(d=!0,e.applyOptions({visible:!0}),h.innerHTML=s)})),l.appendChild(h),a.appendChild(l),n.appendChild(r),n.appendChild(a),this.seriesContainer.appendChild(n);const c=e.options().color;this._lines.push({name:t,div:r,row:n,toggle:a,series:e,solid:c.startsWith("rgba")?c.replace(/[^,]+(?=\))/,"1"):c})}legendItemFormat(t,e){return t.toFixed(e).toString().padStart(8," ")}shorthandFormat(t){const e=Math.abs(t);return e>=1e6?(t/1e6).toFixed(1)+"M":e>=1e3?(t/1e3).toFixed(1)+"K":t.toString().padStart(8," ")}legendHandler(t,e=!1){if(!this.ohlcEnabled&&!this.linesEnabled&&!this.percentEnabled)return;const i=this.handler.series.options();if(!t.time)return this.candle.style.color="transparent",void(this.candle.innerHTML=this.candle.innerHTML.replace(i.upColor,"").replace(i.downColor,""));let s,o=null;if(e){const e=this.handler.chart.timeScale();let i=e.timeToCoordinate(t.time);i&&(o=e.coordinateToLogical(i.valueOf())),o&&(s=this.handler.series.dataByIndex(o.valueOf()))}else s=t.seriesData.get(this.handler.series);this.candle.style.color="";let n='';if(s){if(this.ohlcEnabled&&(n+=`O ${this.legendItemFormat(s.open,this.handler.precision)} `,n+=`| H ${this.legendItemFormat(s.high,this.handler.precision)} `,n+=`| L ${this.legendItemFormat(s.low,this.handler.precision)} `,n+=`| C ${this.legendItemFormat(s.close,this.handler.precision)} `),this.percentEnabled){let t=(s.close-s.open)/s.open*100,e=t>0?i.upColor:i.downColor,o=`${t>=0?"+":""}${t.toFixed(2)} %`;this.colorBasedOnCandle?n+=`| ${o}`:n+="| "+o}if(this.handler.volumeSeries){let e;e=o?this.handler.volumeSeries.dataByIndex(o):t.seriesData.get(this.handler.volumeSeries),e&&(n+=this.ohlcEnabled?`
V ${this.shorthandFormat(e.value)}`:"")}}this.candle.innerHTML=n+"
",this._lines.forEach((i=>{if(!this.linesEnabled)return void(i.row.style.display="none");let s,n;if(i.row.style.display="flex",s=e&&o?i.series.dataByIndex(o):t.seriesData.get(i.series),s?.value){if("Histogram"==i.series.seriesType())n=this.shorthandFormat(s.value);else{const t=i.series.options().priceFormat;n=this.legendItemFormat(s.value,t.precision)}i.div.innerHTML=` ${i.name} : ${n}`}}))}}function n(t){if(void 0===t)throw new Error("Value is undefined");return t}class r{_chart=void 0;_series=void 0;requestUpdate(){this._requestUpdate&&this._requestUpdate()}_requestUpdate;attached({chart:t,series:e,requestUpdate:i}){this._chart=t,this._series=e,this._series.subscribeDataChanged(this._fireDataUpdated),this._requestUpdate=i,this.requestUpdate()}detached(){this._chart=void 0,this._series=void 0,this._requestUpdate=void 0}get chart(){return n(this._chart)}get series(){return n(this._series)}_fireDataUpdated(t){this.dataUpdated&&this.dataUpdated(t)}}const a={lineColor:"#1E80F0",lineStyle:e.LineStyle.Solid,width:4};var l;!function(t){t[t.NONE=0]="NONE",t[t.HOVERING=1]="HOVERING",t[t.DRAGGING=2]="DRAGGING",t[t.DRAGGINGP1=3]="DRAGGINGP1",t[t.DRAGGINGP2=4]="DRAGGINGP2",t[t.DRAGGINGP3=5]="DRAGGINGP3",t[t.DRAGGINGP4=6]="DRAGGINGP4"}(l||(l={}));class h extends r{_paneViews=[];_options;_points=[];_state=l.NONE;_startDragPoint=null;_latestHoverPoint=null;static _mouseIsDown=!1;static hoveredObject=null;static lastHoveredObject=null;_listeners=[];constructor(t){super(),this._options={...a,...t}}updateAllViews(){this._paneViews.forEach((t=>t.update()))}paneViews(){return this._paneViews}applyOptions(t){this._options={...this._options,...t},this.requestUpdate()}updatePoints(...t){for(let e=0;ei.name===t&&i.listener===e));this._listeners.splice(this._listeners.indexOf(i),1)}_handleHoverInteraction(t){if(this._latestHoverPoint=t.point,h._mouseIsDown)this._handleDragInteraction(t);else if(this._mouseIsOverDrawing(t)){if(this._state!=l.NONE)return;this._moveToState(l.HOVERING),h.hoveredObject=h.lastHoveredObject=this}else{if(this._state==l.NONE)return;this._moveToState(l.NONE),h.hoveredObject===this&&(h.hoveredObject=null)}}static _eventToPoint(t,e){if(!e||!t.point||!t.logical)return null;const i=e.coordinateToPrice(t.point.y);return null==i?null:{time:t.time||null,logical:t.logical,price:i.valueOf()}}static _getDiff(t,e){return{logical:t.logical-e.logical,price:t.price-e.price}}_addDiffToPoint(t,e,i){t&&(t.logical=t.logical+e,t.price=t.price+i,t.time=this.series.dataByIndex(t.logical)?.time||null)}_handleMouseDownInteraction=()=>{h._mouseIsDown=!0,this._onMouseDown()};_handleMouseUpInteraction=()=>{h._mouseIsDown=!1,this._moveToState(l.HOVERING)};_handleDragInteraction(t){if(this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP1&&this._state!=l.DRAGGINGP2&&this._state!=l.DRAGGINGP3&&this._state!=l.DRAGGINGP4)return;const e=h._eventToPoint(t,this.series);if(!e)return;this._startDragPoint=this._startDragPoint||e;const i=h._getDiff(e,this._startDragPoint);this._onDrag(i),this.requestUpdate(),this._startDragPoint=e}}class d{_options;constructor(t){this._options=t}}class c extends d{_p1;_p2;_hovered;constructor(t,e,i,s){super(i),this._p1=t,this._p2=e,this._hovered=s}_getScaledCoordinates(t){return null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y?null:{x1:Math.round(this._p1.x*t.horizontalPixelRatio),y1:Math.round(this._p1.y*t.verticalPixelRatio),x2:Math.round(this._p2.x*t.horizontalPixelRatio),y2:Math.round(this._p2.y*t.verticalPixelRatio)}}_drawEndCircle(t,e,i){t.context.fillStyle="#000",t.context.beginPath(),t.context.arc(e,i,9,0,2*Math.PI),t.context.stroke(),t.context.fill()}}function p(t,i){const s={[e.LineStyle.Solid]:[],[e.LineStyle.Dotted]:[t.lineWidth,t.lineWidth],[e.LineStyle.Dashed]:[2*t.lineWidth,2*t.lineWidth],[e.LineStyle.LargeDashed]:[6*t.lineWidth,6*t.lineWidth],[e.LineStyle.SparseDotted]:[t.lineWidth,4*t.lineWidth]}[i];t.setLineDash(s)}class u extends d{_point={x:null,y:null};constructor(t,e){super(e),this._point=t}draw(t){t.useBitmapCoordinateSpace((t=>{if(null==this._point.y)return;const e=t.context,i=Math.round(this._point.y*t.verticalPixelRatio),s=this._point.x?this._point.x*t.horizontalPixelRatio:0;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.beginPath(),e.moveTo(s,i),e.lineTo(t.bitmapSize.width,i),e.stroke()}))}}class _{_source;constructor(t){this._source=t}}class m extends _{_p1={x:null,y:null};_p2={x:null,y:null};_source;constructor(t){super(t),this._source=t}update(){if(!this._source.p1||!this._source.p2)return;const t=this._source.series,e=t.priceToCoordinate(this._source.p1.price),i=t.priceToCoordinate(this._source.p2.price),s=this._getX(this._source.p1),o=this._getX(this._source.p2);this._p1={x:s,y:e},this._p2={x:o,y:i}}_getX(t){return this._source.chart.timeScale().logicalToCoordinate(t.logical)}}class v extends _{_source;_point={x:null,y:null};constructor(t){super(t),this._source=t}update(){const t=this._source._point,e=this._source.chart.timeScale(),i=this._source.series;"RayLine"==this._source._type&&(this._point.x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical)),this._point.y=i.priceToCoordinate(t.price)}renderer(){return new u(this._point,this._source._options)}}class g{_source;_y=null;_price=null;constructor(t){this._source=t}update(){if(!this._source.series||!this._source._point)return;this._y=this._source.series.priceToCoordinate(this._source._point.price);const t=this._source.series.options().priceFormat.precision;this._price=this._source._point.price.toFixed(t).toString()}visible(){return!0}tickVisible(){return!0}coordinate(){return this._y??0}text(){return this._source._options.text||this._price||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class w extends h{_type="HorizontalLine";_paneViews;_point;_callbackName;_priceAxisViews;_startDragPoint=null;constructor(t,e,i=null){super(e),this._point=t,this._point.time=null,this._paneViews=[new v(this)],this._priceAxisViews=[new g(this)],this._callbackName=i}get points(){return[this._point]}updatePoints(...t){for(const e of t)e&&(this._point.price=e.price);this.requestUpdate()}updateAllViews(){this._paneViews.forEach((t=>t.update())),this._priceAxisViews.forEach((t=>t.update()))}priceAxisViews(){return this._priceAxisViews}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._addDiffToPoint(this._point,0,t.price),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.series.priceToCoordinate(this._point.price);return!!i&&Math.abs(i-t.point.y){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class y{_chart;_series;_finishDrawingCallback=null;_drawings=[];_activeDrawing=null;_isDrawing=!1;_drawingType=null;constructor(t,e,i=null){this._chart=t,this._series=e,this._finishDrawingCallback=i,this._chart.subscribeClick(this._clickHandler),this._chart.subscribeCrosshairMove(this._moveHandler)}_clickHandler=t=>this._onClick(t);_moveHandler=t=>this._onMouseMove(t);beginDrawing(t){this._drawingType=t,this._isDrawing=!0}stopDrawing(){this._isDrawing=!1,this._activeDrawing=null}get drawings(){return this._drawings}addNewDrawing(t){this._series.attachPrimitive(t),this._drawings.push(t)}delete(t){if(null==t)return;const e=this._drawings.indexOf(t);-1!=e&&(this._drawings.splice(e,1),t.detach())}clearDrawings(){for(const t of this._drawings)t.detach();this._drawings=[]}repositionOnTime(){for(const t of this.drawings){const e=[];for(const i of t.points){if(!i){e.push(i);continue}const t=i.time?this._chart.timeScale().coordinateToLogical(this._chart.timeScale().timeToCoordinate(i.time)||0):i.logical;e.push({time:i.time,logical:t,price:i.price})}t.updatePoints(...e)}}_onClick(t){if(!this._isDrawing)return;const e=h._eventToPoint(t,this._series);if(e)if(null==this._activeDrawing){if(null==this._drawingType)return;this._activeDrawing=new this._drawingType(e,e),this._series.attachPrimitive(this._activeDrawing),this._drawingType==w&&this._onClick(t)}else{if(this._drawings.push(this._activeDrawing),this.stopDrawing(),!this._finishDrawingCallback)return;this._finishDrawingCallback()}}_onMouseMove(t){if(!t)return;for(const e of this._drawings)e._handleHoverInteraction(t);if(!this._isDrawing||!this._activeDrawing)return;const e=h._eventToPoint(t,this._series);e&&this._activeDrawing.updatePoints(null,e)}}class b extends c{constructor(t,e,i,s){super(t,e,i,s)}draw(t){t.useBitmapCoordinateSpace((t=>{if(null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y)return;const e=t.context,i=this._getScaledCoordinates(t);i&&(e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.beginPath(),e.moveTo(i.x1,i.y1),e.lineTo(i.x2,i.y2),e.stroke(),this._hovered&&(this._drawEndCircle(t,i.x1,i.y1),this._drawEndCircle(t,i.x2,i.y2)))}))}}class x extends m{constructor(t){super(t)}renderer(){return new b(this._p1,this._p2,this._source._options,this._source.hovered)}}class C extends h{_paneViews=[];_hovered=!1;constructor(t,e,i){super(),this.points.push(t),this.points.push(e),this._options={...a,...i}}setFirstPoint(t){this.updatePoints(t)}setSecondPoint(t){this.updatePoints(null,t)}get p1(){return this.points[0]}get p2(){return this.points[1]}get hovered(){return this._hovered}}class f extends C{_type="TrendLine";constructor(t,e,i){super(t,e,i),this._paneViews=[new x(this)]}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._hovered=!1,this.requestUpdate(),this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this.requestUpdate(),this._subscribe("mousedown",this._handleMouseDownInteraction),this._unsubscribe("mouseup",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGINGP1:case l.DRAGGINGP2:case l.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP1||this._addDiffToPoint(this.p1,t.logical,t.price),this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP2||this._addDiffToPoint(this.p2,t.logical,t.price)}_onMouseDown(){this._startDragPoint=null;const t=this._latestHoverPoint;if(!t)return;const e=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(e.x&&i.x&&e.y&&i.y))return this._moveToState(l.DRAGGING);Math.abs(t.x-e.x)<10&&Math.abs(t.y-e.y)<10?this._moveToState(l.DRAGGINGP1):Math.abs(t.x-i.x)<10&&Math.abs(t.y-i.y)<10?this._moveToState(l.DRAGGINGP2):this._moveToState(l.DRAGGING)}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this._paneViews[0]._p1.x,s=this._paneViews[0]._p1.y,o=this._paneViews[0]._p2.x,n=this._paneViews[0]._p2.y;if(!(i&&o&&s&&n))return!1;const r=t.point.x,a=t.point.y;if(r<=Math.min(i,o)-e||r>=Math.max(i,o)+e)return!1;return Math.abs((n-s)*r-(o-i)*a+o*s-n*i)/Math.sqrt((n-s)**2+(o-i)**2)<=e}}class D extends c{constructor(t,e,i,s){super(t,e,i,s)}draw(t){t.useBitmapCoordinateSpace((t=>{const e=t.context,i=this._getScaledCoordinates(t);if(!i)return;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.fillStyle=this._options.fillColor;const s=Math.min(i.x1,i.x2),o=Math.min(i.y1,i.y2),n=Math.abs(i.x1-i.x2),r=Math.abs(i.y1-i.y2);e.strokeRect(s,o,n,r),e.fillRect(s,o,n,r),this._hovered&&(this._drawEndCircle(t,s,o),this._drawEndCircle(t,s+n,o),this._drawEndCircle(t,s+n,o+r),this._drawEndCircle(t,s,o+r))}))}}class k extends m{constructor(t){super(t)}renderer(){return new D(this._p1,this._p2,this._source._options,this._source.hovered)}}const E={fillEnabled:!0,fillColor:"rgba(255, 255, 255, 0.2)",...a};class L extends C{_type="Box";constructor(t,e,i){super(t,e,i),this._options={...E,...i},this._paneViews=[new k(this)]}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._hovered=!1,this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this._unsubscribe("mouseup",this._handleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGINGP1:case l.DRAGGINGP2:case l.DRAGGINGP3:case l.DRAGGINGP4:case l.DRAGGING:document.body.style.cursor="grabbing",document.body.addEventListener("mouseup",this._handleMouseUpInteraction),this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP1||this._addDiffToPoint(this.p1,t.logical,t.price),this._state!=l.DRAGGING&&this._state!=l.DRAGGINGP2||this._addDiffToPoint(this.p2,t.logical,t.price),this._state!=l.DRAGGING&&(this._state==l.DRAGGINGP3&&(this._addDiffToPoint(this.p1,t.logical,0),this._addDiffToPoint(this.p2,0,t.price)),this._state==l.DRAGGINGP4&&(this._addDiffToPoint(this.p1,0,t.price),this._addDiffToPoint(this.p2,t.logical,0)))}_onMouseDown(){this._startDragPoint=null;const t=this._latestHoverPoint,e=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(e.x&&i.x&&e.y&&i.y))return this._moveToState(l.DRAGGING);const s=10;Math.abs(t.x-e.x)l-p&&rh-p&&ai.appendChild(this.makeColorBox(t))));let s=document.createElement("div");s.style.backgroundColor=window.pane.borderColor,s.style.height="1px",s.style.width="130px";let o=document.createElement("div");o.style.margin="10px";let n=document.createElement("div");n.style.color="lightgray",n.style.fontSize="12px",n.innerText="Opacity",this._opacityLabel=document.createElement("div"),this._opacityLabel.style.color="lightgray",this._opacityLabel.style.fontSize="12px",this._opacitySlider=document.createElement("input"),this._opacitySlider.type="range",this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%",this._opacitySlider.oninput=()=>{this._opacityLabel.innerText=this._opacitySlider.value+"%",this.opacity=parseInt(this._opacitySlider.value)/100,this.updateColor()},o.appendChild(n),o.appendChild(this._opacitySlider),o.appendChild(this._opacityLabel),this._div.appendChild(i),this._div.appendChild(s),this._div.appendChild(o),window.containerDiv.appendChild(this._div)}_updateOpacitySlider(){this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%"}makeColorBox(t){const e=document.createElement("div");e.style.width="18px",e.style.height="18px",e.style.borderRadius="3px",e.style.margin="3px",e.style.boxSizing="border-box",e.style.backgroundColor=t,e.addEventListener("mouseover",(()=>e.style.border="2px solid lightgray")),e.addEventListener("mouseout",(()=>e.style.border="none"));const i=S.extractRGBA(t);return e.addEventListener("click",(()=>{this.rgba=i,this.updateColor()})),e}static extractRGBA(t){const e=document.createElement("div");e.style.color=t,document.body.appendChild(e);const i=getComputedStyle(e).color;document.body.removeChild(e);const s=i.match(/\d+/g)?.map(Number);if(!s)return[];let o=i.includes("rgba")?parseFloat(i.split(",")[3]):1;return[s[0],s[1],s[2],o]}updateColor(){if(!h.lastHoveredObject||!this.rgba)return;const t=`rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`;h.lastHoveredObject.applyOptions({[this.colorOption]:t}),this.saveDrawings()}openMenu(t){h.lastHoveredObject&&(this.rgba=S.extractRGBA(h.lastHoveredObject._options[this.colorOption]),this.opacity=this.rgba[3],this._updateOpacitySlider(),this._div.style.top=t.top-30+"px",this._div.style.left=t.right+"px",this._div.style.display="flex",setTimeout((()=>document.addEventListener("mousedown",(t=>{this._div.contains(t.target)||this.closeMenu()}))),10))}closeMenu(){document.body.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}class G{static _styles=[{name:"Solid",var:e.LineStyle.Solid},{name:"Dotted",var:e.LineStyle.Dotted},{name:"Dashed",var:e.LineStyle.Dashed},{name:"Large Dashed",var:e.LineStyle.LargeDashed},{name:"Sparse Dotted",var:e.LineStyle.SparseDotted}];_div;_saveDrawings;constructor(t){this._saveDrawings=t,this._div=document.createElement("div"),this._div.classList.add("context-menu"),G._styles.forEach((t=>{this._div.appendChild(this._makeTextBox(t.name,t.var))})),window.containerDiv.appendChild(this._div)}_makeTextBox(t,e){const i=document.createElement("span");return i.classList.add("context-menu-item"),i.innerText=t,i.addEventListener("click",(()=>{h.lastHoveredObject?.applyOptions({lineStyle:e}),this._saveDrawings()})),i}openMenu(t){this._div.style.top=t.top-30+"px",this._div.style.left=t.right+"px",this._div.style.display="block",setTimeout((()=>document.addEventListener("mousedown",(t=>{this._div.contains(t.target)||this.closeMenu()}))),10)}closeMenu(){document.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}function T(t){const e=[];for(const i of t)0==e.length?e.push(i.toUpperCase()):i==i.toUpperCase()?e.push(" "+i):e.push(i);return e.join("")}class I{saveDrawings;drawingTool;div;hoverItem;items=[];constructor(t,e){this.saveDrawings=t,this.drawingTool=e,this._onRightClick=this._onRightClick.bind(this),this.div=document.createElement("div"),this.div.classList.add("context-menu"),document.body.appendChild(this.div),this.hoverItem=null,document.body.addEventListener("contextmenu",this._onRightClick)}_handleClick=t=>this._onClick(t);_onClick(t){t.target&&(this.div.contains(t.target)||(this.div.style.display="none",document.body.removeEventListener("click",this._handleClick)))}_onRightClick(t){if(!h.hoveredObject)return;for(const t of this.items)this.div.removeChild(t);this.items=[];for(const t of Object.keys(h.hoveredObject._options)){let e;if(t.toLowerCase().includes("color"))e=new S(this.saveDrawings,t);else{if("lineStyle"!==t)continue;e=new G(this.saveDrawings)}let i=t=>e.openMenu(t);this.menuItem(T(t),i,(()=>{document.removeEventListener("click",e.closeMenu),e._div.style.display="none"}))}this.separator(),this.menuItem("Delete Drawing",(()=>this.drawingTool.delete(h.lastHoveredObject))),t.preventDefault(),this.div.style.left=t.clientX+"px",this.div.style.top=t.clientY+"px",this.div.style.display="block",document.body.addEventListener("click",this._handleClick)}menuItem(t,e,i=null){const s=document.createElement("span");s.classList.add("context-menu-item"),this.div.appendChild(s);const o=document.createElement("span");if(o.innerText=t,o.style.pointerEvents="none",s.appendChild(o),i){let t=document.createElement("span");t.innerText="►",t.style.fontSize="8px",t.style.pointerEvents="none",s.appendChild(t)}if(s.addEventListener("mouseover",(()=>{this.hoverItem&&this.hoverItem.closeAction&&this.hoverItem.closeAction(),this.hoverItem={elem:o,action:e,closeAction:i}})),i){let t;s.addEventListener("mouseover",(()=>t=setTimeout((()=>e(s.getBoundingClientRect())),100))),s.addEventListener("mouseout",(()=>clearTimeout(t)))}else s.addEventListener("click",(t=>{e(t),this.div.style.display="none"}));this.items.push(s)}separator(){const t=document.createElement("div");t.style.width="90%",t.style.height="1px",t.style.margin="3px 0px",t.style.backgroundColor=window.pane.borderColor,this.div.appendChild(t),this.items.push(t)}}class M extends w{_type="RayLine";constructor(t,e){super({...t},e),this._point.time=t.time}updatePoints(...t){for(const e of t)e&&(this._point=e);this.requestUpdate()}_onDrag(t){this._addDiffToPoint(this._point,t.logical,t.price),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.series.priceToCoordinate(this._point.price),s=this._point.time?this.chart.timeScale().timeToCoordinate(this._point.time):null;return!(!i||!s)&&(Math.abs(i-t.point.y)s-e)}}class N extends d{_point={x:null,y:null};constructor(t,e){super(e),this._point=t}draw(t){t.useBitmapCoordinateSpace((t=>{if(null==this._point.x)return;const e=t.context,i=this._point.x*t.horizontalPixelRatio;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,p(e,this._options.lineStyle),e.beginPath(),e.moveTo(i,0),e.lineTo(i,t.bitmapSize.height),e.stroke()}))}}class R extends _{_source;_point={x:null,y:null};constructor(t){super(t),this._source=t}update(){const t=this._source._point,e=this._source.chart.timeScale(),i=this._source.series;this._point.x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical),this._point.y=i.priceToCoordinate(t.price)}renderer(){return new N(this._point,this._source._options)}}class A{_source;_x=null;constructor(t){this._source=t}update(){if(!this._source.chart||!this._source._point)return;const t=this._source._point,e=this._source.chart.timeScale();this._x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical)}visible(){return!!this._source._options.text}tickVisible(){return!0}coordinate(){return this._x??0}text(){return this._source._options.text||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class B extends h{_type="VerticalLine";_paneViews;_timeAxisViews;_point;_callbackName;_startDragPoint=null;constructor(t,e,i=null){super(e),this._point=t,this._paneViews=[new R(this)],this._callbackName=i,this._timeAxisViews=[new A(this)]}updateAllViews(){this._paneViews.forEach((t=>t.update())),this._timeAxisViews.forEach((t=>t.update()))}timeAxisViews(){return this._timeAxisViews}updatePoints(...t){for(const e of t)e&&(!e.time&&e.logical&&(e.time=this.series.dataByIndex(e.logical)?.time||null),this._point=e);this.requestUpdate()}get points(){return[this._point]}_moveToState(t){switch(t){case l.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case l.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case l.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._addDiffToPoint(this._point,t.logical,0),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.chart.timeScale();let s;return s=this._point.time?i.timeToCoordinate(this._point.time):i.logicalToCoordinate(this._point.logical),!!s&&Math.abs(s-t.point.x){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class P{static TREND_SVG='';static HORZ_SVG='';static RAY_SVG='';static BOX_SVG='';static VERT_SVG=P.RAY_SVG;div;activeIcon=null;buttons=[];_commandFunctions;_handlerID;_drawingTool;constructor(t,e,i,s){this._handlerID=t,this._commandFunctions=s,this._drawingTool=new y(e,i,(()=>this.removeActiveAndSave())),this.div=this._makeToolBox(),new I(this.saveDrawings,this._drawingTool),s.push((t=>{if((t.metaKey||t.ctrlKey)&&"KeyZ"===t.code){const t=this._drawingTool.drawings.pop();return t&&this._drawingTool.delete(t),!0}return!1}))}toJSON(){const{...t}=this;return t}_makeToolBox(){let t=document.createElement("div");t.classList.add("toolbox"),this.buttons.push(this._makeToolBoxElement(f,"KeyT",P.TREND_SVG)),this.buttons.push(this._makeToolBoxElement(w,"KeyH",P.HORZ_SVG)),this.buttons.push(this._makeToolBoxElement(M,"KeyR",P.RAY_SVG)),this.buttons.push(this._makeToolBoxElement(L,"KeyB",P.BOX_SVG)),this.buttons.push(this._makeToolBoxElement(B,"KeyV",P.VERT_SVG,!0));for(const e of this.buttons)t.appendChild(e);return t}_makeToolBoxElement(t,e,i,s=!1){const o=document.createElement("div");o.classList.add("toolbox-button");const n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("width","29"),n.setAttribute("height","29");const r=document.createElementNS("http://www.w3.org/2000/svg","g");r.innerHTML=i,r.setAttribute("fill",window.pane.color),n.appendChild(r),o.appendChild(n);const a={div:o,group:r,type:t};return o.addEventListener("click",(()=>this._onIconClick(a))),this._commandFunctions.push((t=>this._handlerID===window.handlerInFocus&&(!(!t.altKey||t.code!==e)&&(t.preventDefault(),this._onIconClick(a),!0)))),1==s&&(n.style.transform="rotate(90deg)",n.style.transformBox="fill-box",n.style.transformOrigin="center"),o}_onIconClick(t){this.activeIcon&&(this.activeIcon.div.classList.remove("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.stopDrawing(),this.activeIcon===t)?this.activeIcon=null:(this.activeIcon=t,this.activeIcon.div.classList.add("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.beginDrawing(this.activeIcon.type))}removeActiveAndSave=()=>{window.setCursor("default"),this.activeIcon&&this.activeIcon.div.classList.remove("active-toolbox-button"),this.activeIcon=null,this.saveDrawings()};addNewDrawing(t){this._drawingTool.addNewDrawing(t)}clearDrawings(){this._drawingTool.clearDrawings()}saveDrawings=()=>{const t=[];for(const e of this._drawingTool.drawings)t.push({type:e._type,points:e.points,options:e._options});const e=JSON.stringify(t);window.callbackFunction(`save_drawings${this._handlerID}_~_${e}`)};loadDrawings(t){t.forEach((t=>{switch(t.type){case"Box":this._drawingTool.addNewDrawing(new L(t.points[0],t.points[1],t.options));break;case"TrendLine":this._drawingTool.addNewDrawing(new f(t.points[0],t.points[1],t.options));break;case"HorizontalLine":this._drawingTool.addNewDrawing(new w(t.points[0],t.options));break;case"RayLine":this._drawingTool.addNewDrawing(new M(t.points[0],t.options));break;case"VerticalLine":this._drawingTool.addNewDrawing(new B(t.points[0],t.options))}}))}}class O{makeButton;callbackName;div;isOpen=!1;widget;constructor(t,e,i,s,o,n){this.makeButton=t,this.callbackName=e,this.div=document.createElement("div"),this.div.classList.add("topbar-menu"),this.widget=this.makeButton(s+" ↓",null,o,!0,n),this.updateMenuItems(i),this.widget.elem.addEventListener("click",(()=>{if(this.isOpen=!this.isOpen,!this.isOpen)return void(this.div.style.display="none");let t=this.widget.elem.getBoundingClientRect();this.div.style.display="flex",this.div.style.flexDirection="column";let e=t.x+t.width/2;this.div.style.left=e-this.div.clientWidth/2+"px",this.div.style.top=t.y+t.height+"px"})),document.body.appendChild(this.div)}updateMenuItems(t){this.div.innerHTML="",t.forEach((t=>{let e=this.makeButton(t,null,!1,!1);e.elem.addEventListener("click",(()=>{this._clickHandler(e.elem.innerText)})),e.elem.style.margin="4px 4px",e.elem.style.padding="2px 2px",this.div.appendChild(e.elem)})),this.widget.elem.innerText=t[0]+" ↓"}_clickHandler(t){this.widget.elem.innerText=t+" ↓",window.callbackFunction(`${this.callbackName}_~_${t}`),this.div.style.display="none",this.isOpen=!1}}class V{_handler;_div;left;right;constructor(t){this._handler=t,this._div=document.createElement("div"),this._div.classList.add("topbar");const e=t=>{const e=document.createElement("div");return e.classList.add("topbar-container"),e.style.justifyContent=t,this._div.appendChild(e),e};this.left=e("flex-start"),this.right=e("flex-end")}makeSwitcher(t,e,i,s="left"){const o=document.createElement("div");let n;o.style.margin="4px 12px";const r={elem:o,callbackName:i,intervalElements:t.map((t=>{const i=document.createElement("button");i.classList.add("topbar-button"),i.classList.add("switcher-button"),i.style.margin="0px 2px",i.innerText=t,t==e&&(n=i,i.classList.add("active-switcher-button"));const s=V.getClientWidth(i);return i.style.minWidth=s+1+"px",i.addEventListener("click",(()=>r.onItemClicked(i))),o.appendChild(i),i})),onItemClicked:t=>{t!=n&&(n.classList.remove("active-switcher-button"),t.classList.add("active-switcher-button"),n=t,window.callbackFunction(`${r.callbackName}_~_${t.innerText}`))}};return this.appendWidget(o,s,!0),r}makeTextBoxWidget(t,e="left",i=null){if(i){const s=document.createElement("input");return s.classList.add("topbar-textbox-input"),s.value=t,s.style.width=`${s.value.length+2}ch`,s.addEventListener("focus",(()=>{window.textBoxFocused=!0})),s.addEventListener("input",(t=>{t.preventDefault(),s.style.width=`${s.value.length+2}ch`})),s.addEventListener("keydown",(t=>{"Enter"==t.key&&(t.preventDefault(),s.blur())})),s.addEventListener("blur",(()=>{window.callbackFunction(`${i}_~_${s.value}`),window.textBoxFocused=!1})),this.appendWidget(s,e,!0),s}{const i=document.createElement("div");return i.classList.add("topbar-textbox"),i.innerText=t,this.appendWidget(i,e,!0),i}}makeMenu(t,e,i,s,o){return new O(this.makeButton.bind(this),s,t,e,i,o)}makeButton(t,e,i,s=!0,o="left",n=!1){let r=document.createElement("button");r.classList.add("topbar-button"),r.innerText=t,document.body.appendChild(r),r.style.minWidth=r.clientWidth+1+"px",document.body.removeChild(r);let a={elem:r,callbackName:e};if(e){let t;if(n){let e=!1;t=()=>{e=!e,window.callbackFunction(`${a.callbackName}_~_${e}`),r.style.backgroundColor=e?"var(--active-bg-color)":"",r.style.color=e?"var(--active-color)":""}}else t=()=>window.callbackFunction(`${a.callbackName}_~_${r.innerText}`);r.addEventListener("click",t)}return s&&this.appendWidget(r,o,i),a}makeSeparator(t="left"){const e=document.createElement("div");e.classList.add("topbar-seperator");("left"==t?this.left:this.right).appendChild(e)}appendWidget(t,e,i){const s="left"==e?this.left:this.right;i?("left"==e&&s.appendChild(t),this.makeSeparator(e),"right"==e&&s.appendChild(t)):s.appendChild(t),this._handler.reSize()}static getClientWidth(t){document.body.appendChild(t);const e=t.clientWidth;return document.body.removeChild(t),e}}s();return t.Box=L,t.Handler=class{id;commandFunctions=[];wrapper;div;chart;scale;precision=2;series;volumeSeries;legend;_topBar;toolBox;spinner;_seriesList=[];constructor(t,e,i,s,n){this.reSize=this.reSize.bind(this),this.id=t,this.scale={width:e,height:i},this.wrapper=document.createElement("div"),this.wrapper.classList.add("handler"),this.wrapper.style.float=s,this.div=document.createElement("div"),this.div.style.position="relative",this.wrapper.appendChild(this.div),window.containerDiv.append(this.wrapper),this.chart=this._createChart(),this.series=this.createCandlestickSeries(),this.volumeSeries=this.createVolumeSeries(),this.legend=new o(this),document.addEventListener("keydown",(t=>{for(let e=0;ewindow.handlerInFocus=this.id)),this.reSize(),n&&window.addEventListener("resize",(()=>this.reSize()))}reSize(){let t=0!==this.scale.height&&this._topBar?._div.offsetHeight||0;this.chart.resize(window.innerWidth*this.scale.width,window.innerHeight*this.scale.height-t),this.wrapper.style.width=100*this.scale.width+"%",this.wrapper.style.height=100*this.scale.height+"%",0===this.scale.height||0===this.scale.width?this.toolBox&&(this.toolBox.div.style.display="none"):this.toolBox&&(this.toolBox.div.style.display="flex")}_createChart(){return e.createChart(this.div,{width:window.innerWidth*this.scale.width,height:window.innerHeight*this.scale.height,layout:{textColor:window.pane.color,background:{color:"#000000",type:e.ColorType.Solid},fontSize:12},rightPriceScale:{scaleMargins:{top:.3,bottom:.25}},timeScale:{timeVisible:!0,secondsVisible:!1},crosshair:{mode:e.CrosshairMode.Normal,vertLine:{labelBackgroundColor:"rgb(46, 46, 46)"},horzLine:{labelBackgroundColor:"rgb(55, 55, 55)"}},grid:{vertLines:{color:"rgba(29, 30, 38, 5)"},horzLines:{color:"rgba(29, 30, 58, 5)"}},handleScroll:{vertTouchDrag:!0}})}createCandlestickSeries(){const t="rgba(39, 157, 130, 100)",e="rgba(200, 97, 100, 100)",i=this.chart.addCandlestickSeries({upColor:t,borderUpColor:t,wickUpColor:t,downColor:e,borderDownColor:e,wickDownColor:e});return i.priceScale().applyOptions({scaleMargins:{top:.2,bottom:.2}}),i}createVolumeSeries(){const t=this.chart.addHistogramSeries({color:"#26a69a",priceFormat:{type:"volume"},priceScaleId:"volume_scale"});return t.priceScale().applyOptions({scaleMargins:{top:.8,bottom:0}}),t}createLineSeries(t,e){const i=this.chart.addLineSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createHistogramSeries(t,e){const i=this.chart.addHistogramSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createAreaSeries(t,e){const i=this.chart.addAreaSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createBarSeries(t,e){const i=this.chart.addBarSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createToolBox(){this.toolBox=new P(this.id,this.chart,this.series,this.commandFunctions),this.div.appendChild(this.toolBox.div)}createTopBar(){return this._topBar=new V(this),this.wrapper.prepend(this._topBar._div),this._topBar}toJSON(){const{chart:t,...e}=this;return e}static syncCharts(t,e,i=!1){function s(t,e){e?(t.chart.setCrosshairPosition(e.value||e.close,e.time,t.series),t.legend.legendHandler(e,!0)):t.chart.clearCrosshairPosition()}function o(t,e){return e.time&&e.seriesData.get(t)||null}const n=t.chart.timeScale(),r=e.chart.timeScale(),a=t=>{t&&n.setVisibleLogicalRange(t)},l=t=>{t&&r.setVisibleLogicalRange(t)},h=i=>{s(e,o(t.series,i))},d=i=>{s(t,o(e.series,i))};let c=e;function p(t,e,s,o,n,r){t.wrapper.addEventListener("mouseover",(()=>{c!==t&&(c=t,e.chart.unsubscribeCrosshairMove(s),t.chart.subscribeCrosshairMove(o),i||(e.chart.timeScale().unsubscribeVisibleLogicalRangeChange(n),t.chart.timeScale().subscribeVisibleLogicalRangeChange(r)))}))}p(e,t,h,d,l,a),p(t,e,d,h,a,l),e.chart.subscribeCrosshairMove(d);const u=r.getVisibleLogicalRange();u&&n.setVisibleLogicalRange(u),i||e.chart.timeScale().subscribeVisibleLogicalRangeChange(a)}static makeSearchBox(t){const e=document.createElement("div");e.classList.add("searchbox"),e.style.display="none";const i=document.createElement("div");i.innerHTML='';const s=document.createElement("input");return s.type="text",e.appendChild(i),e.appendChild(s),t.div.appendChild(e),t.commandFunctions.push((i=>window.handlerInFocus===t.id&&!window.textBoxFocused&&("none"===e.style.display?!!/^[a-zA-Z0-9]$/.test(i.key)&&(e.style.display="flex",s.focus(),!0):("Enter"===i.key||"Escape"===i.key)&&("Enter"===i.key&&window.callbackFunction(`search${t.id}_~_${s.value}`),e.style.display="none",s.value="",!0)))),s.addEventListener("input",(()=>s.value=s.value.toUpperCase())),{window:e,box:s}}static makeSpinner(t){t.spinner=document.createElement("div"),t.spinner.classList.add("spinner"),t.wrapper.appendChild(t.spinner);let e=0;!function i(){t.spinner&&(e+=10,t.spinner.style.transform=`translate(-50%, -50%) rotate(${e}deg)`,requestAnimationFrame(i))}()}static _styleMap={"--bg-color":"backgroundColor","--hover-bg-color":"hoverBackgroundColor","--click-bg-color":"clickBackgroundColor","--active-bg-color":"activeBackgroundColor","--muted-bg-color":"mutedBackgroundColor","--border-color":"borderColor","--color":"color","--active-color":"activeColor"};static setRootStyles(t){const e=document.documentElement.style;for(const[i,s]of Object.entries(this._styleMap))e.setProperty(i,t[s])}},t.HorizontalLine=w,t.Legend=o,t.RayLine=M,t.Table=class{_div;callbackName;borderColor;borderWidth;table;rows={};headings;widths;alignments;footer;header;constructor(t,e,i,s,o,n,r=!1,a,l,h,d,c){this._div=document.createElement("div"),this.callbackName=null,this.borderColor=l,this.borderWidth=h,r?(this._div.style.position="absolute",this._div.style.cursor="move"):(this._div.style.position="relative",this._div.style.float=n),this._div.style.zIndex="2000",this.reSize(t,e),this._div.style.display="flex",this._div.style.flexDirection="column",this._div.style.borderRadius="5px",this._div.style.color="white",this._div.style.fontSize="12px",this._div.style.fontVariantNumeric="tabular-nums",this.table=document.createElement("table"),this.table.style.width="100%",this.table.style.borderCollapse="collapse",this._div.style.overflow="hidden",this.headings=i,this.widths=s.map((t=>100*t+"%")),this.alignments=o;let p=this.table.createTHead().insertRow();for(let t=0;t0?c[t]:a,e.style.color=d[t],p.appendChild(e)}let u,_,m=document.createElement("div");if(m.style.overflowY="auto",m.style.overflowX="hidden",m.style.backgroundColor=a,m.appendChild(this.table),this._div.appendChild(m),window.containerDiv.appendChild(this._div),!r)return;let v=t=>{this._div.style.left=t.clientX-u+"px",this._div.style.top=t.clientY-_+"px"},g=()=>{document.removeEventListener("mousemove",v),document.removeEventListener("mouseup",g)};this._div.addEventListener("mousedown",(t=>{u=t.clientX-this._div.offsetLeft,_=t.clientY-this._div.offsetTop,document.addEventListener("mousemove",v),document.addEventListener("mouseup",g)}))}divToButton(t,e){t.addEventListener("mouseover",(()=>t.style.backgroundColor="rgba(60, 60, 60, 0.6)")),t.addEventListener("mouseout",(()=>t.style.backgroundColor="transparent")),t.addEventListener("mousedown",(()=>t.style.backgroundColor="rgba(60, 60, 60)")),t.addEventListener("click",(()=>window.callbackFunction(e))),t.addEventListener("mouseup",(()=>t.style.backgroundColor="rgba(60, 60, 60, 0.6)"))}newRow(t,e=!1){let i=this.table.insertRow();i.style.cursor="default";for(let s=0;s{t&&(window.cursor=t),document.body.style.cursor=window.cursor},t}({},LightweightCharts); +var Lib=function(t,e){"use strict";const i={backgroundColor:"#0c0d0f",hoverBackgroundColor:"#3c434c",clickBackgroundColor:"#50565E",activeBackgroundColor:"rgba(0, 122, 255, 0.7)",mutedBackgroundColor:"rgba(0, 122, 255, 0.3)",borderColor:"#3C434C",color:"#d8d9db",activeColor:"#ececed"};function s(){window.pane={...i},window.containerDiv=document.getElementById("container")||document.createElement("div"),window.setCursor=t=>{t&&(window.cursor=t),document.body.style.cursor=window.cursor},window.cursor="default",window.textBoxFocused=!1}const n='\n\n \n \n\n',o='\n\n \n \n \n\n';class r{handler;div;seriesContainer;ohlcEnabled=!1;percentEnabled=!1;linesEnabled=!1;colorBasedOnCandle=!1;text;candle;_lines=[];_groups=[];constructor(t){this.handler=t,this.div=document.createElement("div"),this.div.classList.add("legend"),this.seriesContainer=document.createElement("div"),this.text=document.createElement("span"),this.candle=document.createElement("div"),this.setupLegend(),this.legendHandler=this.legendHandler.bind(this),t.chart.subscribeCrosshairMove(this.legendHandler)}setupLegend(){this.div.style.maxWidth=100*this.handler.scale.width-8+"vw",this.div.style.display="none";const t=document.createElement("div");t.style.display="flex",t.style.flexDirection="row",this.seriesContainer.classList.add("series-container"),this.text.style.lineHeight="1.8",t.appendChild(this.seriesContainer),this.div.appendChild(this.text),this.div.appendChild(this.candle),this.div.appendChild(t),this.handler.div.appendChild(this.div)}legendItemFormat(t,e){return t.toFixed(e).toString().padStart(8," ")}shorthandFormat(t){const e=Math.abs(t);return e>=1e6?(t/1e6).toFixed(1)+"M":e>=1e3?(t/1e3).toFixed(1)+"K":t.toString().padStart(8," ")}makeSeriesRow(t,e){const i=document.createElement("div");i.style.display="flex",i.style.alignItems="center";const s=document.createElement("div");s.innerText=t;const r=document.createElement("div");r.classList.add("legend-toggle-switch");const a=e.options().color||"rgba(255,0,0,1)",l=a.startsWith("rgba")?a.replace(/[^,]+(?=\))/,"1"):a,d=this.createSvgIcon(n),h=this.createSvgIcon(o);r.appendChild(d.cloneNode(!0));let c=!0;return r.addEventListener("click",(()=>{c=!c,e.applyOptions({visible:c}),r.innerHTML="",r.appendChild(c?d.cloneNode(!0):h.cloneNode(!0))})),i.appendChild(s),i.appendChild(r),this._lines.push({name:t,div:s,row:i,toggle:r,series:e,solid:l}),this.seriesContainer.appendChild(i),i}makeSeriesGroup(t,e,i,s){const r=document.createElement("div");r.style.display="flex",r.style.alignItems="center";const a=document.createElement("div");a.style.color="#FFF",a.innerText=`${t}:`;const l=document.createElement("div");l.classList.add("legend-toggle-switch");const d=this.createSvgIcon(n),h=this.createSvgIcon(o);l.appendChild(d.cloneNode(!0));let c=!0;l.addEventListener("click",(()=>{c=!c,i.forEach((t=>t.applyOptions({visible:c}))),l.innerHTML="",l.appendChild(c?d.cloneNode(!0):h.cloneNode(!0))}));let p=`${t}:`;return e.forEach(((t,e)=>{const i=s[e];p+=` ${t}: -`})),a.innerHTML=p,this._groups.push({name:t,seriesList:i,div:a,row:r,toggle:l,solidColors:s,names:e}),r.appendChild(a),r.appendChild(l),this.seriesContainer.appendChild(r),r}createSvgIcon(t){const e=document.createElement("div");e.innerHTML=t.trim();return e.querySelector("svg")}legendHandler(t,e=!1){if(!this.ohlcEnabled&&!this.linesEnabled&&!this.percentEnabled)return;const i=this.handler.series.options();if(!t.time)return this.candle.style.color="transparent",void(this.candle.innerHTML=this.candle.innerHTML.replace(i.upColor,"").replace(i.downColor,""));let s,n=null;if(e){const e=this.handler.chart.timeScale(),i=e.timeToCoordinate(t.time);i&&(n=e.coordinateToLogical(i.valueOf())),n&&(s=this.handler.series.dataByIndex(n.valueOf()))}else s=t.seriesData.get(this.handler.series);let o='';if(s&&(this.ohlcEnabled&&(o+=`O ${this.legendItemFormat(s.open,this.handler.precision)} `,o+=`| H ${this.legendItemFormat(s.high,this.handler.precision)} `,o+=`| L ${this.legendItemFormat(s.low,this.handler.precision)} `,o+=`| C ${this.legendItemFormat(s.close,this.handler.precision)} `),this.percentEnabled)){const t=(s.close-s.open)/s.open*100,e=t>0?i.upColor:i.downColor,n=`${t>=0?"+":""}${t.toFixed(2)} %`;o+=this.colorBasedOnCandle?`| ${n}`:`| ${n}`}this.candle.innerHTML=o+"",this.updateGroupLegend(t,n,e),this.updateSeriesLegend(t,n,e)}updateGroupLegend(t,e,i){this._groups.forEach((s=>{if(!this.linesEnabled)return void(s.row.style.display="none");s.row.style.display="flex";let n=`${s.name}:`;s.seriesList.forEach(((o,r)=>{const a=i&&e?o.dataByIndex(e):t.seriesData.get(o);if(!a?.value)return;const l=o.options().priceFormat,d="precision"in l?this.legendItemFormat(a.value,l.precision):this.legendItemFormat(a.value,2),h=s.solidColors?s.solidColors[r]:"inherit",c=s.names[r];n+=` ${c}: ${d}`})),s.div.innerHTML=n}))}updateSeriesLegend(t,e,i){this._lines.forEach((s=>{if(!this.linesEnabled)return void(s.row.style.display="none");s.row.style.display="flex";const n=i&&e?s.series.dataByIndex(e):t.seriesData.get(s.series);if(void 0!==n?.value){const t=s.series.options().priceFormat,e="precision"in t?this.legendItemFormat(n.value,t.precision):this.legendItemFormat(n.value,2);s.div.innerHTML=` ${s.name}: ${e}`}else s.div.innerHTML=`${s.name}: -`}))}}function a(t){if(void 0===t)throw new Error("Value is undefined");return t}class l{_chart=void 0;_series=void 0;requestUpdate(){this._requestUpdate&&this._requestUpdate()}_requestUpdate;attached({chart:t,series:e,requestUpdate:i}){this._chart=t,this._series=e,this._series.subscribeDataChanged(this._fireDataUpdated),this._requestUpdate=i,this.requestUpdate()}detached(){this._chart=void 0,this._series=void 0,this._requestUpdate=void 0}get chart(){return a(this._chart)}get series(){return a(this._series)}_fireDataUpdated(t){this.dataUpdated&&this.dataUpdated(t)}}const d={lineColor:"#1E80F0",lineStyle:e.LineStyle.Solid,width:4};var h;!function(t){t[t.NONE=0]="NONE",t[t.HOVERING=1]="HOVERING",t[t.DRAGGING=2]="DRAGGING",t[t.DRAGGINGP1=3]="DRAGGINGP1",t[t.DRAGGINGP2=4]="DRAGGINGP2",t[t.DRAGGINGP3=5]="DRAGGINGP3",t[t.DRAGGINGP4=6]="DRAGGINGP4"}(h||(h={}));class c extends l{_paneViews=[];_options;_points=[];_state=h.NONE;_startDragPoint=null;_latestHoverPoint=null;static _mouseIsDown=!1;static hoveredObject=null;static lastHoveredObject=null;_listeners=[];constructor(t){super(),this._options={...d,...t}}updateAllViews(){this._paneViews.forEach((t=>t.update()))}paneViews(){return this._paneViews}applyOptions(t){this._options={...this._options,...t},this.requestUpdate()}updatePoints(...t){for(let e=0;ei.name===t&&i.listener===e));this._listeners.splice(this._listeners.indexOf(i),1)}_handleHoverInteraction(t){if(this._latestHoverPoint=t.point,c._mouseIsDown)this._handleDragInteraction(t);else if(this._mouseIsOverDrawing(t)){if(this._state!=h.NONE)return;this._moveToState(h.HOVERING),c.hoveredObject=c.lastHoveredObject=this}else{if(this._state==h.NONE)return;this._moveToState(h.NONE),c.hoveredObject===this&&(c.hoveredObject=null)}}static _eventToPoint(t,e){if(!e||!t.point||!t.logical)return null;const i=e.coordinateToPrice(t.point.y);return null==i?null:{time:t.time||null,logical:t.logical,price:i.valueOf()}}static _getDiff(t,e){return{logical:t.logical-e.logical,price:t.price-e.price}}_addDiffToPoint(t,e,i){t&&(t.logical=t.logical+e,t.price=t.price+i,t.time=this.series.dataByIndex(t.logical)?.time||null)}_handleMouseDownInteraction=()=>{c._mouseIsDown=!0,this._onMouseDown()};_handleMouseUpInteraction=()=>{c._mouseIsDown=!1,this._moveToState(h.HOVERING)};_handleDragInteraction(t){if(this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1&&this._state!=h.DRAGGINGP2&&this._state!=h.DRAGGINGP3&&this._state!=h.DRAGGINGP4)return;const e=c._eventToPoint(t,this.series);if(!e)return;this._startDragPoint=this._startDragPoint||e;const i=c._getDiff(e,this._startDragPoint);this._onDrag(i),this.requestUpdate(),this._startDragPoint=e}}class p{_options;constructor(t){this._options=t}}class u extends p{_p1;_p2;_hovered;constructor(t,e,i,s){super(i),this._p1=t,this._p2=e,this._hovered=s}_getScaledCoordinates(t){return null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y?null:{x1:Math.round(this._p1.x*t.horizontalPixelRatio),y1:Math.round(this._p1.y*t.verticalPixelRatio),x2:Math.round(this._p2.x*t.horizontalPixelRatio),y2:Math.round(this._p2.y*t.verticalPixelRatio)}}_drawEndCircle(t,e,i){t.context.fillStyle="#000",t.context.beginPath(),t.context.arc(e,i,9,0,2*Math.PI),t.context.stroke(),t.context.fill()}}function _(t,i){const s={[e.LineStyle.Solid]:[],[e.LineStyle.Dotted]:[t.lineWidth,t.lineWidth],[e.LineStyle.Dashed]:[2*t.lineWidth,2*t.lineWidth],[e.LineStyle.LargeDashed]:[6*t.lineWidth,6*t.lineWidth],[e.LineStyle.SparseDotted]:[t.lineWidth,4*t.lineWidth]}[i];t.setLineDash(s)}class m extends p{_point={x:null,y:null};constructor(t,e){super(e),this._point=t}draw(t){t.useBitmapCoordinateSpace((t=>{if(null==this._point.y)return;const e=t.context,i=Math.round(this._point.y*t.verticalPixelRatio),s=this._point.x?this._point.x*t.horizontalPixelRatio:0;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,_(e,this._options.lineStyle),e.beginPath(),e.moveTo(s,i),e.lineTo(t.bitmapSize.width,i),e.stroke()}))}}class g{_source;constructor(t){this._source=t}}class v extends g{_p1={x:null,y:null};_p2={x:null,y:null};_source;constructor(t){super(t),this._source=t}update(){if(!this._source.p1||!this._source.p2)return;const t=this._source.series,e=t.priceToCoordinate(this._source.p1.price),i=t.priceToCoordinate(this._source.p2.price),s=this._getX(this._source.p1),n=this._getX(this._source.p2);this._p1={x:s,y:e},this._p2={x:n,y:i}}_getX(t){return this._source.chart.timeScale().logicalToCoordinate(t.logical)}}class w extends g{_source;_point={x:null,y:null};constructor(t){super(t),this._source=t}update(){const t=this._source._point,e=this._source.chart.timeScale(),i=this._source.series;"RayLine"==this._source._type&&(this._point.x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical)),this._point.y=i.priceToCoordinate(t.price)}renderer(){return new m(this._point,this._source._options)}}class y{_source;_y=null;_price=null;constructor(t){this._source=t}update(){if(!this._source.series||!this._source._point)return;this._y=this._source.series.priceToCoordinate(this._source._point.price);const t=this._source.series.options().priceFormat.precision;this._price=this._source._point.price.toFixed(t).toString()}visible(){return!0}tickVisible(){return!0}coordinate(){return this._y??0}text(){return this._source._options.text||this._price||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class b extends c{_type="HorizontalLine";_paneViews;_point;_callbackName;_priceAxisViews;_startDragPoint=null;constructor(t,e,i=null){super(e),this._point=t,this._point.time=null,this._paneViews=[new w(this)],this._priceAxisViews=[new y(this)],this._callbackName=i}get points(){return[this._point]}updatePoints(...t){for(const e of t)e&&(this._point.price=e.price);this.requestUpdate()}updateAllViews(){this._paneViews.forEach((t=>t.update())),this._priceAxisViews.forEach((t=>t.update()))}priceAxisViews(){return this._priceAxisViews}_moveToState(t){switch(t){case h.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._addDiffToPoint(this._point,0,t.price),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.series.priceToCoordinate(this._point.price);return!!i&&Math.abs(i-t.point.y){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class x{_chart;_series;_finishDrawingCallback=null;_drawings=[];_activeDrawing=null;_isDrawing=!1;_drawingType=null;constructor(t,e,i=null){this._chart=t,this._series=e,this._finishDrawingCallback=i,this._chart.subscribeClick(this._clickHandler),this._chart.subscribeCrosshairMove(this._moveHandler)}_clickHandler=t=>this._onClick(t);_moveHandler=t=>this._onMouseMove(t);beginDrawing(t){this._drawingType=t,this._isDrawing=!0}stopDrawing(){this._isDrawing=!1,this._activeDrawing=null}get drawings(){return this._drawings}addNewDrawing(t){this._series.attachPrimitive(t),this._drawings.push(t)}delete(t){if(null==t)return;const e=this._drawings.indexOf(t);-1!=e&&(this._drawings.splice(e,1),t.detach())}clearDrawings(){for(const t of this._drawings)t.detach();this._drawings=[]}repositionOnTime(){for(const t of this.drawings){const e=[];for(const i of t.points){if(!i){e.push(i);continue}const t=i.time?this._chart.timeScale().coordinateToLogical(this._chart.timeScale().timeToCoordinate(i.time)||0):i.logical;e.push({time:i.time,logical:t,price:i.price})}t.updatePoints(...e)}}_onClick(t){if(!this._isDrawing)return;const e=c._eventToPoint(t,this._series);if(e)if(null==this._activeDrawing){if(null==this._drawingType)return;this._activeDrawing=new this._drawingType(e,e),this._series.attachPrimitive(this._activeDrawing),this._drawingType==b&&this._onClick(t)}else{if(this._drawings.push(this._activeDrawing),this.stopDrawing(),!this._finishDrawingCallback)return;this._finishDrawingCallback()}}_onMouseMove(t){if(!t)return;for(const e of this._drawings)e._handleHoverInteraction(t);if(!this._isDrawing||!this._activeDrawing)return;const e=c._eventToPoint(t,this._series);e&&this._activeDrawing.updatePoints(null,e)}}class C extends u{constructor(t,e,i,s){super(t,e,i,s)}draw(t){t.useBitmapCoordinateSpace((t=>{if(null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y)return;const e=t.context,i=this._getScaledCoordinates(t);i&&(e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,_(e,this._options.lineStyle),e.beginPath(),e.moveTo(i.x1,i.y1),e.lineTo(i.x2,i.y2),e.stroke(),this._hovered&&(this._drawEndCircle(t,i.x1,i.y1),this._drawEndCircle(t,i.x2,i.y2)))}))}}class f extends v{constructor(t){super(t)}renderer(){return new C(this._p1,this._p2,this._source._options,this._source.hovered)}}class D extends c{_paneViews=[];_hovered=!1;constructor(t,e,i){super(),this.points.push(t),this.points.push(e),this._options={...d,...i}}setFirstPoint(t){this.updatePoints(t)}setSecondPoint(t){this.updatePoints(null,t)}get p1(){return this.points[0]}get p2(){return this.points[1]}get hovered(){return this._hovered}}class k extends D{_type="TrendLine";constructor(t,e,i){super(t,e,i),this._paneViews=[new f(this)]}_moveToState(t){switch(t){case h.NONE:document.body.style.cursor="default",this._hovered=!1,this.requestUpdate(),this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this.requestUpdate(),this._subscribe("mousedown",this._handleMouseDownInteraction),this._unsubscribe("mouseup",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGINGP1:case h.DRAGGINGP2:case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1||this._addDiffToPoint(this.p1,t.logical,t.price),this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP2||this._addDiffToPoint(this.p2,t.logical,t.price)}_onMouseDown(){this._startDragPoint=null;const t=this._latestHoverPoint;if(!t)return;const e=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(e.x&&i.x&&e.y&&i.y))return this._moveToState(h.DRAGGING);Math.abs(t.x-e.x)<10&&Math.abs(t.y-e.y)<10?this._moveToState(h.DRAGGINGP1):Math.abs(t.x-i.x)<10&&Math.abs(t.y-i.y)<10?this._moveToState(h.DRAGGINGP2):this._moveToState(h.DRAGGING)}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this._paneViews[0]._p1.x,s=this._paneViews[0]._p1.y,n=this._paneViews[0]._p2.x,o=this._paneViews[0]._p2.y;if(!(i&&n&&s&&o))return!1;const r=t.point.x,a=t.point.y;if(r<=Math.min(i,n)-e||r>=Math.max(i,n)+e)return!1;return Math.abs((o-s)*r-(n-i)*a+n*s-o*i)/Math.sqrt((o-s)**2+(n-i)**2)<=e}}class L extends u{constructor(t,e,i,s){super(t,e,i,s)}draw(t){t.useBitmapCoordinateSpace((t=>{const e=t.context,i=this._getScaledCoordinates(t);if(!i)return;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,_(e,this._options.lineStyle),e.fillStyle=this._options.fillColor;const s=Math.min(i.x1,i.x2),n=Math.min(i.y1,i.y2),o=Math.abs(i.x1-i.x2),r=Math.abs(i.y1-i.y2);e.strokeRect(s,n,o,r),e.fillRect(s,n,o,r),this._hovered&&(this._drawEndCircle(t,s,n),this._drawEndCircle(t,s+o,n),this._drawEndCircle(t,s+o,n+r),this._drawEndCircle(t,s,n+r))}))}}class E extends v{constructor(t){super(t)}renderer(){return new L(this._p1,this._p2,this._source._options,this._source.hovered)}}const S={fillEnabled:!0,fillColor:"rgba(255, 255, 255, 0.2)",...d};class G extends D{_type="Box";constructor(t,e,i){super(t,e,i),this._options={...S,...i},this._paneViews=[new E(this)]}_moveToState(t){switch(t){case h.NONE:document.body.style.cursor="default",this._hovered=!1,this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this._unsubscribe("mouseup",this._handleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGINGP1:case h.DRAGGINGP2:case h.DRAGGINGP3:case h.DRAGGINGP4:case h.DRAGGING:document.body.style.cursor="grabbing",document.body.addEventListener("mouseup",this._handleMouseUpInteraction),this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1||this._addDiffToPoint(this.p1,t.logical,t.price),this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP2||this._addDiffToPoint(this.p2,t.logical,t.price),this._state!=h.DRAGGING&&(this._state==h.DRAGGINGP3&&(this._addDiffToPoint(this.p1,t.logical,0),this._addDiffToPoint(this.p2,0,t.price)),this._state==h.DRAGGINGP4&&(this._addDiffToPoint(this.p1,0,t.price),this._addDiffToPoint(this.p2,t.logical,0)))}_onMouseDown(){this._startDragPoint=null;const t=this._latestHoverPoint,e=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(e.x&&i.x&&e.y&&i.y))return this._moveToState(h.DRAGGING);const s=10;Math.abs(t.x-e.x)l-p&&rd-p&&ai.appendChild(this.makeColorBox(t))));let s=document.createElement("div");s.style.backgroundColor=window.pane.borderColor,s.style.height="1px",s.style.width="130px";let n=document.createElement("div");n.style.margin="10px";let o=document.createElement("div");o.style.color="lightgray",o.style.fontSize="12px",o.innerText="Opacity",this._opacityLabel=document.createElement("div"),this._opacityLabel.style.color="lightgray",this._opacityLabel.style.fontSize="12px",this._opacitySlider=document.createElement("input"),this._opacitySlider.type="range",this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%",this._opacitySlider.oninput=()=>{this._opacityLabel.innerText=this._opacitySlider.value+"%",this.opacity=parseInt(this._opacitySlider.value)/100,this.updateColor()},n.appendChild(o),n.appendChild(this._opacitySlider),n.appendChild(this._opacityLabel),this._div.appendChild(i),this._div.appendChild(s),this._div.appendChild(n),window.containerDiv.appendChild(this._div)}_updateOpacitySlider(){this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%"}makeColorBox(t){const e=document.createElement("div");e.style.width="18px",e.style.height="18px",e.style.borderRadius="3px",e.style.margin="3px",e.style.boxSizing="border-box",e.style.backgroundColor=t,e.addEventListener("mouseover",(()=>e.style.border="2px solid lightgray")),e.addEventListener("mouseout",(()=>e.style.border="none"));const i=I.extractRGBA(t);return e.addEventListener("click",(()=>{this.rgba=i,this.updateColor()})),e}static extractRGBA(t){const e=document.createElement("div");e.style.color=t,document.body.appendChild(e);const i=getComputedStyle(e).color;document.body.removeChild(e);const s=i.match(/\d+/g)?.map(Number);if(!s)return[];let n=i.includes("rgba")?parseFloat(i.split(",")[3]):1;return[s[0],s[1],s[2],n]}updateColor(){if(!c.lastHoveredObject||!this.rgba)return;const t=`rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`;c.lastHoveredObject.applyOptions({[this.colorOption]:t}),this.saveDrawings()}openMenu(t){c.lastHoveredObject&&(this.rgba=I.extractRGBA(c.lastHoveredObject._options[this.colorOption]),this.opacity=this.rgba[3],this._updateOpacitySlider(),this._div.style.top=t.top-30+"px",this._div.style.left=t.right+"px",this._div.style.display="flex",setTimeout((()=>document.addEventListener("mousedown",(t=>{this._div.contains(t.target)||this.closeMenu()}))),10))}closeMenu(){document.body.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}class T{static _styles=[{name:"Solid",var:e.LineStyle.Solid},{name:"Dotted",var:e.LineStyle.Dotted},{name:"Dashed",var:e.LineStyle.Dashed},{name:"Large Dashed",var:e.LineStyle.LargeDashed},{name:"Sparse Dotted",var:e.LineStyle.SparseDotted}];_div;_saveDrawings;constructor(t){this._saveDrawings=t,this._div=document.createElement("div"),this._div.classList.add("context-menu"),T._styles.forEach((t=>{this._div.appendChild(this._makeTextBox(t.name,t.var))})),window.containerDiv.appendChild(this._div)}_makeTextBox(t,e){const i=document.createElement("span");return i.classList.add("context-menu-item"),i.innerText=t,i.addEventListener("click",(()=>{c.lastHoveredObject?.applyOptions({lineStyle:e}),this._saveDrawings()})),i}openMenu(t){this._div.style.top=t.top-30+"px",this._div.style.left=t.right+"px",this._div.style.display="block",setTimeout((()=>document.addEventListener("mousedown",(t=>{this._div.contains(t.target)||this.closeMenu()}))),10)}closeMenu(){document.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}function M(t){const e=[];for(const i of t)0==e.length?e.push(i.toUpperCase()):i==i.toUpperCase()?e.push(" "+i):e.push(i);return e.join("")}class N{saveDrawings;drawingTool;div;hoverItem;items=[];constructor(t,e){this.saveDrawings=t,this.drawingTool=e,this._onRightClick=this._onRightClick.bind(this),this.div=document.createElement("div"),this.div.classList.add("context-menu"),document.body.appendChild(this.div),this.hoverItem=null,document.body.addEventListener("contextmenu",this._onRightClick)}_handleClick=t=>this._onClick(t);_onClick(t){t.target&&(this.div.contains(t.target)||(this.div.style.display="none",document.body.removeEventListener("click",this._handleClick)))}_onRightClick(t){if(!c.hoveredObject)return;for(const t of this.items)this.div.removeChild(t);this.items=[];for(const t of Object.keys(c.hoveredObject._options)){let e;if(t.toLowerCase().includes("color"))e=new I(this.saveDrawings,t);else{if("lineStyle"!==t)continue;e=new T(this.saveDrawings)}let i=t=>e.openMenu(t);this.menuItem(M(t),i,(()=>{document.removeEventListener("click",e.closeMenu),e._div.style.display="none"}))}this.separator(),this.menuItem("Delete Drawing",(()=>this.drawingTool.delete(c.lastHoveredObject))),t.preventDefault(),this.div.style.left=t.clientX+"px",this.div.style.top=t.clientY+"px",this.div.style.display="block",document.body.addEventListener("click",this._handleClick)}menuItem(t,e,i=null){const s=document.createElement("span");s.classList.add("context-menu-item"),this.div.appendChild(s);const n=document.createElement("span");if(n.innerText=t,n.style.pointerEvents="none",s.appendChild(n),i){let t=document.createElement("span");t.innerText="►",t.style.fontSize="8px",t.style.pointerEvents="none",s.appendChild(t)}if(s.addEventListener("mouseover",(()=>{this.hoverItem&&this.hoverItem.closeAction&&this.hoverItem.closeAction(),this.hoverItem={elem:n,action:e,closeAction:i}})),i){let t;s.addEventListener("mouseover",(()=>t=setTimeout((()=>e(s.getBoundingClientRect())),100))),s.addEventListener("mouseout",(()=>clearTimeout(t)))}else s.addEventListener("click",(t=>{e(t),this.div.style.display="none"}));this.items.push(s)}separator(){const t=document.createElement("div");t.style.width="90%",t.style.height="1px",t.style.margin="3px 0px",t.style.backgroundColor=window.pane.borderColor,this.div.appendChild(t),this.items.push(t)}}class R extends b{_type="RayLine";constructor(t,e){super({...t},e),this._point.time=t.time}updatePoints(...t){for(const e of t)e&&(this._point=e);this.requestUpdate()}_onDrag(t){this._addDiffToPoint(this._point,t.logical,t.price),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.series.priceToCoordinate(this._point.price),s=this._point.time?this.chart.timeScale().timeToCoordinate(this._point.time):null;return!(!i||!s)&&(Math.abs(i-t.point.y)s-e)}}class B extends p{_point={x:null,y:null};constructor(t,e){super(e),this._point=t}draw(t){t.useBitmapCoordinateSpace((t=>{if(null==this._point.x)return;const e=t.context,i=this._point.x*t.horizontalPixelRatio;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,_(e,this._options.lineStyle),e.beginPath(),e.moveTo(i,0),e.lineTo(i,t.bitmapSize.height),e.stroke()}))}}class A extends g{_source;_point={x:null,y:null};constructor(t){super(t),this._source=t}update(){const t=this._source._point,e=this._source.chart.timeScale(),i=this._source.series;this._point.x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical),this._point.y=i.priceToCoordinate(t.price)}renderer(){return new B(this._point,this._source._options)}}class P{_source;_x=null;constructor(t){this._source=t}update(){if(!this._source.chart||!this._source._point)return;const t=this._source._point,e=this._source.chart.timeScale();this._x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical)}visible(){return!!this._source._options.text}tickVisible(){return!0}coordinate(){return this._x??0}text(){return this._source._options.text||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class O extends c{_type="VerticalLine";_paneViews;_timeAxisViews;_point;_callbackName;_startDragPoint=null;constructor(t,e,i=null){super(e),this._point=t,this._paneViews=[new A(this)],this._callbackName=i,this._timeAxisViews=[new P(this)]}updateAllViews(){this._paneViews.forEach((t=>t.update())),this._timeAxisViews.forEach((t=>t.update()))}timeAxisViews(){return this._timeAxisViews}updatePoints(...t){for(const e of t)e&&(!e.time&&e.logical&&(e.time=this.series.dataByIndex(e.logical)?.time||null),this._point=e);this.requestUpdate()}get points(){return[this._point]}_moveToState(t){switch(t){case h.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._addDiffToPoint(this._point,t.logical,0),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.chart.timeScale();let s;return s=this._point.time?i.timeToCoordinate(this._point.time):i.logicalToCoordinate(this._point.logical),!!s&&Math.abs(s-t.point.x){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class F{static TREND_SVG='';static HORZ_SVG='';static RAY_SVG='';static BOX_SVG='';static VERT_SVG=F.RAY_SVG;div;activeIcon=null;buttons=[];_commandFunctions;_handlerID;_drawingTool;constructor(t,e,i,s){this._handlerID=t,this._commandFunctions=s,this._drawingTool=new x(e,i,(()=>this.removeActiveAndSave())),this.div=this._makeToolBox(),new N(this.saveDrawings,this._drawingTool),s.push((t=>{if((t.metaKey||t.ctrlKey)&&"KeyZ"===t.code){const t=this._drawingTool.drawings.pop();return t&&this._drawingTool.delete(t),!0}return!1}))}toJSON(){const{...t}=this;return t}_makeToolBox(){let t=document.createElement("div");t.classList.add("toolbox"),this.buttons.push(this._makeToolBoxElement(k,"KeyT",F.TREND_SVG)),this.buttons.push(this._makeToolBoxElement(b,"KeyH",F.HORZ_SVG)),this.buttons.push(this._makeToolBoxElement(R,"KeyR",F.RAY_SVG)),this.buttons.push(this._makeToolBoxElement(G,"KeyB",F.BOX_SVG)),this.buttons.push(this._makeToolBoxElement(O,"KeyV",F.VERT_SVG,!0));for(const e of this.buttons)t.appendChild(e);return t}_makeToolBoxElement(t,e,i,s=!1){const n=document.createElement("div");n.classList.add("toolbox-button");const o=document.createElementNS("http://www.w3.org/2000/svg","svg");o.setAttribute("width","29"),o.setAttribute("height","29");const r=document.createElementNS("http://www.w3.org/2000/svg","g");r.innerHTML=i,r.setAttribute("fill",window.pane.color),o.appendChild(r),n.appendChild(o);const a={div:n,group:r,type:t};return n.addEventListener("click",(()=>this._onIconClick(a))),this._commandFunctions.push((t=>this._handlerID===window.handlerInFocus&&(!(!t.altKey||t.code!==e)&&(t.preventDefault(),this._onIconClick(a),!0)))),1==s&&(o.style.transform="rotate(90deg)",o.style.transformBox="fill-box",o.style.transformOrigin="center"),n}_onIconClick(t){this.activeIcon&&(this.activeIcon.div.classList.remove("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.stopDrawing(),this.activeIcon===t)?this.activeIcon=null:(this.activeIcon=t,this.activeIcon.div.classList.add("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.beginDrawing(this.activeIcon.type))}removeActiveAndSave=()=>{window.setCursor("default"),this.activeIcon&&this.activeIcon.div.classList.remove("active-toolbox-button"),this.activeIcon=null,this.saveDrawings()};addNewDrawing(t){this._drawingTool.addNewDrawing(t)}clearDrawings(){this._drawingTool.clearDrawings()}saveDrawings=()=>{const t=[];for(const e of this._drawingTool.drawings)t.push({type:e._type,points:e.points,options:e._options});const e=JSON.stringify(t);window.callbackFunction(`save_drawings${this._handlerID}_~_${e}`)};loadDrawings(t){t.forEach((t=>{switch(t.type){case"Box":this._drawingTool.addNewDrawing(new G(t.points[0],t.points[1],t.options));break;case"TrendLine":this._drawingTool.addNewDrawing(new k(t.points[0],t.points[1],t.options));break;case"HorizontalLine":this._drawingTool.addNewDrawing(new b(t.points[0],t.options));break;case"RayLine":this._drawingTool.addNewDrawing(new R(t.points[0],t.options));break;case"VerticalLine":this._drawingTool.addNewDrawing(new O(t.points[0],t.options))}}))}}class V{makeButton;callbackName;div;isOpen=!1;widget;constructor(t,e,i,s,n,o){this.makeButton=t,this.callbackName=e,this.div=document.createElement("div"),this.div.classList.add("topbar-menu"),this.widget=this.makeButton(s+" ↓",null,n,!0,o),this.updateMenuItems(i),this.widget.elem.addEventListener("click",(()=>{if(this.isOpen=!this.isOpen,!this.isOpen)return void(this.div.style.display="none");let t=this.widget.elem.getBoundingClientRect();this.div.style.display="flex",this.div.style.flexDirection="column";let e=t.x+t.width/2;this.div.style.left=e-this.div.clientWidth/2+"px",this.div.style.top=t.y+t.height+"px"})),document.body.appendChild(this.div)}updateMenuItems(t){this.div.innerHTML="",t.forEach((t=>{let e=this.makeButton(t,null,!1,!1);e.elem.addEventListener("click",(()=>{this._clickHandler(e.elem.innerText)})),e.elem.style.margin="4px 4px",e.elem.style.padding="2px 2px",this.div.appendChild(e.elem)})),this.widget.elem.innerText=t[0]+" ↓"}_clickHandler(t){this.widget.elem.innerText=t+" ↓",window.callbackFunction(`${this.callbackName}_~_${t}`),this.div.style.display="none",this.isOpen=!1}}class H{_handler;_div;left;right;constructor(t){this._handler=t,this._div=document.createElement("div"),this._div.classList.add("topbar");const e=t=>{const e=document.createElement("div");return e.classList.add("topbar-container"),e.style.justifyContent=t,this._div.appendChild(e),e};this.left=e("flex-start"),this.right=e("flex-end")}makeSwitcher(t,e,i,s="left"){const n=document.createElement("div");let o;n.style.margin="4px 12px";const r={elem:n,callbackName:i,intervalElements:t.map((t=>{const i=document.createElement("button");i.classList.add("topbar-button"),i.classList.add("switcher-button"),i.style.margin="0px 2px",i.innerText=t,t==e&&(o=i,i.classList.add("active-switcher-button"));const s=H.getClientWidth(i);return i.style.minWidth=s+1+"px",i.addEventListener("click",(()=>r.onItemClicked(i))),n.appendChild(i),i})),onItemClicked:t=>{t!=o&&(o.classList.remove("active-switcher-button"),t.classList.add("active-switcher-button"),o=t,window.callbackFunction(`${r.callbackName}_~_${t.innerText}`))}};return this.appendWidget(n,s,!0),r}makeTextBoxWidget(t,e="left",i=null){if(i){const s=document.createElement("input");return s.classList.add("topbar-textbox-input"),s.value=t,s.style.width=`${s.value.length+2}ch`,s.addEventListener("focus",(()=>{window.textBoxFocused=!0})),s.addEventListener("input",(t=>{t.preventDefault(),s.style.width=`${s.value.length+2}ch`})),s.addEventListener("keydown",(t=>{"Enter"==t.key&&(t.preventDefault(),s.blur())})),s.addEventListener("blur",(()=>{window.callbackFunction(`${i}_~_${s.value}`),window.textBoxFocused=!1})),this.appendWidget(s,e,!0),s}{const i=document.createElement("div");return i.classList.add("topbar-textbox"),i.innerText=t,this.appendWidget(i,e,!0),i}}makeMenu(t,e,i,s,n){return new V(this.makeButton.bind(this),s,t,e,i,n)}makeButton(t,e,i,s=!0,n="left",o=!1){let r=document.createElement("button");r.classList.add("topbar-button"),r.innerText=t,document.body.appendChild(r),r.style.minWidth=r.clientWidth+1+"px",document.body.removeChild(r);let a={elem:r,callbackName:e};if(e){let t;if(o){let e=!1;t=()=>{e=!e,window.callbackFunction(`${a.callbackName}_~_${e}`),r.style.backgroundColor=e?"var(--active-bg-color)":"",r.style.color=e?"var(--active-color)":""}}else t=()=>window.callbackFunction(`${a.callbackName}_~_${r.innerText}`);r.addEventListener("click",t)}return s&&this.appendWidget(r,n,i),a}makeSeparator(t="left"){const e=document.createElement("div");e.classList.add("topbar-seperator");("left"==t?this.left:this.right).appendChild(e)}appendWidget(t,e,i){const s="left"==e?this.left:this.right;i?("left"==e&&s.appendChild(t),this.makeSeparator(e),"right"==e&&s.appendChild(t)):s.appendChild(t),this._handler.reSize()}static getClientWidth(t){document.body.appendChild(t);const e=t.clientWidth;return document.body.removeChild(t),e}}s();return t.Box=G,t.Handler=class{id;commandFunctions=[];wrapper;div;chart;scale;precision=2;series;volumeSeries;legend;_topBar;toolBox;spinner;_seriesList=[];constructor(t,e,i,s,n){this.reSize=this.reSize.bind(this),this.id=t,this.scale={width:e,height:i},this.wrapper=document.createElement("div"),this.wrapper.classList.add("handler"),this.wrapper.style.float=s,this.div=document.createElement("div"),this.div.style.position="relative",this.wrapper.appendChild(this.div),window.containerDiv.append(this.wrapper),this.chart=this._createChart(),this.series=this.createCandlestickSeries(),this.volumeSeries=this.createVolumeSeries(),this.legend=new r(this),document.addEventListener("keydown",(t=>{for(let e=0;ewindow.handlerInFocus=this.id)),this.reSize(),n&&window.addEventListener("resize",(()=>this.reSize()))}reSize(){let t=0!==this.scale.height&&this._topBar?._div.offsetHeight||0;this.chart.resize(window.innerWidth*this.scale.width,window.innerHeight*this.scale.height-t),this.wrapper.style.width=100*this.scale.width+"%",this.wrapper.style.height=100*this.scale.height+"%",0===this.scale.height||0===this.scale.width?this.toolBox&&(this.toolBox.div.style.display="none"):this.toolBox&&(this.toolBox.div.style.display="flex")}_createChart(){return e.createChart(this.div,{width:window.innerWidth*this.scale.width,height:window.innerHeight*this.scale.height,layout:{textColor:window.pane.color,background:{color:"#000000",type:e.ColorType.Solid},fontSize:12},rightPriceScale:{scaleMargins:{top:.3,bottom:.25}},timeScale:{timeVisible:!0,secondsVisible:!1},crosshair:{mode:e.CrosshairMode.Normal,vertLine:{labelBackgroundColor:"rgb(46, 46, 46)"},horzLine:{labelBackgroundColor:"rgb(55, 55, 55)"}},grid:{vertLines:{color:"rgba(29, 30, 38, 5)"},horzLines:{color:"rgba(29, 30, 58, 5)"}},handleScroll:{vertTouchDrag:!0}})}createCandlestickSeries(){const t="rgba(39, 157, 130, 100)",e="rgba(200, 97, 100, 100)",i=this.chart.addCandlestickSeries({upColor:t,borderUpColor:t,wickUpColor:t,downColor:e,borderDownColor:e,wickDownColor:e});return i.priceScale().applyOptions({scaleMargins:{top:.2,bottom:.2}}),i}createVolumeSeries(){const t=this.chart.addHistogramSeries({color:"#26a69a",priceFormat:{type:"volume"},priceScaleId:"volume_scale"});return t.priceScale().applyOptions({scaleMargins:{top:.8,bottom:0}}),t}createLineSeries(t,e){const{group:i,...s}=e,n=this.chart.addLineSeries(s);this._seriesList.push(n);const o=n.options().color||"rgba(255,0,0,1)",r=o.startsWith("rgba")?o.replace(/[^,]+(?=\))/,"1"):o;if(i&&""!==i){const e=this.legend._groups.find((t=>t.name===i));e?(e.names.push(t),e.seriesList.push(n),e.solidColors.push(r)):this.legend.makeSeriesGroup(i,[t],[n],[r])}else this.legend.makeSeriesRow(t,n);return{name:t,series:n}}createHistogramSeries(t,e){const i=this.chart.addHistogramSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createAreaSeries(t,e){const i=this.chart.addAreaSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createBarSeries(t,e){const i=this.chart.addBarSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createToolBox(){this.toolBox=new F(this.id,this.chart,this.series,this.commandFunctions),this.div.appendChild(this.toolBox.div)}createTopBar(){return this._topBar=new H(this),this.wrapper.prepend(this._topBar._div),this._topBar}toJSON(){const{chart:t,...e}=this;return e}static syncCharts(t,e,i=!1){function s(t,e){e?(t.chart.setCrosshairPosition(e.value||e.close,e.time,t.series),t.legend.legendHandler(e,!0)):t.chart.clearCrosshairPosition()}function n(t,e){return e.time&&e.seriesData.get(t)||null}const o=t.chart.timeScale(),r=e.chart.timeScale(),a=t=>{t&&o.setVisibleLogicalRange(t)},l=t=>{t&&r.setVisibleLogicalRange(t)},d=i=>{s(e,n(t.series,i))},h=i=>{s(t,n(e.series,i))};let c=e;function p(t,e,s,n,o,r){t.wrapper.addEventListener("mouseover",(()=>{c!==t&&(c=t,e.chart.unsubscribeCrosshairMove(s),t.chart.subscribeCrosshairMove(n),i||(e.chart.timeScale().unsubscribeVisibleLogicalRangeChange(o),t.chart.timeScale().subscribeVisibleLogicalRangeChange(r)))}))}p(e,t,d,h,l,a),p(t,e,h,d,a,l),e.chart.subscribeCrosshairMove(h);const u=r.getVisibleLogicalRange();u&&o.setVisibleLogicalRange(u),i||e.chart.timeScale().subscribeVisibleLogicalRangeChange(a)}static makeSearchBox(t){const e=document.createElement("div");e.classList.add("searchbox"),e.style.display="none";const i=document.createElement("div");i.innerHTML='';const s=document.createElement("input");return s.type="text",e.appendChild(i),e.appendChild(s),t.div.appendChild(e),t.commandFunctions.push((i=>window.handlerInFocus===t.id&&!window.textBoxFocused&&("none"===e.style.display?!!/^[a-zA-Z0-9]$/.test(i.key)&&(e.style.display="flex",s.focus(),!0):("Enter"===i.key||"Escape"===i.key)&&("Enter"===i.key&&window.callbackFunction(`search${t.id}_~_${s.value}`),e.style.display="none",s.value="",!0)))),s.addEventListener("input",(()=>s.value=s.value.toUpperCase())),{window:e,box:s}}static makeSpinner(t){t.spinner=document.createElement("div"),t.spinner.classList.add("spinner"),t.wrapper.appendChild(t.spinner);let e=0;!function i(){t.spinner&&(e+=10,t.spinner.style.transform=`translate(-50%, -50%) rotate(${e}deg)`,requestAnimationFrame(i))}()}static _styleMap={"--bg-color":"backgroundColor","--hover-bg-color":"hoverBackgroundColor","--click-bg-color":"clickBackgroundColor","--active-bg-color":"activeBackgroundColor","--muted-bg-color":"mutedBackgroundColor","--border-color":"borderColor","--color":"color","--active-color":"activeColor"};static setRootStyles(t){const e=document.documentElement.style;for(const[i,s]of Object.entries(this._styleMap))e.setProperty(i,t[s])}},t.HorizontalLine=b,t.Legend=r,t.RayLine=R,t.Table=class{_div;callbackName;borderColor;borderWidth;table;rows={};headings;widths;alignments;footer;header;constructor(t,e,i,s,n,o,r=!1,a,l,d,h,c){this._div=document.createElement("div"),this.callbackName=null,this.borderColor=l,this.borderWidth=d,r?(this._div.style.position="absolute",this._div.style.cursor="move"):(this._div.style.position="relative",this._div.style.float=o),this._div.style.zIndex="2000",this.reSize(t,e),this._div.style.display="flex",this._div.style.flexDirection="column",this._div.style.borderRadius="5px",this._div.style.color="white",this._div.style.fontSize="12px",this._div.style.fontVariantNumeric="tabular-nums",this.table=document.createElement("table"),this.table.style.width="100%",this.table.style.borderCollapse="collapse",this._div.style.overflow="hidden",this.headings=i,this.widths=s.map((t=>100*t+"%")),this.alignments=n;let p=this.table.createTHead().insertRow();for(let t=0;t0?c[t]:a,e.style.color=h[t],p.appendChild(e)}let u,_,m=document.createElement("div");if(m.style.overflowY="auto",m.style.overflowX="hidden",m.style.backgroundColor=a,m.appendChild(this.table),this._div.appendChild(m),window.containerDiv.appendChild(this._div),!r)return;let g=t=>{this._div.style.left=t.clientX-u+"px",this._div.style.top=t.clientY-_+"px"},v=()=>{document.removeEventListener("mousemove",g),document.removeEventListener("mouseup",v)};this._div.addEventListener("mousedown",(t=>{u=t.clientX-this._div.offsetLeft,_=t.clientY-this._div.offsetTop,document.addEventListener("mousemove",g),document.addEventListener("mouseup",v)}))}divToButton(t,e){t.addEventListener("mouseover",(()=>t.style.backgroundColor="rgba(60, 60, 60, 0.6)")),t.addEventListener("mouseout",(()=>t.style.backgroundColor="transparent")),t.addEventListener("mousedown",(()=>t.style.backgroundColor="rgba(60, 60, 60)")),t.addEventListener("click",(()=>window.callbackFunction(e))),t.addEventListener("mouseup",(()=>t.style.backgroundColor="rgba(60, 60, 60, 0.6)"))}newRow(t,e=!1){let i=this.table.insertRow();i.style.cursor="default";for(let s=0;s{t&&(window.cursor=t),document.body.style.cursor=window.cursor},t}({},LightweightCharts); From 7038770fe0fd0cebe3b1e22ffa3c44198235f7a0 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:32:29 -0700 Subject: [PATCH 12/89] Add group, legend_symbol to Area, Histogram, Bar series types --- lightweight_charts/abstract.py | 161 ++++++++++++++++++++++++--------- 1 file changed, 120 insertions(+), 41 deletions(-) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index a6b3f70..6447696 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -423,25 +423,28 @@ def vertical_span( return VerticalSpan(self, start_time, end_time, color) - class Line(SeriesCommon): - def __init__(self, chart, name, color, style, width, price_line, price_label, group, price_scale_id=None, crosshair_marker=True): + def __init__( + self, chart, name, color, style, width, price_line, price_label, + group, legend_symbol, price_scale_id, crosshair_marker=True): super().__init__(chart, name) self.color = color - self.group = group # Store group for potential internal use + self.group = group # Store group for legend grouping + self.legend_symbol = legend_symbol # Store the legend symbol - # Pass group as part of the options if createLineSeries handles removing it + # Initialize series with configuration options self.run_script(f''' {self.id} = {self._chart.id}.createLineSeries( "{name}", {{ - group: '{group}', + group: '{group}', color: '{color}', lineStyle: {as_enum(style, LINE_STYLE)}, lineWidth: {width}, lastValueVisible: {jbool(price_label)}, priceLineVisible: {jbool(price_line)}, crosshairMarkerVisible: {jbool(crosshair_marker)}, + legendSymbol: '{legend_symbol}', priceScaleId: {f'"{price_scale_id}"' if price_scale_id else 'undefined'} {"""autoscaleInfoProvider: () => ({ priceRange: { @@ -453,8 +456,6 @@ def __init__(self, chart, name, color, style, width, price_line, price_label, gr }} ) null''') - - # def _set_trend(self, start_time, start_value, end_time, end_value, ray=False, round=False): # if round: # start_time = self._single_datetime_format(start_time) # end_time = self._single_datetime_format(end_time) @@ -489,18 +490,24 @@ def delete(self): class Histogram(SeriesCommon): - def __init__(self, chart, name, color, price_line, price_label, scale_margin_top, scale_margin_bottom): + def __init__( + self, chart, name, color, price_line, price_label, group, legend_symbol, scale_margin_top, scale_margin_bottom): super().__init__(chart, name) self.color = color + self.group = group # Store group for legend grouping + self.legend_symbol = legend_symbol # Store legend symbol + self.run_script(f''' {self.id} = {chart.id}.createHistogramSeries( "{name}", {{ + group: '{group}', color: '{color}', lastValueVisible: {jbool(price_label)}, priceLineVisible: {jbool(price_line)}, + legendSymbol: '{legend_symbol}', priceScaleId: '{self.id}', - priceFormat: {{type: "volume"}}, + priceFormat: {{type: "volume"}} }}, // precision: 2, ) @@ -534,17 +541,21 @@ def scale(self, scale_margin_top: float = 0.0, scale_margin_bottom: float = 0.0) class Area(SeriesCommon): - def __init__(self, chart, name, top_color, bottom_color, invert = False, line_color = '#FFFFFF', style='solid', width=1, price_line=True, price_label=False, price_scale_id=None, crosshair_marker=True): - - super().__init__(chart, name) + def __init__( + self, chart, name, top_color, bottom_color, invert, line_color, + style, width, price_line, price_label, group, legend_symbol, price_scale_id, crosshair_marker=True): + super().__init__(chart, name) self.color = line_color self.topColor = top_color self.bottomColor = bottom_color + self.group = group # Store group for legend grouping + self.legend_symbol = legend_symbol # Store legend symbol self.run_script(f''' {self.id} = {self._chart.id}.createAreaSeries( "{name}", {{ + group: '{group}', topColor: '{top_color}', bottomColor: '{bottom_color}', invertFilledArea: {jbool(invert)}, @@ -555,6 +566,7 @@ def __init__(self, chart, name, top_color, bottom_color, invert = False, line_co lastValueVisible: {jbool(price_label)}, priceLineVisible: {jbool(price_line)}, crosshairMarkerVisible: {jbool(crosshair_marker)}, + legendSymbol: '{legend_symbol}', priceScaleId: {f'"{price_scale_id}"' if price_scale_id else 'undefined'} {"""autoscaleInfoProvider: () => ({ priceRange: { @@ -584,16 +596,22 @@ def delete(self): delete {self.id} ''') + class Bar(SeriesCommon): - def __init__(self, chart, name, up_color='#26a69a', down_color='#ef5350', open_visible=True, thin_bars=True, price_line=True, price_label=False, price_scale_id=None): + def __init__( + self, chart, name, up_color, down_color, open_visible, thin_bars, + price_line, price_label, group, legend_symbol, price_scale_id): super().__init__(chart, name) self.up_color = up_color self.down_color = down_color + self.group = group # Store group for legend grouping + self.legend_symbol = legend_symbol if isinstance(legend_symbol, list) else [legend_symbol, legend_symbol] # Store legend symbols self.run_script(f''' {self.id} = {chart.id}.createBarSeries( "{name}", {{ + group: '{group}', color: '{up_color}', upColor: '{up_color}', downColor: '{down_color}', @@ -601,8 +619,10 @@ def __init__(self, chart, name, up_color='#26a69a', down_color='#ef5350', open_v thinBars: {jbool(thin_bars)}, lastValueVisible: {jbool(price_label)}, priceLineVisible: {jbool(price_line)}, + legendSymbol: {json.dumps(self.legend_symbol)}, priceScaleId: {f'"{price_scale_id}"' if price_scale_id else 'undefined'} }} + )''') def set(self, df: Optional[pd.DataFrame] = None): if df is None or df.empty: @@ -644,6 +664,7 @@ def delete(self): delete {self.id}legendItem delete {self.id} ''') + class Candlestick(SeriesCommon): def __init__(self, chart: 'AbstractChart'): @@ -835,59 +856,117 @@ def fit(self): """ self.run_script(f'{self.id}.chart.timeScale().fitContent()') - def create_line( - self, name: str = '', color: str = 'rgba(214, 237, 255, 0.6)', - style: LINE_STYLE = 'solid', width: int = 2, - price_line: bool = True, price_label: bool = True, group: str = '', - price_scale_id: Optional[str] =None - ) -> Line: + def create_line( + self, + name: str = '', + color: str = 'rgba(214, 237, 255, 0.6)', + style: LINE_STYLE = 'solid', + width: int = 2, + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: str = '', + price_scale_id: Optional[str] = None + ) -> Line: """ Creates and returns a Line object. """ - self._lines.append(Line(self, name, color, style, width, price_line, price_label, group, price_scale_id )) - return self._lines[-1] + + symbol_styles = { + 'solid':'―', + 'dotted':'··', + 'dashed':'--', + 'large_dashed':'- -', + 'sparse_dotted':"· ·", + } + if legend_symbol == '': + legend_symbol = symbol_styles.get(style, '━') # Default to 'solid' if style is unrecognized + if not isinstance(legend_symbol, str): + raise TypeError("legend_symbol must be a string for Line series.") + + self._lines.append(Line( + self, name, color, style, width, price_line, price_label, + group, legend_symbol, price_scale_id + )) + return self._lines[-1] def create_histogram( - self, name: str = '', color: str = 'rgba(214, 237, 255, 0.6)', - price_line: bool = True, price_label: bool = True, - scale_margin_top: float = 0.0, scale_margin_bottom: float = 0.0 - ) -> Histogram: + self, + name: str = '', + color: str = 'rgba(214, 237, 255, 0.6)', + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: str = '▥', + scale_margin_top: float = 0.0, + scale_margin_bottom: float = 0.0 + ) -> Histogram: """ Creates and returns a Histogram object. """ + if not isinstance(legend_symbol, str): + raise TypeError("legend_symbol must be a string for Histogram series.") + return Histogram( - self, name, color, price_line, price_label, - scale_margin_top, scale_margin_bottom) + self, name, color, price_line, price_label, + group, legend_symbol, scale_margin_top, scale_margin_bottom + ) - def create_area( - self, name: str = '', top_color: str ='rgba(0, 100, 0, 0.5)', - bottom_color: str ='rgba(138, 3, 3, 0.5)',invert: bool = False, color: str ='rgba(0,0,255,1)', - style: LINE_STYLE = 'solid', width: int = 2, price_line: bool = True, price_label: bool = True, + self, + name: str = '', + top_color: str = 'rgba(0, 100, 0, 0.5)', + bottom_color: str = 'rgba(138, 3, 3, 0.5)', + invert: bool = False, + color: str = 'rgba(0,0,255,1)', + style: LINE_STYLE = 'solid', + width: int = 2, + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: str = '◪', price_scale_id: Optional[str] = None - ) -> Area: + ) -> Area: """ Creates and returns an Area object. """ - self._lines.append(Area(self, name, top_color, bottom_color, invert, color, style, - width, price_line, price_label, price_scale_id)) - + if not isinstance(legend_symbol, str): + raise TypeError("legend_symbol must be a string for Area series.") + + self._lines.append(Area( + self, name, top_color, bottom_color, invert, color, style, + width, price_line, price_label, group, legend_symbol, price_scale_id + )) return self._lines[-1] - def create_bar( - self, name: str = '', up_color: str = '#26a69a', down_color: str = '#ef5350', - open_visible: bool = True, thin_bars: bool = True, - price_line: bool = True, price_label: bool = True, + self, + name: str = '', + up_color: str = '#26a69a', + down_color: str = '#ef5350', + open_visible: bool = True, + thin_bars: bool = True, + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: Union[str, List[str]] = ['┌', '└'], price_scale_id: Optional[str] = None ) -> Bar: """ Creates and returns a Bar object. """ + if not isinstance(legend_symbol, (str, list)): + raise TypeError("legend_symbol must be a string or list of strings for Bar series.") + if isinstance(legend_symbol, list) and not all(isinstance(symbol, str) for symbol in legend_symbol): + raise TypeError("Each item in legend_symbol list must be a string for Bar series.") + return Bar( - self, name, up_color, down_color, open_visible, thin_bars, - price_line, price_label, price_scale_id) + self, name, up_color, down_color, open_visible, thin_bars, + price_line, price_label, group, legend_symbol, price_scale_id + ) + + def lines(self) -> List[Line]: From 5d54397af42460645f7039e002cb38596ea48f61 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:34:35 -0700 Subject: [PATCH 13/89] Add legend_symbol, group to series types Add legend_symbol, group to series types --- src/general/legend.ts | 265 +++++++++++++++++++++++++++++++----------- 1 file changed, 200 insertions(+), 65 deletions(-) diff --git a/src/general/legend.ts b/src/general/legend.ts index 2be2486..0296d6b 100644 --- a/src/general/legend.ts +++ b/src/general/legend.ts @@ -1,4 +1,4 @@ -import { ISeriesApi, LineData, Logical, MouseEventParams, PriceFormatBuiltIn, SeriesType } from "lightweight-charts"; +import {AreaData, BarData, HistogramData, ISeriesApi, LineData, Logical, MouseEventParams, PriceFormatBuiltIn, PriceFormat, SeriesType } from "lightweight-charts"; import { Handler } from "./handler"; // Interfaces for the legend elements @@ -9,8 +9,10 @@ interface LineElement { toggle: HTMLDivElement; series: ISeriesApi; solid: string; + legendSymbol: string; // Add legend symbol for individual series } +// Interface for a group of series in the legend interface LegendGroup { name: string; seriesList: ISeriesApi[]; @@ -19,6 +21,7 @@ interface LegendGroup { toggle: HTMLDivElement; solidColors: string[]; names: string[]; + legendSymbols: string[]; // Add array of legend symbols for grouped series } // Define the SVG path data const openEye = ` @@ -110,97 +113,144 @@ export class Legend { absNum >= 1000 ? (num / 1000).toFixed(1) + 'K' : num.toString().padStart(8, ' '); } - - makeSeriesRow(name: string, series: ISeriesApi): HTMLDivElement { + makeSeriesRow( + name: string, + series: ISeriesApi, + legendSymbol: string[] = ['▨'], + colors: string[] + ): HTMLDivElement { const row = document.createElement('div'); row.style.display = 'flex'; row.style.alignItems = 'center'; const div = document.createElement('div'); - div.innerText = name; + // Iterate over colors and symbols for multi-color support + div.innerHTML = legendSymbol + .map((symbol, index) => `${symbol}`) + .join(' ') + ` ${name}`; const toggle = document.createElement('div'); toggle.classList.add('legend-toggle-switch'); - const color = (series.options() as any).color || 'rgba(255,0,0,1)'; // Use a default color - const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; - const onIcon = this.createSvgIcon(openEye); const offIcon = this.createSvgIcon(closedEye); - toggle.appendChild(onIcon.cloneNode(true)); // Clone nodes to avoid duplication - + toggle.appendChild(onIcon.cloneNode(true)); + let visible = true; toggle.addEventListener('click', () => { visible = !visible; series.applyOptions({ visible }); - toggle.innerHTML = ''; // Clear current icon + toggle.innerHTML = ''; toggle.appendChild(visible ? onIcon.cloneNode(true) : offIcon.cloneNode(true)); }); row.appendChild(div); row.appendChild(toggle); + this.seriesContainer.appendChild(row); + // Push the row and related information to the `_lines` array this._lines.push({ name, div, row, toggle, series, - solid: solidColor, + solid: colors[0], // Assume the first color is the main color for the series + legendSymbol: legendSymbol[0], // Store the primary legend symbol }); - this.seriesContainer.appendChild(row); - + return row; } - makeSeriesGroup(groupName: string, names: string[], seriesList: ISeriesApi[], solidColors: string[]) { + + makeSeriesGroup( + groupName: string, + names: string[], + seriesList: ISeriesApi[], + colors: string[], + legendSymbols: string[] + ): HTMLDivElement { const row = document.createElement('div'); row.style.display = 'flex'; row.style.alignItems = 'center'; const div = document.createElement('div'); - div.style.color = '#FFF'; // Keep group name text in white - div.innerText = `${groupName}:`; + div.innerHTML = `${groupName}:`; const toggle = document.createElement('div'); toggle.classList.add('legend-toggle-switch'); const onIcon = this.createSvgIcon(openEye); const offIcon = this.createSvgIcon(closedEye); - toggle.appendChild(onIcon.cloneNode(true)); // Default to visible + toggle.appendChild(onIcon.cloneNode(true)); // Default to visible let visible = true; toggle.addEventListener('click', () => { visible = !visible; seriesList.forEach(series => series.applyOptions({ visible })); - toggle.innerHTML = ''; // Clear toggle before appending new icon + toggle.innerHTML = ''; // Clear toggle before appending new icon toggle.appendChild(visible ? onIcon.cloneNode(true) : offIcon.cloneNode(true)); }); - // Build the legend text with only colored squares and regular-weight line names - let legendText = `${groupName}:`; + // Build the legend content for each series in the group + let colorIndex = 0; // Separate index for colors and symbols to account for bar pairs names.forEach((name, index) => { - const color = solidColors[index]; - legendText += ` ${name}: -`; + const series = seriesList[index]; + const isBarSeries = series && series.seriesType() === 'Bar'; + + if (isBarSeries) { + // Use current color index for the up symbol/color, then increment to down symbol/color + const upSymbol = legendSymbols[colorIndex] || '▨'; + const downSymbol = legendSymbols[colorIndex + 1] || '▨'; + const upColor = colors[colorIndex]; + const downColor = colors[colorIndex + 1]; + + // Dual symbol and color formatting for bar series + div.innerHTML += ` + ${upSymbol} + ${downSymbol} + ${name}: - + `; + + // Increment color index by 2 for bar series (to account for up/down pair) + colorIndex += 2; + } else { + // Single symbol and color for non-bar series + const singleSymbol = legendSymbols[colorIndex] || '▨'; + const singleColor = colors[colorIndex]; + + div.innerHTML += ` + ${singleSymbol} + ${name}: - + `; + + // Increment color index by 1 for non-bar series + colorIndex += 1; + } }); - div.innerHTML = legendText; // Set HTML content to maintain colored squares and regular font for line names + // Add div and toggle to row + row.appendChild(div); + row.appendChild(toggle); + + // Append row to the series container + this.seriesContainer.appendChild(row); + // Store group data this._groups.push({ name: groupName, seriesList, div, row, toggle, - solidColors, + solidColors: colors, names, + legendSymbols, }); - row.appendChild(div); - row.appendChild(toggle); - this.seriesContainer.appendChild(row); return row; } + private createSvgIcon(svgContent: string): SVGElement { const tempContainer = document.createElement('div'); @@ -208,7 +258,6 @@ export class Legend { const svgElement = tempContainer.querySelector('svg'); return svgElement as SVGElement; } - legendHandler(param: MouseEventParams, usingPoint = false) { if (!this.ohlcEnabled && !this.linesEnabled && !this.percentEnabled) return; @@ -255,7 +304,7 @@ export class Legend { this.updateGroupLegend(param, logical, usingPoint); this.updateSeriesLegend(param, logical, usingPoint); } - + private updateGroupLegend(param: MouseEventParams, logical: Logical | null, usingPoint: boolean) { this._groups.forEach((group) => { if (!this.linesEnabled) { @@ -263,52 +312,138 @@ export class Legend { return; } group.row.style.display = 'flex'; - + + // Start building the legend text with the group name let legendText = `${group.name}:`; - group.seriesList.forEach((series, index) => { - const data = usingPoint && logical - ? series.dataByIndex(logical) as LineData - : param.seriesData.get(series) as LineData; - - if (!data?.value) return; - - const priceFormat = series.options().priceFormat; - const price = 'precision' in priceFormat - ? this.legendItemFormat(data.value, (priceFormat as PriceFormatBuiltIn).precision) - : this.legendItemFormat(data.value, 2); // Default precision - - const color = group.solidColors ? group.solidColors[index] : 'inherit'; - const name = group.names[index]; - - // Include `price` in legendText - legendText += ` ${name}: ${price}`; + + // Track color index for bar-specific colors and symbols + let colorIndex = 0; + + // Iterate over each series in the group + group.seriesList.forEach((series, idx) => { + const seriesType = series.seriesType(); + let data; + + // Get data based on the current logical point or series data + if (usingPoint && logical) { + data = series.dataByIndex(logical); + } else { + data = param.seriesData.get(series); + } + + if (!data) return; // Skip if no data is available for this series + + // Retrieve price format for precision + const priceFormat = series.options().priceFormat as PriceFormatBuiltIn; + const name = group.names[idx]; + + if (seriesType === 'Bar') { + // Handle Bar series with open and close values and separate up/down symbols and colors + const barData = data as BarData; + const openPrice = this.legendItemFormat(barData.open, priceFormat.precision); + const closePrice = this.legendItemFormat(barData.close, priceFormat.precision); + + const upSymbol = group.legendSymbols[colorIndex] || '▨'; + const downSymbol = group.legendSymbols[colorIndex + 1] || '▨'; + const upColor = group.solidColors[colorIndex]; + const downColor = group.solidColors[colorIndex + 1]; + + // Append Bar series info with open and close prices, and separate symbols/colors + legendText += ` + ${upSymbol} + ${downSymbol} + ${name}: O ${openPrice}, C ${closePrice} + `; + + colorIndex += 2; // Increment color index by 2 for Bar series + } else { + // Handle other series types that use a single `value` + const otherData = data as LineData | AreaData | HistogramData; + const price = this.legendItemFormat(otherData.value, priceFormat.precision); + + const symbol = group.legendSymbols[colorIndex] || '▨'; + const color = group.solidColors[colorIndex]; + + // Append non-Bar series info with single symbol and color + legendText += ` + ${symbol} + ${name}: ${price} + `; + colorIndex += 1; // Increment color index by 1 for non-Bar series + } }); - + + // Update the group legend div with the constructed legend text group.div.innerHTML = legendText; }); } private updateSeriesLegend(param: MouseEventParams, logical: Logical | null, usingPoint: boolean) { - this._lines.forEach((line) => { + if (!this._lines || !this._lines.length) { + console.error("No lines available to update legend."); + return; + } + + this._lines.forEach((e) => { + // Check if the line row should be displayed if (!this.linesEnabled) { - line.row.style.display = 'none'; + e.row.style.display = 'none'; return; } - line.row.style.display = 'flex'; - - const data = usingPoint && logical - ? line.series.dataByIndex(logical) as LineData - : param.seriesData.get(line.series) as LineData; - - if (data?.value !== undefined) { - const priceFormat = line.series.options().priceFormat as PriceFormatBuiltIn; - const price = 'precision' in priceFormat - ? this.legendItemFormat(data.value, priceFormat.precision) - : this.legendItemFormat(data.value, 2); + e.row.style.display = 'flex'; + + // Determine series type and get the appropriate data + const seriesType = e.series.seriesType(); + let data; + + if (usingPoint && logical) { + data = e.series.dataByIndex(logical); + } else { + data = param.seriesData.get(e.series); + } + + // If no data is available, show a placeholder and continue + if (!data) { + e.div.innerHTML = `${e.name}: -`; + return; + } + + const priceFormat = e.series.options().priceFormat as PriceFormatBuiltIn; + let legendContent: string; + console.log(`Series: ${e.name}, Type: ${seriesType}, Data:`, data); - line.div.innerHTML = ` ${line.name}: ${price}`; + if (seriesType === 'Bar') { + // Handle Bar series with open and close values + const barData = data as BarData; + const openPrice = this.legendItemFormat(barData.open, priceFormat.precision); + const closePrice = this.legendItemFormat(barData.close, priceFormat.precision); + + // Use specific symbols and colors for Bar series open/close display + const upSymbol = e.legendSymbol[0] || '▨'; + const downSymbol = e.legendSymbol[1] || '▨'; + const upColor = e.solid[0]; + const downColor = e.solid[1]; + + legendContent = ` + ${upSymbol} + ${downSymbol} + ${e.name}: O ${openPrice}, C ${closePrice} + `; + } else if (seriesType === 'Histogram') { + // Handle Histogram with shorthand format + const histogramData = data as HistogramData; + const price = this.shorthandFormat(histogramData.value); + + legendContent = `${e.legendSymbol || '▨'} ${e.name}: ${price}`; } else { - line.div.innerHTML = `${line.name}: -`; + // Handle Line, Area, and other series types with a single value + const otherData = data as LineData | AreaData; + const price = this.legendItemFormat(otherData.value, priceFormat.precision); + + legendContent = `${e.legendSymbol || '▨'} ${e.name}: ${price}`; } + + // Update the legend row content + e.div.innerHTML = legendContent; }); - } -} + }} + From 3cd916632b8e47400374c506fb524bd928c0401f Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:35:22 -0700 Subject: [PATCH 14/89] Add legend_symbol, group to series types --- src/general/handler.ts | 158 ++++++++++++++++++++++++++++++----------- 1 file changed, 117 insertions(+), 41 deletions(-) diff --git a/src/general/handler.ts b/src/general/handler.ts index 8ed085e..6355b18 100644 --- a/src/general/handler.ts +++ b/src/general/handler.ts @@ -28,10 +28,28 @@ export interface Scale{ width: number, height: number, } +// Define specific options interfaces with optional group and legendSymbol properties +interface LineSeriesOptions extends DeepPartial { + group?: string; + legendSymbol?: string; +} + +interface HistogramSeriesOptions extends DeepPartial { + group?: string; + legendSymbol?: string; +} + +interface AreaSeriesOptions extends DeepPartial { + group?: string; + legendSymbol?: string; +} -interface MultiLineOptions extends DeepPartial { - group?: string; // Define group as an optional string identifier +interface BarSeriesOptions extends DeepPartial { + group?: string; + legendSymbol?: string[]; // Updated to an array of strings to support dual symbols } + + globalParamInit(); declare const window: GlobalParams; @@ -181,70 +199,128 @@ export class Handler { return volumeSeries; } - createLineSeries( + createLineSeries( name: string, - options: MultiLineOptions + options: LineSeriesOptions ): { name: string; series: ISeriesApi } { - const { group, ...lineOptions } = options; + const { group, legendSymbol = '▨', ...lineOptions } = options; const line = this.chart.addLineSeries(lineOptions); this._seriesList.push(line); - // Get color of the series for legend display - const color = line.options().color || 'rgba(255,0,0,1)'; // Default to red if no color is defined + const color = line.options().color || 'rgba(255,0,0,1)'; const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; if (!group || group === '') { - // No group: create a standalone series row - this.legend.makeSeriesRow(name, line); + this.legend.makeSeriesRow(name, line, [legendSymbol], [solidColor]); } else { - // Check if the group already exists const existingGroup = this.legend._groups.find(g => g.name === group); - if (existingGroup) { - // Group exists: add the new line's name and color to the `names` and `solidColors` arrays existingGroup.names.push(name); existingGroup.seriesList.push(line); - existingGroup.solidColors.push(solidColor); + existingGroup.solidColors.push(solidColor); // Single color + existingGroup.legendSymbols.push(legendSymbol); // Single symbol } else { - // Group does not exist: create a new one - this.legend.makeSeriesGroup(group, [name], [line], [solidColor]); + this.legend.makeSeriesGroup(group, [name], [line], [solidColor], [legendSymbol]); } } return { name, series: line }; } + createHistogramSeries( + name: string, + options: HistogramSeriesOptions + ): { name: string; series: ISeriesApi } { + const { group, legendSymbol = '▨', ...histogramOptions } = options; + const histogram = this.chart.addHistogramSeries(histogramOptions); + this._seriesList.push(histogram); - createHistogramSeries(name: string, options: DeepPartial) { - const line = this.chart.addHistogramSeries({...options}); - this._seriesList.push(line); - this.legend.makeSeriesRow(name, line) - return { - name: name, - series: line, + const color = histogram.options().color || 'rgba(255,0,0,1)'; + const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; + + if (!group || group === '') { + this.legend.makeSeriesRow(name, histogram, [legendSymbol], [solidColor]); + } else { + const existingGroup = this.legend._groups.find(g => g.name === group); + if (existingGroup) { + existingGroup.names.push(name); + existingGroup.seriesList.push(histogram); + existingGroup.solidColors.push(solidColor); // Single color + existingGroup.legendSymbols.push(legendSymbol); // Single symbol + } else { + this.legend.makeSeriesGroup(group, [name], [histogram], [solidColor], [legendSymbol]); + } } + + return { name, series: histogram }; } - createAreaSeries(name: string, options: DeepPartial) { - const line = this.chart.addAreaSeries({ ...options }); - this._seriesList.push(line); - this.legend.makeSeriesRow(name, line); - return { - name: name, - series: line, - }; + + createAreaSeries( + name: string, + options: AreaSeriesOptions + ): { name: string; series: ISeriesApi } { + const { group, legendSymbol = '▨', ...areaOptions } = options; + const area = this.chart.addAreaSeries(areaOptions); + this._seriesList.push(area); + + const color = area.options().color || 'rgba(255,0,0,1)'; + const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; + + if (!group || group === '') { + this.legend.makeSeriesRow(name, area, [legendSymbol], [solidColor]); + } else { + const existingGroup = this.legend._groups.find(g => g.name === group); + if (existingGroup) { + existingGroup.names.push(name); + existingGroup.seriesList.push(area); + existingGroup.solidColors.push(solidColor); // Single color + existingGroup.legendSymbols.push(legendSymbol); // Single symbol + } else { + this.legend.makeSeriesGroup(group, [name], [area], [solidColor], [legendSymbol]); + } + } + + return { name, series: area }; } - - createBarSeries(name: string, options: DeepPartial) { - const line = this.chart.addBarSeries({ ...options }); - this._seriesList.push(line); - this.legend.makeSeriesRow(name, line); - return { - name: name, - series: line, - }; + + createBarSeries( + name: string, + options: BarSeriesOptions + ): { name: string; series: ISeriesApi } { + const { group, legendSymbol = ['▨', '▨'], ...barOptions } = options; + const bar = this.chart.addBarSeries(barOptions); + this._seriesList.push(bar); + + // Extract upColor and downColor, with default values + const upColor = (bar.options() as any).upColor || 'rgba(0,255,0,1)'; // Default green + const downColor = (bar.options() as any).downColor || 'rgba(255,0,0,1)'; // Default red + + if (!group || group === '') { + // Pass both symbols and colors to makeSeriesRow for standalone bars + this.legend.makeSeriesRow(name, bar, legendSymbol, [upColor, downColor]); + } else { + const existingGroup = this.legend._groups.find(g => g.name === group); + if (existingGroup) { + existingGroup.names.push(name); + existingGroup.seriesList.push(bar); + existingGroup.solidColors.push(upColor, downColor); // Add both colors + existingGroup.legendSymbols.push(legendSymbol[0], legendSymbol[1]); // Add both symbols + } else { + this.legend.makeSeriesGroup( + group, + [name], + [bar], + [upColor, downColor], // Two colors for up/down + legendSymbol // Two symbols for up/down + ); + } + } + + return { name, series: bar }; } - - + + + createToolBox() { this.toolBox = new ToolBox(this.id, this.chart, this.series, this.commandFunctions); this.div.appendChild(this.toolBox.div); From d17365b964618aed6d126c6e0096572cbb7825c8 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Thu, 31 Oct 2024 05:17:41 -0700 Subject: [PATCH 15/89] Update bundle.js --- lightweight_charts/js/bundle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightweight_charts/js/bundle.js b/lightweight_charts/js/bundle.js index 730a0c7..3544737 100644 --- a/lightweight_charts/js/bundle.js +++ b/lightweight_charts/js/bundle.js @@ -1 +1 @@ -var Lib=function(t,e){"use strict";const i={backgroundColor:"#0c0d0f",hoverBackgroundColor:"#3c434c",clickBackgroundColor:"#50565E",activeBackgroundColor:"rgba(0, 122, 255, 0.7)",mutedBackgroundColor:"rgba(0, 122, 255, 0.3)",borderColor:"#3C434C",color:"#d8d9db",activeColor:"#ececed"};function s(){window.pane={...i},window.containerDiv=document.getElementById("container")||document.createElement("div"),window.setCursor=t=>{t&&(window.cursor=t),document.body.style.cursor=window.cursor},window.cursor="default",window.textBoxFocused=!1}const n='\n\n \n \n\n',o='\n\n \n \n \n\n';class r{handler;div;seriesContainer;ohlcEnabled=!1;percentEnabled=!1;linesEnabled=!1;colorBasedOnCandle=!1;text;candle;_lines=[];_groups=[];constructor(t){this.handler=t,this.div=document.createElement("div"),this.div.classList.add("legend"),this.seriesContainer=document.createElement("div"),this.text=document.createElement("span"),this.candle=document.createElement("div"),this.setupLegend(),this.legendHandler=this.legendHandler.bind(this),t.chart.subscribeCrosshairMove(this.legendHandler)}setupLegend(){this.div.style.maxWidth=100*this.handler.scale.width-8+"vw",this.div.style.display="none";const t=document.createElement("div");t.style.display="flex",t.style.flexDirection="row",this.seriesContainer.classList.add("series-container"),this.text.style.lineHeight="1.8",t.appendChild(this.seriesContainer),this.div.appendChild(this.text),this.div.appendChild(this.candle),this.div.appendChild(t),this.handler.div.appendChild(this.div)}legendItemFormat(t,e){return t.toFixed(e).toString().padStart(8," ")}shorthandFormat(t){const e=Math.abs(t);return e>=1e6?(t/1e6).toFixed(1)+"M":e>=1e3?(t/1e3).toFixed(1)+"K":t.toString().padStart(8," ")}makeSeriesRow(t,e){const i=document.createElement("div");i.style.display="flex",i.style.alignItems="center";const s=document.createElement("div");s.innerText=t;const r=document.createElement("div");r.classList.add("legend-toggle-switch");const a=e.options().color||"rgba(255,0,0,1)",l=a.startsWith("rgba")?a.replace(/[^,]+(?=\))/,"1"):a,d=this.createSvgIcon(n),h=this.createSvgIcon(o);r.appendChild(d.cloneNode(!0));let c=!0;return r.addEventListener("click",(()=>{c=!c,e.applyOptions({visible:c}),r.innerHTML="",r.appendChild(c?d.cloneNode(!0):h.cloneNode(!0))})),i.appendChild(s),i.appendChild(r),this._lines.push({name:t,div:s,row:i,toggle:r,series:e,solid:l}),this.seriesContainer.appendChild(i),i}makeSeriesGroup(t,e,i,s){const r=document.createElement("div");r.style.display="flex",r.style.alignItems="center";const a=document.createElement("div");a.style.color="#FFF",a.innerText=`${t}:`;const l=document.createElement("div");l.classList.add("legend-toggle-switch");const d=this.createSvgIcon(n),h=this.createSvgIcon(o);l.appendChild(d.cloneNode(!0));let c=!0;l.addEventListener("click",(()=>{c=!c,i.forEach((t=>t.applyOptions({visible:c}))),l.innerHTML="",l.appendChild(c?d.cloneNode(!0):h.cloneNode(!0))}));let p=`${t}:`;return e.forEach(((t,e)=>{const i=s[e];p+=` ${t}: -`})),a.innerHTML=p,this._groups.push({name:t,seriesList:i,div:a,row:r,toggle:l,solidColors:s,names:e}),r.appendChild(a),r.appendChild(l),this.seriesContainer.appendChild(r),r}createSvgIcon(t){const e=document.createElement("div");e.innerHTML=t.trim();return e.querySelector("svg")}legendHandler(t,e=!1){if(!this.ohlcEnabled&&!this.linesEnabled&&!this.percentEnabled)return;const i=this.handler.series.options();if(!t.time)return this.candle.style.color="transparent",void(this.candle.innerHTML=this.candle.innerHTML.replace(i.upColor,"").replace(i.downColor,""));let s,n=null;if(e){const e=this.handler.chart.timeScale(),i=e.timeToCoordinate(t.time);i&&(n=e.coordinateToLogical(i.valueOf())),n&&(s=this.handler.series.dataByIndex(n.valueOf()))}else s=t.seriesData.get(this.handler.series);let o='';if(s&&(this.ohlcEnabled&&(o+=`O ${this.legendItemFormat(s.open,this.handler.precision)} `,o+=`| H ${this.legendItemFormat(s.high,this.handler.precision)} `,o+=`| L ${this.legendItemFormat(s.low,this.handler.precision)} `,o+=`| C ${this.legendItemFormat(s.close,this.handler.precision)} `),this.percentEnabled)){const t=(s.close-s.open)/s.open*100,e=t>0?i.upColor:i.downColor,n=`${t>=0?"+":""}${t.toFixed(2)} %`;o+=this.colorBasedOnCandle?`| ${n}`:`| ${n}`}this.candle.innerHTML=o+"",this.updateGroupLegend(t,n,e),this.updateSeriesLegend(t,n,e)}updateGroupLegend(t,e,i){this._groups.forEach((s=>{if(!this.linesEnabled)return void(s.row.style.display="none");s.row.style.display="flex";let n=`${s.name}:`;s.seriesList.forEach(((o,r)=>{const a=i&&e?o.dataByIndex(e):t.seriesData.get(o);if(!a?.value)return;const l=o.options().priceFormat,d="precision"in l?this.legendItemFormat(a.value,l.precision):this.legendItemFormat(a.value,2),h=s.solidColors?s.solidColors[r]:"inherit",c=s.names[r];n+=` ${c}: ${d}`})),s.div.innerHTML=n}))}updateSeriesLegend(t,e,i){this._lines.forEach((s=>{if(!this.linesEnabled)return void(s.row.style.display="none");s.row.style.display="flex";const n=i&&e?s.series.dataByIndex(e):t.seriesData.get(s.series);if(void 0!==n?.value){const t=s.series.options().priceFormat,e="precision"in t?this.legendItemFormat(n.value,t.precision):this.legendItemFormat(n.value,2);s.div.innerHTML=` ${s.name}: ${e}`}else s.div.innerHTML=`${s.name}: -`}))}}function a(t){if(void 0===t)throw new Error("Value is undefined");return t}class l{_chart=void 0;_series=void 0;requestUpdate(){this._requestUpdate&&this._requestUpdate()}_requestUpdate;attached({chart:t,series:e,requestUpdate:i}){this._chart=t,this._series=e,this._series.subscribeDataChanged(this._fireDataUpdated),this._requestUpdate=i,this.requestUpdate()}detached(){this._chart=void 0,this._series=void 0,this._requestUpdate=void 0}get chart(){return a(this._chart)}get series(){return a(this._series)}_fireDataUpdated(t){this.dataUpdated&&this.dataUpdated(t)}}const d={lineColor:"#1E80F0",lineStyle:e.LineStyle.Solid,width:4};var h;!function(t){t[t.NONE=0]="NONE",t[t.HOVERING=1]="HOVERING",t[t.DRAGGING=2]="DRAGGING",t[t.DRAGGINGP1=3]="DRAGGINGP1",t[t.DRAGGINGP2=4]="DRAGGINGP2",t[t.DRAGGINGP3=5]="DRAGGINGP3",t[t.DRAGGINGP4=6]="DRAGGINGP4"}(h||(h={}));class c extends l{_paneViews=[];_options;_points=[];_state=h.NONE;_startDragPoint=null;_latestHoverPoint=null;static _mouseIsDown=!1;static hoveredObject=null;static lastHoveredObject=null;_listeners=[];constructor(t){super(),this._options={...d,...t}}updateAllViews(){this._paneViews.forEach((t=>t.update()))}paneViews(){return this._paneViews}applyOptions(t){this._options={...this._options,...t},this.requestUpdate()}updatePoints(...t){for(let e=0;ei.name===t&&i.listener===e));this._listeners.splice(this._listeners.indexOf(i),1)}_handleHoverInteraction(t){if(this._latestHoverPoint=t.point,c._mouseIsDown)this._handleDragInteraction(t);else if(this._mouseIsOverDrawing(t)){if(this._state!=h.NONE)return;this._moveToState(h.HOVERING),c.hoveredObject=c.lastHoveredObject=this}else{if(this._state==h.NONE)return;this._moveToState(h.NONE),c.hoveredObject===this&&(c.hoveredObject=null)}}static _eventToPoint(t,e){if(!e||!t.point||!t.logical)return null;const i=e.coordinateToPrice(t.point.y);return null==i?null:{time:t.time||null,logical:t.logical,price:i.valueOf()}}static _getDiff(t,e){return{logical:t.logical-e.logical,price:t.price-e.price}}_addDiffToPoint(t,e,i){t&&(t.logical=t.logical+e,t.price=t.price+i,t.time=this.series.dataByIndex(t.logical)?.time||null)}_handleMouseDownInteraction=()=>{c._mouseIsDown=!0,this._onMouseDown()};_handleMouseUpInteraction=()=>{c._mouseIsDown=!1,this._moveToState(h.HOVERING)};_handleDragInteraction(t){if(this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1&&this._state!=h.DRAGGINGP2&&this._state!=h.DRAGGINGP3&&this._state!=h.DRAGGINGP4)return;const e=c._eventToPoint(t,this.series);if(!e)return;this._startDragPoint=this._startDragPoint||e;const i=c._getDiff(e,this._startDragPoint);this._onDrag(i),this.requestUpdate(),this._startDragPoint=e}}class p{_options;constructor(t){this._options=t}}class u extends p{_p1;_p2;_hovered;constructor(t,e,i,s){super(i),this._p1=t,this._p2=e,this._hovered=s}_getScaledCoordinates(t){return null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y?null:{x1:Math.round(this._p1.x*t.horizontalPixelRatio),y1:Math.round(this._p1.y*t.verticalPixelRatio),x2:Math.round(this._p2.x*t.horizontalPixelRatio),y2:Math.round(this._p2.y*t.verticalPixelRatio)}}_drawEndCircle(t,e,i){t.context.fillStyle="#000",t.context.beginPath(),t.context.arc(e,i,9,0,2*Math.PI),t.context.stroke(),t.context.fill()}}function _(t,i){const s={[e.LineStyle.Solid]:[],[e.LineStyle.Dotted]:[t.lineWidth,t.lineWidth],[e.LineStyle.Dashed]:[2*t.lineWidth,2*t.lineWidth],[e.LineStyle.LargeDashed]:[6*t.lineWidth,6*t.lineWidth],[e.LineStyle.SparseDotted]:[t.lineWidth,4*t.lineWidth]}[i];t.setLineDash(s)}class m extends p{_point={x:null,y:null};constructor(t,e){super(e),this._point=t}draw(t){t.useBitmapCoordinateSpace((t=>{if(null==this._point.y)return;const e=t.context,i=Math.round(this._point.y*t.verticalPixelRatio),s=this._point.x?this._point.x*t.horizontalPixelRatio:0;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,_(e,this._options.lineStyle),e.beginPath(),e.moveTo(s,i),e.lineTo(t.bitmapSize.width,i),e.stroke()}))}}class g{_source;constructor(t){this._source=t}}class v extends g{_p1={x:null,y:null};_p2={x:null,y:null};_source;constructor(t){super(t),this._source=t}update(){if(!this._source.p1||!this._source.p2)return;const t=this._source.series,e=t.priceToCoordinate(this._source.p1.price),i=t.priceToCoordinate(this._source.p2.price),s=this._getX(this._source.p1),n=this._getX(this._source.p2);this._p1={x:s,y:e},this._p2={x:n,y:i}}_getX(t){return this._source.chart.timeScale().logicalToCoordinate(t.logical)}}class w extends g{_source;_point={x:null,y:null};constructor(t){super(t),this._source=t}update(){const t=this._source._point,e=this._source.chart.timeScale(),i=this._source.series;"RayLine"==this._source._type&&(this._point.x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical)),this._point.y=i.priceToCoordinate(t.price)}renderer(){return new m(this._point,this._source._options)}}class y{_source;_y=null;_price=null;constructor(t){this._source=t}update(){if(!this._source.series||!this._source._point)return;this._y=this._source.series.priceToCoordinate(this._source._point.price);const t=this._source.series.options().priceFormat.precision;this._price=this._source._point.price.toFixed(t).toString()}visible(){return!0}tickVisible(){return!0}coordinate(){return this._y??0}text(){return this._source._options.text||this._price||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class b extends c{_type="HorizontalLine";_paneViews;_point;_callbackName;_priceAxisViews;_startDragPoint=null;constructor(t,e,i=null){super(e),this._point=t,this._point.time=null,this._paneViews=[new w(this)],this._priceAxisViews=[new y(this)],this._callbackName=i}get points(){return[this._point]}updatePoints(...t){for(const e of t)e&&(this._point.price=e.price);this.requestUpdate()}updateAllViews(){this._paneViews.forEach((t=>t.update())),this._priceAxisViews.forEach((t=>t.update()))}priceAxisViews(){return this._priceAxisViews}_moveToState(t){switch(t){case h.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._addDiffToPoint(this._point,0,t.price),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.series.priceToCoordinate(this._point.price);return!!i&&Math.abs(i-t.point.y){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class x{_chart;_series;_finishDrawingCallback=null;_drawings=[];_activeDrawing=null;_isDrawing=!1;_drawingType=null;constructor(t,e,i=null){this._chart=t,this._series=e,this._finishDrawingCallback=i,this._chart.subscribeClick(this._clickHandler),this._chart.subscribeCrosshairMove(this._moveHandler)}_clickHandler=t=>this._onClick(t);_moveHandler=t=>this._onMouseMove(t);beginDrawing(t){this._drawingType=t,this._isDrawing=!0}stopDrawing(){this._isDrawing=!1,this._activeDrawing=null}get drawings(){return this._drawings}addNewDrawing(t){this._series.attachPrimitive(t),this._drawings.push(t)}delete(t){if(null==t)return;const e=this._drawings.indexOf(t);-1!=e&&(this._drawings.splice(e,1),t.detach())}clearDrawings(){for(const t of this._drawings)t.detach();this._drawings=[]}repositionOnTime(){for(const t of this.drawings){const e=[];for(const i of t.points){if(!i){e.push(i);continue}const t=i.time?this._chart.timeScale().coordinateToLogical(this._chart.timeScale().timeToCoordinate(i.time)||0):i.logical;e.push({time:i.time,logical:t,price:i.price})}t.updatePoints(...e)}}_onClick(t){if(!this._isDrawing)return;const e=c._eventToPoint(t,this._series);if(e)if(null==this._activeDrawing){if(null==this._drawingType)return;this._activeDrawing=new this._drawingType(e,e),this._series.attachPrimitive(this._activeDrawing),this._drawingType==b&&this._onClick(t)}else{if(this._drawings.push(this._activeDrawing),this.stopDrawing(),!this._finishDrawingCallback)return;this._finishDrawingCallback()}}_onMouseMove(t){if(!t)return;for(const e of this._drawings)e._handleHoverInteraction(t);if(!this._isDrawing||!this._activeDrawing)return;const e=c._eventToPoint(t,this._series);e&&this._activeDrawing.updatePoints(null,e)}}class C extends u{constructor(t,e,i,s){super(t,e,i,s)}draw(t){t.useBitmapCoordinateSpace((t=>{if(null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y)return;const e=t.context,i=this._getScaledCoordinates(t);i&&(e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,_(e,this._options.lineStyle),e.beginPath(),e.moveTo(i.x1,i.y1),e.lineTo(i.x2,i.y2),e.stroke(),this._hovered&&(this._drawEndCircle(t,i.x1,i.y1),this._drawEndCircle(t,i.x2,i.y2)))}))}}class f extends v{constructor(t){super(t)}renderer(){return new C(this._p1,this._p2,this._source._options,this._source.hovered)}}class D extends c{_paneViews=[];_hovered=!1;constructor(t,e,i){super(),this.points.push(t),this.points.push(e),this._options={...d,...i}}setFirstPoint(t){this.updatePoints(t)}setSecondPoint(t){this.updatePoints(null,t)}get p1(){return this.points[0]}get p2(){return this.points[1]}get hovered(){return this._hovered}}class k extends D{_type="TrendLine";constructor(t,e,i){super(t,e,i),this._paneViews=[new f(this)]}_moveToState(t){switch(t){case h.NONE:document.body.style.cursor="default",this._hovered=!1,this.requestUpdate(),this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this.requestUpdate(),this._subscribe("mousedown",this._handleMouseDownInteraction),this._unsubscribe("mouseup",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGINGP1:case h.DRAGGINGP2:case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1||this._addDiffToPoint(this.p1,t.logical,t.price),this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP2||this._addDiffToPoint(this.p2,t.logical,t.price)}_onMouseDown(){this._startDragPoint=null;const t=this._latestHoverPoint;if(!t)return;const e=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(e.x&&i.x&&e.y&&i.y))return this._moveToState(h.DRAGGING);Math.abs(t.x-e.x)<10&&Math.abs(t.y-e.y)<10?this._moveToState(h.DRAGGINGP1):Math.abs(t.x-i.x)<10&&Math.abs(t.y-i.y)<10?this._moveToState(h.DRAGGINGP2):this._moveToState(h.DRAGGING)}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this._paneViews[0]._p1.x,s=this._paneViews[0]._p1.y,n=this._paneViews[0]._p2.x,o=this._paneViews[0]._p2.y;if(!(i&&n&&s&&o))return!1;const r=t.point.x,a=t.point.y;if(r<=Math.min(i,n)-e||r>=Math.max(i,n)+e)return!1;return Math.abs((o-s)*r-(n-i)*a+n*s-o*i)/Math.sqrt((o-s)**2+(n-i)**2)<=e}}class L extends u{constructor(t,e,i,s){super(t,e,i,s)}draw(t){t.useBitmapCoordinateSpace((t=>{const e=t.context,i=this._getScaledCoordinates(t);if(!i)return;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,_(e,this._options.lineStyle),e.fillStyle=this._options.fillColor;const s=Math.min(i.x1,i.x2),n=Math.min(i.y1,i.y2),o=Math.abs(i.x1-i.x2),r=Math.abs(i.y1-i.y2);e.strokeRect(s,n,o,r),e.fillRect(s,n,o,r),this._hovered&&(this._drawEndCircle(t,s,n),this._drawEndCircle(t,s+o,n),this._drawEndCircle(t,s+o,n+r),this._drawEndCircle(t,s,n+r))}))}}class E extends v{constructor(t){super(t)}renderer(){return new L(this._p1,this._p2,this._source._options,this._source.hovered)}}const S={fillEnabled:!0,fillColor:"rgba(255, 255, 255, 0.2)",...d};class G extends D{_type="Box";constructor(t,e,i){super(t,e,i),this._options={...S,...i},this._paneViews=[new E(this)]}_moveToState(t){switch(t){case h.NONE:document.body.style.cursor="default",this._hovered=!1,this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this._unsubscribe("mouseup",this._handleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGINGP1:case h.DRAGGINGP2:case h.DRAGGINGP3:case h.DRAGGINGP4:case h.DRAGGING:document.body.style.cursor="grabbing",document.body.addEventListener("mouseup",this._handleMouseUpInteraction),this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1||this._addDiffToPoint(this.p1,t.logical,t.price),this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP2||this._addDiffToPoint(this.p2,t.logical,t.price),this._state!=h.DRAGGING&&(this._state==h.DRAGGINGP3&&(this._addDiffToPoint(this.p1,t.logical,0),this._addDiffToPoint(this.p2,0,t.price)),this._state==h.DRAGGINGP4&&(this._addDiffToPoint(this.p1,0,t.price),this._addDiffToPoint(this.p2,t.logical,0)))}_onMouseDown(){this._startDragPoint=null;const t=this._latestHoverPoint,e=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(e.x&&i.x&&e.y&&i.y))return this._moveToState(h.DRAGGING);const s=10;Math.abs(t.x-e.x)l-p&&rd-p&&ai.appendChild(this.makeColorBox(t))));let s=document.createElement("div");s.style.backgroundColor=window.pane.borderColor,s.style.height="1px",s.style.width="130px";let n=document.createElement("div");n.style.margin="10px";let o=document.createElement("div");o.style.color="lightgray",o.style.fontSize="12px",o.innerText="Opacity",this._opacityLabel=document.createElement("div"),this._opacityLabel.style.color="lightgray",this._opacityLabel.style.fontSize="12px",this._opacitySlider=document.createElement("input"),this._opacitySlider.type="range",this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%",this._opacitySlider.oninput=()=>{this._opacityLabel.innerText=this._opacitySlider.value+"%",this.opacity=parseInt(this._opacitySlider.value)/100,this.updateColor()},n.appendChild(o),n.appendChild(this._opacitySlider),n.appendChild(this._opacityLabel),this._div.appendChild(i),this._div.appendChild(s),this._div.appendChild(n),window.containerDiv.appendChild(this._div)}_updateOpacitySlider(){this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%"}makeColorBox(t){const e=document.createElement("div");e.style.width="18px",e.style.height="18px",e.style.borderRadius="3px",e.style.margin="3px",e.style.boxSizing="border-box",e.style.backgroundColor=t,e.addEventListener("mouseover",(()=>e.style.border="2px solid lightgray")),e.addEventListener("mouseout",(()=>e.style.border="none"));const i=I.extractRGBA(t);return e.addEventListener("click",(()=>{this.rgba=i,this.updateColor()})),e}static extractRGBA(t){const e=document.createElement("div");e.style.color=t,document.body.appendChild(e);const i=getComputedStyle(e).color;document.body.removeChild(e);const s=i.match(/\d+/g)?.map(Number);if(!s)return[];let n=i.includes("rgba")?parseFloat(i.split(",")[3]):1;return[s[0],s[1],s[2],n]}updateColor(){if(!c.lastHoveredObject||!this.rgba)return;const t=`rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`;c.lastHoveredObject.applyOptions({[this.colorOption]:t}),this.saveDrawings()}openMenu(t){c.lastHoveredObject&&(this.rgba=I.extractRGBA(c.lastHoveredObject._options[this.colorOption]),this.opacity=this.rgba[3],this._updateOpacitySlider(),this._div.style.top=t.top-30+"px",this._div.style.left=t.right+"px",this._div.style.display="flex",setTimeout((()=>document.addEventListener("mousedown",(t=>{this._div.contains(t.target)||this.closeMenu()}))),10))}closeMenu(){document.body.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}class T{static _styles=[{name:"Solid",var:e.LineStyle.Solid},{name:"Dotted",var:e.LineStyle.Dotted},{name:"Dashed",var:e.LineStyle.Dashed},{name:"Large Dashed",var:e.LineStyle.LargeDashed},{name:"Sparse Dotted",var:e.LineStyle.SparseDotted}];_div;_saveDrawings;constructor(t){this._saveDrawings=t,this._div=document.createElement("div"),this._div.classList.add("context-menu"),T._styles.forEach((t=>{this._div.appendChild(this._makeTextBox(t.name,t.var))})),window.containerDiv.appendChild(this._div)}_makeTextBox(t,e){const i=document.createElement("span");return i.classList.add("context-menu-item"),i.innerText=t,i.addEventListener("click",(()=>{c.lastHoveredObject?.applyOptions({lineStyle:e}),this._saveDrawings()})),i}openMenu(t){this._div.style.top=t.top-30+"px",this._div.style.left=t.right+"px",this._div.style.display="block",setTimeout((()=>document.addEventListener("mousedown",(t=>{this._div.contains(t.target)||this.closeMenu()}))),10)}closeMenu(){document.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}function M(t){const e=[];for(const i of t)0==e.length?e.push(i.toUpperCase()):i==i.toUpperCase()?e.push(" "+i):e.push(i);return e.join("")}class N{saveDrawings;drawingTool;div;hoverItem;items=[];constructor(t,e){this.saveDrawings=t,this.drawingTool=e,this._onRightClick=this._onRightClick.bind(this),this.div=document.createElement("div"),this.div.classList.add("context-menu"),document.body.appendChild(this.div),this.hoverItem=null,document.body.addEventListener("contextmenu",this._onRightClick)}_handleClick=t=>this._onClick(t);_onClick(t){t.target&&(this.div.contains(t.target)||(this.div.style.display="none",document.body.removeEventListener("click",this._handleClick)))}_onRightClick(t){if(!c.hoveredObject)return;for(const t of this.items)this.div.removeChild(t);this.items=[];for(const t of Object.keys(c.hoveredObject._options)){let e;if(t.toLowerCase().includes("color"))e=new I(this.saveDrawings,t);else{if("lineStyle"!==t)continue;e=new T(this.saveDrawings)}let i=t=>e.openMenu(t);this.menuItem(M(t),i,(()=>{document.removeEventListener("click",e.closeMenu),e._div.style.display="none"}))}this.separator(),this.menuItem("Delete Drawing",(()=>this.drawingTool.delete(c.lastHoveredObject))),t.preventDefault(),this.div.style.left=t.clientX+"px",this.div.style.top=t.clientY+"px",this.div.style.display="block",document.body.addEventListener("click",this._handleClick)}menuItem(t,e,i=null){const s=document.createElement("span");s.classList.add("context-menu-item"),this.div.appendChild(s);const n=document.createElement("span");if(n.innerText=t,n.style.pointerEvents="none",s.appendChild(n),i){let t=document.createElement("span");t.innerText="►",t.style.fontSize="8px",t.style.pointerEvents="none",s.appendChild(t)}if(s.addEventListener("mouseover",(()=>{this.hoverItem&&this.hoverItem.closeAction&&this.hoverItem.closeAction(),this.hoverItem={elem:n,action:e,closeAction:i}})),i){let t;s.addEventListener("mouseover",(()=>t=setTimeout((()=>e(s.getBoundingClientRect())),100))),s.addEventListener("mouseout",(()=>clearTimeout(t)))}else s.addEventListener("click",(t=>{e(t),this.div.style.display="none"}));this.items.push(s)}separator(){const t=document.createElement("div");t.style.width="90%",t.style.height="1px",t.style.margin="3px 0px",t.style.backgroundColor=window.pane.borderColor,this.div.appendChild(t),this.items.push(t)}}class R extends b{_type="RayLine";constructor(t,e){super({...t},e),this._point.time=t.time}updatePoints(...t){for(const e of t)e&&(this._point=e);this.requestUpdate()}_onDrag(t){this._addDiffToPoint(this._point,t.logical,t.price),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.series.priceToCoordinate(this._point.price),s=this._point.time?this.chart.timeScale().timeToCoordinate(this._point.time):null;return!(!i||!s)&&(Math.abs(i-t.point.y)s-e)}}class B extends p{_point={x:null,y:null};constructor(t,e){super(e),this._point=t}draw(t){t.useBitmapCoordinateSpace((t=>{if(null==this._point.x)return;const e=t.context,i=this._point.x*t.horizontalPixelRatio;e.lineWidth=this._options.width,e.strokeStyle=this._options.lineColor,_(e,this._options.lineStyle),e.beginPath(),e.moveTo(i,0),e.lineTo(i,t.bitmapSize.height),e.stroke()}))}}class A extends g{_source;_point={x:null,y:null};constructor(t){super(t),this._source=t}update(){const t=this._source._point,e=this._source.chart.timeScale(),i=this._source.series;this._point.x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical),this._point.y=i.priceToCoordinate(t.price)}renderer(){return new B(this._point,this._source._options)}}class P{_source;_x=null;constructor(t){this._source=t}update(){if(!this._source.chart||!this._source._point)return;const t=this._source._point,e=this._source.chart.timeScale();this._x=t.time?e.timeToCoordinate(t.time):e.logicalToCoordinate(t.logical)}visible(){return!!this._source._options.text}tickVisible(){return!0}coordinate(){return this._x??0}text(){return this._source._options.text||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class O extends c{_type="VerticalLine";_paneViews;_timeAxisViews;_point;_callbackName;_startDragPoint=null;constructor(t,e,i=null){super(e),this._point=t,this._paneViews=[new A(this)],this._callbackName=i,this._timeAxisViews=[new P(this)]}updateAllViews(){this._paneViews.forEach((t=>t.update())),this._timeAxisViews.forEach((t=>t.update()))}timeAxisViews(){return this._timeAxisViews}updatePoints(...t){for(const e of t)e&&(!e.time&&e.logical&&(e.time=this.series.dataByIndex(e.logical)?.time||null),this._point=e);this.requestUpdate()}get points(){return[this._point]}_moveToState(t){switch(t){case h.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=t}_onDrag(t){this._addDiffToPoint(this._point,t.logical,0),this.requestUpdate()}_mouseIsOverDrawing(t,e=4){if(!t.point)return!1;const i=this.chart.timeScale();let s;return s=this._point.time?i.timeToCoordinate(this._point.time):i.logicalToCoordinate(this._point.logical),!!s&&Math.abs(s-t.point.x){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class F{static TREND_SVG='';static HORZ_SVG='';static RAY_SVG='';static BOX_SVG='';static VERT_SVG=F.RAY_SVG;div;activeIcon=null;buttons=[];_commandFunctions;_handlerID;_drawingTool;constructor(t,e,i,s){this._handlerID=t,this._commandFunctions=s,this._drawingTool=new x(e,i,(()=>this.removeActiveAndSave())),this.div=this._makeToolBox(),new N(this.saveDrawings,this._drawingTool),s.push((t=>{if((t.metaKey||t.ctrlKey)&&"KeyZ"===t.code){const t=this._drawingTool.drawings.pop();return t&&this._drawingTool.delete(t),!0}return!1}))}toJSON(){const{...t}=this;return t}_makeToolBox(){let t=document.createElement("div");t.classList.add("toolbox"),this.buttons.push(this._makeToolBoxElement(k,"KeyT",F.TREND_SVG)),this.buttons.push(this._makeToolBoxElement(b,"KeyH",F.HORZ_SVG)),this.buttons.push(this._makeToolBoxElement(R,"KeyR",F.RAY_SVG)),this.buttons.push(this._makeToolBoxElement(G,"KeyB",F.BOX_SVG)),this.buttons.push(this._makeToolBoxElement(O,"KeyV",F.VERT_SVG,!0));for(const e of this.buttons)t.appendChild(e);return t}_makeToolBoxElement(t,e,i,s=!1){const n=document.createElement("div");n.classList.add("toolbox-button");const o=document.createElementNS("http://www.w3.org/2000/svg","svg");o.setAttribute("width","29"),o.setAttribute("height","29");const r=document.createElementNS("http://www.w3.org/2000/svg","g");r.innerHTML=i,r.setAttribute("fill",window.pane.color),o.appendChild(r),n.appendChild(o);const a={div:n,group:r,type:t};return n.addEventListener("click",(()=>this._onIconClick(a))),this._commandFunctions.push((t=>this._handlerID===window.handlerInFocus&&(!(!t.altKey||t.code!==e)&&(t.preventDefault(),this._onIconClick(a),!0)))),1==s&&(o.style.transform="rotate(90deg)",o.style.transformBox="fill-box",o.style.transformOrigin="center"),n}_onIconClick(t){this.activeIcon&&(this.activeIcon.div.classList.remove("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.stopDrawing(),this.activeIcon===t)?this.activeIcon=null:(this.activeIcon=t,this.activeIcon.div.classList.add("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.beginDrawing(this.activeIcon.type))}removeActiveAndSave=()=>{window.setCursor("default"),this.activeIcon&&this.activeIcon.div.classList.remove("active-toolbox-button"),this.activeIcon=null,this.saveDrawings()};addNewDrawing(t){this._drawingTool.addNewDrawing(t)}clearDrawings(){this._drawingTool.clearDrawings()}saveDrawings=()=>{const t=[];for(const e of this._drawingTool.drawings)t.push({type:e._type,points:e.points,options:e._options});const e=JSON.stringify(t);window.callbackFunction(`save_drawings${this._handlerID}_~_${e}`)};loadDrawings(t){t.forEach((t=>{switch(t.type){case"Box":this._drawingTool.addNewDrawing(new G(t.points[0],t.points[1],t.options));break;case"TrendLine":this._drawingTool.addNewDrawing(new k(t.points[0],t.points[1],t.options));break;case"HorizontalLine":this._drawingTool.addNewDrawing(new b(t.points[0],t.options));break;case"RayLine":this._drawingTool.addNewDrawing(new R(t.points[0],t.options));break;case"VerticalLine":this._drawingTool.addNewDrawing(new O(t.points[0],t.options))}}))}}class V{makeButton;callbackName;div;isOpen=!1;widget;constructor(t,e,i,s,n,o){this.makeButton=t,this.callbackName=e,this.div=document.createElement("div"),this.div.classList.add("topbar-menu"),this.widget=this.makeButton(s+" ↓",null,n,!0,o),this.updateMenuItems(i),this.widget.elem.addEventListener("click",(()=>{if(this.isOpen=!this.isOpen,!this.isOpen)return void(this.div.style.display="none");let t=this.widget.elem.getBoundingClientRect();this.div.style.display="flex",this.div.style.flexDirection="column";let e=t.x+t.width/2;this.div.style.left=e-this.div.clientWidth/2+"px",this.div.style.top=t.y+t.height+"px"})),document.body.appendChild(this.div)}updateMenuItems(t){this.div.innerHTML="",t.forEach((t=>{let e=this.makeButton(t,null,!1,!1);e.elem.addEventListener("click",(()=>{this._clickHandler(e.elem.innerText)})),e.elem.style.margin="4px 4px",e.elem.style.padding="2px 2px",this.div.appendChild(e.elem)})),this.widget.elem.innerText=t[0]+" ↓"}_clickHandler(t){this.widget.elem.innerText=t+" ↓",window.callbackFunction(`${this.callbackName}_~_${t}`),this.div.style.display="none",this.isOpen=!1}}class H{_handler;_div;left;right;constructor(t){this._handler=t,this._div=document.createElement("div"),this._div.classList.add("topbar");const e=t=>{const e=document.createElement("div");return e.classList.add("topbar-container"),e.style.justifyContent=t,this._div.appendChild(e),e};this.left=e("flex-start"),this.right=e("flex-end")}makeSwitcher(t,e,i,s="left"){const n=document.createElement("div");let o;n.style.margin="4px 12px";const r={elem:n,callbackName:i,intervalElements:t.map((t=>{const i=document.createElement("button");i.classList.add("topbar-button"),i.classList.add("switcher-button"),i.style.margin="0px 2px",i.innerText=t,t==e&&(o=i,i.classList.add("active-switcher-button"));const s=H.getClientWidth(i);return i.style.minWidth=s+1+"px",i.addEventListener("click",(()=>r.onItemClicked(i))),n.appendChild(i),i})),onItemClicked:t=>{t!=o&&(o.classList.remove("active-switcher-button"),t.classList.add("active-switcher-button"),o=t,window.callbackFunction(`${r.callbackName}_~_${t.innerText}`))}};return this.appendWidget(n,s,!0),r}makeTextBoxWidget(t,e="left",i=null){if(i){const s=document.createElement("input");return s.classList.add("topbar-textbox-input"),s.value=t,s.style.width=`${s.value.length+2}ch`,s.addEventListener("focus",(()=>{window.textBoxFocused=!0})),s.addEventListener("input",(t=>{t.preventDefault(),s.style.width=`${s.value.length+2}ch`})),s.addEventListener("keydown",(t=>{"Enter"==t.key&&(t.preventDefault(),s.blur())})),s.addEventListener("blur",(()=>{window.callbackFunction(`${i}_~_${s.value}`),window.textBoxFocused=!1})),this.appendWidget(s,e,!0),s}{const i=document.createElement("div");return i.classList.add("topbar-textbox"),i.innerText=t,this.appendWidget(i,e,!0),i}}makeMenu(t,e,i,s,n){return new V(this.makeButton.bind(this),s,t,e,i,n)}makeButton(t,e,i,s=!0,n="left",o=!1){let r=document.createElement("button");r.classList.add("topbar-button"),r.innerText=t,document.body.appendChild(r),r.style.minWidth=r.clientWidth+1+"px",document.body.removeChild(r);let a={elem:r,callbackName:e};if(e){let t;if(o){let e=!1;t=()=>{e=!e,window.callbackFunction(`${a.callbackName}_~_${e}`),r.style.backgroundColor=e?"var(--active-bg-color)":"",r.style.color=e?"var(--active-color)":""}}else t=()=>window.callbackFunction(`${a.callbackName}_~_${r.innerText}`);r.addEventListener("click",t)}return s&&this.appendWidget(r,n,i),a}makeSeparator(t="left"){const e=document.createElement("div");e.classList.add("topbar-seperator");("left"==t?this.left:this.right).appendChild(e)}appendWidget(t,e,i){const s="left"==e?this.left:this.right;i?("left"==e&&s.appendChild(t),this.makeSeparator(e),"right"==e&&s.appendChild(t)):s.appendChild(t),this._handler.reSize()}static getClientWidth(t){document.body.appendChild(t);const e=t.clientWidth;return document.body.removeChild(t),e}}s();return t.Box=G,t.Handler=class{id;commandFunctions=[];wrapper;div;chart;scale;precision=2;series;volumeSeries;legend;_topBar;toolBox;spinner;_seriesList=[];constructor(t,e,i,s,n){this.reSize=this.reSize.bind(this),this.id=t,this.scale={width:e,height:i},this.wrapper=document.createElement("div"),this.wrapper.classList.add("handler"),this.wrapper.style.float=s,this.div=document.createElement("div"),this.div.style.position="relative",this.wrapper.appendChild(this.div),window.containerDiv.append(this.wrapper),this.chart=this._createChart(),this.series=this.createCandlestickSeries(),this.volumeSeries=this.createVolumeSeries(),this.legend=new r(this),document.addEventListener("keydown",(t=>{for(let e=0;ewindow.handlerInFocus=this.id)),this.reSize(),n&&window.addEventListener("resize",(()=>this.reSize()))}reSize(){let t=0!==this.scale.height&&this._topBar?._div.offsetHeight||0;this.chart.resize(window.innerWidth*this.scale.width,window.innerHeight*this.scale.height-t),this.wrapper.style.width=100*this.scale.width+"%",this.wrapper.style.height=100*this.scale.height+"%",0===this.scale.height||0===this.scale.width?this.toolBox&&(this.toolBox.div.style.display="none"):this.toolBox&&(this.toolBox.div.style.display="flex")}_createChart(){return e.createChart(this.div,{width:window.innerWidth*this.scale.width,height:window.innerHeight*this.scale.height,layout:{textColor:window.pane.color,background:{color:"#000000",type:e.ColorType.Solid},fontSize:12},rightPriceScale:{scaleMargins:{top:.3,bottom:.25}},timeScale:{timeVisible:!0,secondsVisible:!1},crosshair:{mode:e.CrosshairMode.Normal,vertLine:{labelBackgroundColor:"rgb(46, 46, 46)"},horzLine:{labelBackgroundColor:"rgb(55, 55, 55)"}},grid:{vertLines:{color:"rgba(29, 30, 38, 5)"},horzLines:{color:"rgba(29, 30, 58, 5)"}},handleScroll:{vertTouchDrag:!0}})}createCandlestickSeries(){const t="rgba(39, 157, 130, 100)",e="rgba(200, 97, 100, 100)",i=this.chart.addCandlestickSeries({upColor:t,borderUpColor:t,wickUpColor:t,downColor:e,borderDownColor:e,wickDownColor:e});return i.priceScale().applyOptions({scaleMargins:{top:.2,bottom:.2}}),i}createVolumeSeries(){const t=this.chart.addHistogramSeries({color:"#26a69a",priceFormat:{type:"volume"},priceScaleId:"volume_scale"});return t.priceScale().applyOptions({scaleMargins:{top:.8,bottom:0}}),t}createLineSeries(t,e){const{group:i,...s}=e,n=this.chart.addLineSeries(s);this._seriesList.push(n);const o=n.options().color||"rgba(255,0,0,1)",r=o.startsWith("rgba")?o.replace(/[^,]+(?=\))/,"1"):o;if(i&&""!==i){const e=this.legend._groups.find((t=>t.name===i));e?(e.names.push(t),e.seriesList.push(n),e.solidColors.push(r)):this.legend.makeSeriesGroup(i,[t],[n],[r])}else this.legend.makeSeriesRow(t,n);return{name:t,series:n}}createHistogramSeries(t,e){const i=this.chart.addHistogramSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createAreaSeries(t,e){const i=this.chart.addAreaSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createBarSeries(t,e){const i=this.chart.addBarSeries({...e});return this._seriesList.push(i),this.legend.makeSeriesRow(t,i),{name:t,series:i}}createToolBox(){this.toolBox=new F(this.id,this.chart,this.series,this.commandFunctions),this.div.appendChild(this.toolBox.div)}createTopBar(){return this._topBar=new H(this),this.wrapper.prepend(this._topBar._div),this._topBar}toJSON(){const{chart:t,...e}=this;return e}static syncCharts(t,e,i=!1){function s(t,e){e?(t.chart.setCrosshairPosition(e.value||e.close,e.time,t.series),t.legend.legendHandler(e,!0)):t.chart.clearCrosshairPosition()}function n(t,e){return e.time&&e.seriesData.get(t)||null}const o=t.chart.timeScale(),r=e.chart.timeScale(),a=t=>{t&&o.setVisibleLogicalRange(t)},l=t=>{t&&r.setVisibleLogicalRange(t)},d=i=>{s(e,n(t.series,i))},h=i=>{s(t,n(e.series,i))};let c=e;function p(t,e,s,n,o,r){t.wrapper.addEventListener("mouseover",(()=>{c!==t&&(c=t,e.chart.unsubscribeCrosshairMove(s),t.chart.subscribeCrosshairMove(n),i||(e.chart.timeScale().unsubscribeVisibleLogicalRangeChange(o),t.chart.timeScale().subscribeVisibleLogicalRangeChange(r)))}))}p(e,t,d,h,l,a),p(t,e,h,d,a,l),e.chart.subscribeCrosshairMove(h);const u=r.getVisibleLogicalRange();u&&o.setVisibleLogicalRange(u),i||e.chart.timeScale().subscribeVisibleLogicalRangeChange(a)}static makeSearchBox(t){const e=document.createElement("div");e.classList.add("searchbox"),e.style.display="none";const i=document.createElement("div");i.innerHTML='';const s=document.createElement("input");return s.type="text",e.appendChild(i),e.appendChild(s),t.div.appendChild(e),t.commandFunctions.push((i=>window.handlerInFocus===t.id&&!window.textBoxFocused&&("none"===e.style.display?!!/^[a-zA-Z0-9]$/.test(i.key)&&(e.style.display="flex",s.focus(),!0):("Enter"===i.key||"Escape"===i.key)&&("Enter"===i.key&&window.callbackFunction(`search${t.id}_~_${s.value}`),e.style.display="none",s.value="",!0)))),s.addEventListener("input",(()=>s.value=s.value.toUpperCase())),{window:e,box:s}}static makeSpinner(t){t.spinner=document.createElement("div"),t.spinner.classList.add("spinner"),t.wrapper.appendChild(t.spinner);let e=0;!function i(){t.spinner&&(e+=10,t.spinner.style.transform=`translate(-50%, -50%) rotate(${e}deg)`,requestAnimationFrame(i))}()}static _styleMap={"--bg-color":"backgroundColor","--hover-bg-color":"hoverBackgroundColor","--click-bg-color":"clickBackgroundColor","--active-bg-color":"activeBackgroundColor","--muted-bg-color":"mutedBackgroundColor","--border-color":"borderColor","--color":"color","--active-color":"activeColor"};static setRootStyles(t){const e=document.documentElement.style;for(const[i,s]of Object.entries(this._styleMap))e.setProperty(i,t[s])}},t.HorizontalLine=b,t.Legend=r,t.RayLine=R,t.Table=class{_div;callbackName;borderColor;borderWidth;table;rows={};headings;widths;alignments;footer;header;constructor(t,e,i,s,n,o,r=!1,a,l,d,h,c){this._div=document.createElement("div"),this.callbackName=null,this.borderColor=l,this.borderWidth=d,r?(this._div.style.position="absolute",this._div.style.cursor="move"):(this._div.style.position="relative",this._div.style.float=o),this._div.style.zIndex="2000",this.reSize(t,e),this._div.style.display="flex",this._div.style.flexDirection="column",this._div.style.borderRadius="5px",this._div.style.color="white",this._div.style.fontSize="12px",this._div.style.fontVariantNumeric="tabular-nums",this.table=document.createElement("table"),this.table.style.width="100%",this.table.style.borderCollapse="collapse",this._div.style.overflow="hidden",this.headings=i,this.widths=s.map((t=>100*t+"%")),this.alignments=n;let p=this.table.createTHead().insertRow();for(let t=0;t0?c[t]:a,e.style.color=h[t],p.appendChild(e)}let u,_,m=document.createElement("div");if(m.style.overflowY="auto",m.style.overflowX="hidden",m.style.backgroundColor=a,m.appendChild(this.table),this._div.appendChild(m),window.containerDiv.appendChild(this._div),!r)return;let g=t=>{this._div.style.left=t.clientX-u+"px",this._div.style.top=t.clientY-_+"px"},v=()=>{document.removeEventListener("mousemove",g),document.removeEventListener("mouseup",v)};this._div.addEventListener("mousedown",(t=>{u=t.clientX-this._div.offsetLeft,_=t.clientY-this._div.offsetTop,document.addEventListener("mousemove",g),document.addEventListener("mouseup",v)}))}divToButton(t,e){t.addEventListener("mouseover",(()=>t.style.backgroundColor="rgba(60, 60, 60, 0.6)")),t.addEventListener("mouseout",(()=>t.style.backgroundColor="transparent")),t.addEventListener("mousedown",(()=>t.style.backgroundColor="rgba(60, 60, 60)")),t.addEventListener("click",(()=>window.callbackFunction(e))),t.addEventListener("mouseup",(()=>t.style.backgroundColor="rgba(60, 60, 60, 0.6)"))}newRow(t,e=!1){let i=this.table.insertRow();i.style.cursor="default";for(let s=0;s{t&&(window.cursor=t),document.body.style.cursor=window.cursor},t}({},LightweightCharts); +var Lib=function(e,t){"use strict";const i={backgroundColor:"#0c0d0f",hoverBackgroundColor:"#3c434c",clickBackgroundColor:"#50565E",activeBackgroundColor:"rgba(0, 122, 255, 0.7)",mutedBackgroundColor:"rgba(0, 122, 255, 0.3)",borderColor:"#3C434C",color:"#d8d9db",activeColor:"#ececed"};function s(){window.pane={...i},window.containerDiv=document.getElementById("container")||document.createElement("div"),window.setCursor=e=>{e&&(window.cursor=e),document.body.style.cursor=window.cursor},window.cursor="default",window.textBoxFocused=!1}const n='\n\n \n \n\n',o='\n\n \n \n \n\n';class r{handler;div;seriesContainer;ohlcEnabled=!1;percentEnabled=!1;linesEnabled=!1;colorBasedOnCandle=!1;text;candle;_lines=[];_groups=[];constructor(e){this.handler=e,this.div=document.createElement("div"),this.div.classList.add("legend"),this.seriesContainer=document.createElement("div"),this.text=document.createElement("span"),this.candle=document.createElement("div"),this.setupLegend(),this.legendHandler=this.legendHandler.bind(this),e.chart.subscribeCrosshairMove(this.legendHandler)}setupLegend(){this.div.style.maxWidth=100*this.handler.scale.width-8+"vw",this.div.style.display="none";const e=document.createElement("div");e.style.display="flex",e.style.flexDirection="row",this.seriesContainer.classList.add("series-container"),this.text.style.lineHeight="1.8",e.appendChild(this.seriesContainer),this.div.appendChild(this.text),this.div.appendChild(this.candle),this.div.appendChild(e),this.handler.div.appendChild(this.div)}legendItemFormat(e,t){return e.toFixed(t).toString().padStart(8," ")}shorthandFormat(e){const t=Math.abs(e);return t>=1e6?(e/1e6).toFixed(1)+"M":t>=1e3?(e/1e3).toFixed(1)+"K":e.toString().padStart(8," ")}makeSeriesRow(e,t,i=["▨"],s){const r=document.createElement("div");r.style.display="flex",r.style.alignItems="center";const a=document.createElement("div");a.innerHTML=i.map(((e,t)=>`${e}`)).join(" ")+` ${e}`;const l=document.createElement("div");l.classList.add("legend-toggle-switch");const d=this.createSvgIcon(n),h=this.createSvgIcon(o);l.appendChild(d.cloneNode(!0));let c=!0;return l.addEventListener("click",(()=>{c=!c,t.applyOptions({visible:c}),l.innerHTML="",l.appendChild(c?d.cloneNode(!0):h.cloneNode(!0))})),r.appendChild(a),r.appendChild(l),this.seriesContainer.appendChild(r),this._lines.push({name:e,div:a,row:r,toggle:l,series:t,solid:s[0],legendSymbol:i[0]}),r}makeSeriesGroup(e,t,i,s,r){const a=document.createElement("div");a.style.display="flex",a.style.alignItems="center";const l=document.createElement("div");l.innerHTML=`${e}:`;const d=document.createElement("div");d.classList.add("legend-toggle-switch");const h=this.createSvgIcon(n),c=this.createSvgIcon(o);d.appendChild(h.cloneNode(!0));let p=!0;d.addEventListener("click",(()=>{p=!p,i.forEach((e=>e.applyOptions({visible:p}))),d.innerHTML="",d.appendChild(p?h.cloneNode(!0):c.cloneNode(!0))}));let u=0;return t.forEach(((e,t)=>{const n=i[t];if(n&&"Bar"===n.seriesType()){const t=r[u]||"▨",i=r[u+1]||"▨",n=s[u],o=s[u+1];l.innerHTML+=`\n ${t}\n ${i}\n ${e}: -\n `,u+=2}else{const t=r[u]||"▨",i=s[u];l.innerHTML+=`\n ${t}\n ${e}: -\n `,u+=1}})),a.appendChild(l),a.appendChild(d),this.seriesContainer.appendChild(a),this._groups.push({name:e,seriesList:i,div:l,row:a,toggle:d,solidColors:s,names:t,legendSymbols:r}),a}createSvgIcon(e){const t=document.createElement("div");t.innerHTML=e.trim();return t.querySelector("svg")}legendHandler(e,t=!1){if(!this.ohlcEnabled&&!this.linesEnabled&&!this.percentEnabled)return;const i=this.handler.series.options();if(!e.time)return this.candle.style.color="transparent",void(this.candle.innerHTML=this.candle.innerHTML.replace(i.upColor,"").replace(i.downColor,""));let s,n=null;if(t){const t=this.handler.chart.timeScale(),i=t.timeToCoordinate(e.time);i&&(n=t.coordinateToLogical(i.valueOf())),n&&(s=this.handler.series.dataByIndex(n.valueOf()))}else s=e.seriesData.get(this.handler.series);let o='';if(s&&(this.ohlcEnabled&&(o+=`O ${this.legendItemFormat(s.open,this.handler.precision)} `,o+=`| H ${this.legendItemFormat(s.high,this.handler.precision)} `,o+=`| L ${this.legendItemFormat(s.low,this.handler.precision)} `,o+=`| C ${this.legendItemFormat(s.close,this.handler.precision)} `),this.percentEnabled)){const e=(s.close-s.open)/s.open*100,t=e>0?i.upColor:i.downColor,n=`${e>=0?"+":""}${e.toFixed(2)} %`;o+=this.colorBasedOnCandle?`| ${n}`:`| ${n}`}this.candle.innerHTML=o+"",this.updateGroupLegend(e,n,t),this.updateSeriesLegend(e,n,t)}updateGroupLegend(e,t,i){this._groups.forEach((s=>{if(!this.linesEnabled)return void(s.row.style.display="none");s.row.style.display="flex";let n=`${s.name}:`,o=0;s.seriesList.forEach(((r,a)=>{const l=r.seriesType();let d;if(d=i&&t?r.dataByIndex(t):e.seriesData.get(r),!d)return;const h=r.options().priceFormat,c=s.names[a];if("Bar"===l){const e=d,t=this.legendItemFormat(e.open,h.precision),i=this.legendItemFormat(e.close,h.precision),r=s.legendSymbols[o]||"▨",a=s.legendSymbols[o+1]||"▨",l=s.solidColors[o],p=s.solidColors[o+1];n+=`\n ${r}\n ${a}\n ${c}: O ${t}, C ${i}\n `,o+=2}else{const e=d,t=this.legendItemFormat(e.value,h.precision),i=s.legendSymbols[o]||"▨",r=s.solidColors[o];n+=`\n ${i}\n ${c}: ${t}\n `,o+=1}})),s.div.innerHTML=n}))}updateSeriesLegend(e,t,i){this._lines&&this._lines.length?this._lines.forEach((s=>{if(!this.linesEnabled)return void(s.row.style.display="none");s.row.style.display="flex";const n=s.series.seriesType();let o;if(o=i&&t?s.series.dataByIndex(t):e.seriesData.get(s.series),!o)return void(s.div.innerHTML=`${s.name}: -`);const r=s.series.options().priceFormat;let a;if(console.log(`Series: ${s.name}, Type: ${n}, Data:`,o),"Bar"===n){const e=o,t=this.legendItemFormat(e.open,r.precision),i=this.legendItemFormat(e.close,r.precision),n=s.legendSymbol[0]||"▨",l=s.legendSymbol[1]||"▨";a=`\n ${n}\n ${l}\n ${s.name}: O ${t}, C ${i}\n `}else if("Histogram"===n){const e=o,t=this.shorthandFormat(e.value);a=`${s.legendSymbol||"▨"} ${s.name}: ${t}`}else{const e=o,t=this.legendItemFormat(e.value,r.precision);a=`${s.legendSymbol||"▨"} ${s.name}: ${t}`}s.div.innerHTML=a})):console.error("No lines available to update legend.")}}function a(e){if(void 0===e)throw new Error("Value is undefined");return e}class l{_chart=void 0;_series=void 0;requestUpdate(){this._requestUpdate&&this._requestUpdate()}_requestUpdate;attached({chart:e,series:t,requestUpdate:i}){this._chart=e,this._series=t,this._series.subscribeDataChanged(this._fireDataUpdated),this._requestUpdate=i,this.requestUpdate()}detached(){this._chart=void 0,this._series=void 0,this._requestUpdate=void 0}get chart(){return a(this._chart)}get series(){return a(this._series)}_fireDataUpdated(e){this.dataUpdated&&this.dataUpdated(e)}}const d={lineColor:"#1E80F0",lineStyle:t.LineStyle.Solid,width:4};var h;!function(e){e[e.NONE=0]="NONE",e[e.HOVERING=1]="HOVERING",e[e.DRAGGING=2]="DRAGGING",e[e.DRAGGINGP1=3]="DRAGGINGP1",e[e.DRAGGINGP2=4]="DRAGGINGP2",e[e.DRAGGINGP3=5]="DRAGGINGP3",e[e.DRAGGINGP4=6]="DRAGGINGP4"}(h||(h={}));class c extends l{_paneViews=[];_options;_points=[];_state=h.NONE;_startDragPoint=null;_latestHoverPoint=null;static _mouseIsDown=!1;static hoveredObject=null;static lastHoveredObject=null;_listeners=[];constructor(e){super(),this._options={...d,...e}}updateAllViews(){this._paneViews.forEach((e=>e.update()))}paneViews(){return this._paneViews}applyOptions(e){this._options={...this._options,...e},this.requestUpdate()}updatePoints(...e){for(let t=0;ti.name===e&&i.listener===t));this._listeners.splice(this._listeners.indexOf(i),1)}_handleHoverInteraction(e){if(this._latestHoverPoint=e.point,c._mouseIsDown)this._handleDragInteraction(e);else if(this._mouseIsOverDrawing(e)){if(this._state!=h.NONE)return;this._moveToState(h.HOVERING),c.hoveredObject=c.lastHoveredObject=this}else{if(this._state==h.NONE)return;this._moveToState(h.NONE),c.hoveredObject===this&&(c.hoveredObject=null)}}static _eventToPoint(e,t){if(!t||!e.point||!e.logical)return null;const i=t.coordinateToPrice(e.point.y);return null==i?null:{time:e.time||null,logical:e.logical,price:i.valueOf()}}static _getDiff(e,t){return{logical:e.logical-t.logical,price:e.price-t.price}}_addDiffToPoint(e,t,i){e&&(e.logical=e.logical+t,e.price=e.price+i,e.time=this.series.dataByIndex(e.logical)?.time||null)}_handleMouseDownInteraction=()=>{c._mouseIsDown=!0,this._onMouseDown()};_handleMouseUpInteraction=()=>{c._mouseIsDown=!1,this._moveToState(h.HOVERING)};_handleDragInteraction(e){if(this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1&&this._state!=h.DRAGGINGP2&&this._state!=h.DRAGGINGP3&&this._state!=h.DRAGGINGP4)return;const t=c._eventToPoint(e,this.series);if(!t)return;this._startDragPoint=this._startDragPoint||t;const i=c._getDiff(t,this._startDragPoint);this._onDrag(i),this.requestUpdate(),this._startDragPoint=t}}class p{_options;constructor(e){this._options=e}}class u extends p{_p1;_p2;_hovered;constructor(e,t,i,s){super(i),this._p1=e,this._p2=t,this._hovered=s}_getScaledCoordinates(e){return null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y?null:{x1:Math.round(this._p1.x*e.horizontalPixelRatio),y1:Math.round(this._p1.y*e.verticalPixelRatio),x2:Math.round(this._p2.x*e.horizontalPixelRatio),y2:Math.round(this._p2.y*e.verticalPixelRatio)}}_drawEndCircle(e,t,i){e.context.fillStyle="#000",e.context.beginPath(),e.context.arc(t,i,9,0,2*Math.PI),e.context.stroke(),e.context.fill()}}function _(e,i){const s={[t.LineStyle.Solid]:[],[t.LineStyle.Dotted]:[e.lineWidth,e.lineWidth],[t.LineStyle.Dashed]:[2*e.lineWidth,2*e.lineWidth],[t.LineStyle.LargeDashed]:[6*e.lineWidth,6*e.lineWidth],[t.LineStyle.SparseDotted]:[e.lineWidth,4*e.lineWidth]}[i];e.setLineDash(s)}class m extends p{_point={x:null,y:null};constructor(e,t){super(t),this._point=e}draw(e){e.useBitmapCoordinateSpace((e=>{if(null==this._point.y)return;const t=e.context,i=Math.round(this._point.y*e.verticalPixelRatio),s=this._point.x?this._point.x*e.horizontalPixelRatio:0;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,_(t,this._options.lineStyle),t.beginPath(),t.moveTo(s,i),t.lineTo(e.bitmapSize.width,i),t.stroke()}))}}class g{_source;constructor(e){this._source=e}}class v extends g{_p1={x:null,y:null};_p2={x:null,y:null};_source;constructor(e){super(e),this._source=e}update(){if(!this._source.p1||!this._source.p2)return;const e=this._source.series,t=e.priceToCoordinate(this._source.p1.price),i=e.priceToCoordinate(this._source.p2.price),s=this._getX(this._source.p1),n=this._getX(this._source.p2);this._p1={x:s,y:t},this._p2={x:n,y:i}}_getX(e){return this._source.chart.timeScale().logicalToCoordinate(e.logical)}}class y extends g{_source;_point={x:null,y:null};constructor(e){super(e),this._source=e}update(){const e=this._source._point,t=this._source.chart.timeScale(),i=this._source.series;"RayLine"==this._source._type&&(this._point.x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical)),this._point.y=i.priceToCoordinate(e.price)}renderer(){return new m(this._point,this._source._options)}}class w{_source;_y=null;_price=null;constructor(e){this._source=e}update(){if(!this._source.series||!this._source._point)return;this._y=this._source.series.priceToCoordinate(this._source._point.price);const e=this._source.series.options().priceFormat.precision;this._price=this._source._point.price.toFixed(e).toString()}visible(){return!0}tickVisible(){return!0}coordinate(){return this._y??0}text(){return this._source._options.text||this._price||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class b extends c{_type="HorizontalLine";_paneViews;_point;_callbackName;_priceAxisViews;_startDragPoint=null;constructor(e,t,i=null){super(t),this._point=e,this._point.time=null,this._paneViews=[new y(this)],this._priceAxisViews=[new w(this)],this._callbackName=i}get points(){return[this._point]}updatePoints(...e){for(const t of e)t&&(this._point.price=t.price);this.requestUpdate()}updateAllViews(){this._paneViews.forEach((e=>e.update())),this._priceAxisViews.forEach((e=>e.update()))}priceAxisViews(){return this._priceAxisViews}_moveToState(e){switch(e){case h.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._addDiffToPoint(this._point,0,e.price),this.requestUpdate()}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this.series.priceToCoordinate(this._point.price);return!!i&&Math.abs(i-e.point.y){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class x{_chart;_series;_finishDrawingCallback=null;_drawings=[];_activeDrawing=null;_isDrawing=!1;_drawingType=null;constructor(e,t,i=null){this._chart=e,this._series=t,this._finishDrawingCallback=i,this._chart.subscribeClick(this._clickHandler),this._chart.subscribeCrosshairMove(this._moveHandler)}_clickHandler=e=>this._onClick(e);_moveHandler=e=>this._onMouseMove(e);beginDrawing(e){this._drawingType=e,this._isDrawing=!0}stopDrawing(){this._isDrawing=!1,this._activeDrawing=null}get drawings(){return this._drawings}addNewDrawing(e){this._series.attachPrimitive(e),this._drawings.push(e)}delete(e){if(null==e)return;const t=this._drawings.indexOf(e);-1!=t&&(this._drawings.splice(t,1),e.detach())}clearDrawings(){for(const e of this._drawings)e.detach();this._drawings=[]}repositionOnTime(){for(const e of this.drawings){const t=[];for(const i of e.points){if(!i){t.push(i);continue}const e=i.time?this._chart.timeScale().coordinateToLogical(this._chart.timeScale().timeToCoordinate(i.time)||0):i.logical;t.push({time:i.time,logical:e,price:i.price})}e.updatePoints(...t)}}_onClick(e){if(!this._isDrawing)return;const t=c._eventToPoint(e,this._series);if(t)if(null==this._activeDrawing){if(null==this._drawingType)return;this._activeDrawing=new this._drawingType(t,t),this._series.attachPrimitive(this._activeDrawing),this._drawingType==b&&this._onClick(e)}else{if(this._drawings.push(this._activeDrawing),this.stopDrawing(),!this._finishDrawingCallback)return;this._finishDrawingCallback()}}_onMouseMove(e){if(!e)return;for(const t of this._drawings)t._handleHoverInteraction(e);if(!this._isDrawing||!this._activeDrawing)return;const t=c._eventToPoint(e,this._series);t&&this._activeDrawing.updatePoints(null,t)}}class C extends u{constructor(e,t,i,s){super(e,t,i,s)}draw(e){e.useBitmapCoordinateSpace((e=>{if(null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y)return;const t=e.context,i=this._getScaledCoordinates(e);i&&(t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,_(t,this._options.lineStyle),t.beginPath(),t.moveTo(i.x1,i.y1),t.lineTo(i.x2,i.y2),t.stroke(),this._hovered&&(this._drawEndCircle(e,i.x1,i.y1),this._drawEndCircle(e,i.x2,i.y2)))}))}}class f extends v{constructor(e){super(e)}renderer(){return new C(this._p1,this._p2,this._source._options,this._source.hovered)}}class k extends c{_paneViews=[];_hovered=!1;constructor(e,t,i){super(),this.points.push(e),this.points.push(t),this._options={...d,...i}}setFirstPoint(e){this.updatePoints(e)}setSecondPoint(e){this.updatePoints(null,e)}get p1(){return this.points[0]}get p2(){return this.points[1]}get hovered(){return this._hovered}}class D extends k{_type="TrendLine";constructor(e,t,i){super(e,t,i),this._paneViews=[new f(this)]}_moveToState(e){switch(e){case h.NONE:document.body.style.cursor="default",this._hovered=!1,this.requestUpdate(),this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this.requestUpdate(),this._subscribe("mousedown",this._handleMouseDownInteraction),this._unsubscribe("mouseup",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGINGP1:case h.DRAGGINGP2:case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1||this._addDiffToPoint(this.p1,e.logical,e.price),this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP2||this._addDiffToPoint(this.p2,e.logical,e.price)}_onMouseDown(){this._startDragPoint=null;const e=this._latestHoverPoint;if(!e)return;const t=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(t.x&&i.x&&t.y&&i.y))return this._moveToState(h.DRAGGING);Math.abs(e.x-t.x)<10&&Math.abs(e.y-t.y)<10?this._moveToState(h.DRAGGINGP1):Math.abs(e.x-i.x)<10&&Math.abs(e.y-i.y)<10?this._moveToState(h.DRAGGINGP2):this._moveToState(h.DRAGGING)}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this._paneViews[0]._p1.x,s=this._paneViews[0]._p1.y,n=this._paneViews[0]._p2.x,o=this._paneViews[0]._p2.y;if(!(i&&n&&s&&o))return!1;const r=e.point.x,a=e.point.y;if(r<=Math.min(i,n)-t||r>=Math.max(i,n)+t)return!1;return Math.abs((o-s)*r-(n-i)*a+n*s-o*i)/Math.sqrt((o-s)**2+(n-i)**2)<=t}}class L extends u{constructor(e,t,i,s){super(e,t,i,s)}draw(e){e.useBitmapCoordinateSpace((e=>{const t=e.context,i=this._getScaledCoordinates(e);if(!i)return;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,_(t,this._options.lineStyle),t.fillStyle=this._options.fillColor;const s=Math.min(i.x1,i.x2),n=Math.min(i.y1,i.y2),o=Math.abs(i.x1-i.x2),r=Math.abs(i.y1-i.y2);t.strokeRect(s,n,o,r),t.fillRect(s,n,o,r),this._hovered&&(this._drawEndCircle(e,s,n),this._drawEndCircle(e,s+o,n),this._drawEndCircle(e,s+o,n+r),this._drawEndCircle(e,s,n+r))}))}}class S extends v{constructor(e){super(e)}renderer(){return new L(this._p1,this._p2,this._source._options,this._source.hovered)}}const E={fillEnabled:!0,fillColor:"rgba(255, 255, 255, 0.2)",...d};class G extends k{_type="Box";constructor(e,t,i){super(e,t,i),this._options={...E,...i},this._paneViews=[new S(this)]}_moveToState(e){switch(e){case h.NONE:document.body.style.cursor="default",this._hovered=!1,this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this._unsubscribe("mouseup",this._handleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGINGP1:case h.DRAGGINGP2:case h.DRAGGINGP3:case h.DRAGGINGP4:case h.DRAGGING:document.body.style.cursor="grabbing",document.body.addEventListener("mouseup",this._handleMouseUpInteraction),this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1||this._addDiffToPoint(this.p1,e.logical,e.price),this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP2||this._addDiffToPoint(this.p2,e.logical,e.price),this._state!=h.DRAGGING&&(this._state==h.DRAGGINGP3&&(this._addDiffToPoint(this.p1,e.logical,0),this._addDiffToPoint(this.p2,0,e.price)),this._state==h.DRAGGINGP4&&(this._addDiffToPoint(this.p1,0,e.price),this._addDiffToPoint(this.p2,e.logical,0)))}_onMouseDown(){this._startDragPoint=null;const e=this._latestHoverPoint,t=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(t.x&&i.x&&t.y&&i.y))return this._moveToState(h.DRAGGING);const s=10;Math.abs(e.x-t.x)l-p&&rd-p&&ai.appendChild(this.makeColorBox(e))));let s=document.createElement("div");s.style.backgroundColor=window.pane.borderColor,s.style.height="1px",s.style.width="130px";let n=document.createElement("div");n.style.margin="10px";let o=document.createElement("div");o.style.color="lightgray",o.style.fontSize="12px",o.innerText="Opacity",this._opacityLabel=document.createElement("div"),this._opacityLabel.style.color="lightgray",this._opacityLabel.style.fontSize="12px",this._opacitySlider=document.createElement("input"),this._opacitySlider.type="range",this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%",this._opacitySlider.oninput=()=>{this._opacityLabel.innerText=this._opacitySlider.value+"%",this.opacity=parseInt(this._opacitySlider.value)/100,this.updateColor()},n.appendChild(o),n.appendChild(this._opacitySlider),n.appendChild(this._opacityLabel),this._div.appendChild(i),this._div.appendChild(s),this._div.appendChild(n),window.containerDiv.appendChild(this._div)}_updateOpacitySlider(){this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%"}makeColorBox(e){const t=document.createElement("div");t.style.width="18px",t.style.height="18px",t.style.borderRadius="3px",t.style.margin="3px",t.style.boxSizing="border-box",t.style.backgroundColor=e,t.addEventListener("mouseover",(()=>t.style.border="2px solid lightgray")),t.addEventListener("mouseout",(()=>t.style.border="none"));const i=I.extractRGBA(e);return t.addEventListener("click",(()=>{this.rgba=i,this.updateColor()})),t}static extractRGBA(e){const t=document.createElement("div");t.style.color=e,document.body.appendChild(t);const i=getComputedStyle(t).color;document.body.removeChild(t);const s=i.match(/\d+/g)?.map(Number);if(!s)return[];let n=i.includes("rgba")?parseFloat(i.split(",")[3]):1;return[s[0],s[1],s[2],n]}updateColor(){if(!c.lastHoveredObject||!this.rgba)return;const e=`rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`;c.lastHoveredObject.applyOptions({[this.colorOption]:e}),this.saveDrawings()}openMenu(e){c.lastHoveredObject&&(this.rgba=I.extractRGBA(c.lastHoveredObject._options[this.colorOption]),this.opacity=this.rgba[3],this._updateOpacitySlider(),this._div.style.top=e.top-30+"px",this._div.style.left=e.right+"px",this._div.style.display="flex",setTimeout((()=>document.addEventListener("mousedown",(e=>{this._div.contains(e.target)||this.closeMenu()}))),10))}closeMenu(){document.body.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}class T{static _styles=[{name:"Solid",var:t.LineStyle.Solid},{name:"Dotted",var:t.LineStyle.Dotted},{name:"Dashed",var:t.LineStyle.Dashed},{name:"Large Dashed",var:t.LineStyle.LargeDashed},{name:"Sparse Dotted",var:t.LineStyle.SparseDotted}];_div;_saveDrawings;constructor(e){this._saveDrawings=e,this._div=document.createElement("div"),this._div.classList.add("context-menu"),T._styles.forEach((e=>{this._div.appendChild(this._makeTextBox(e.name,e.var))})),window.containerDiv.appendChild(this._div)}_makeTextBox(e,t){const i=document.createElement("span");return i.classList.add("context-menu-item"),i.innerText=e,i.addEventListener("click",(()=>{c.lastHoveredObject?.applyOptions({lineStyle:t}),this._saveDrawings()})),i}openMenu(e){this._div.style.top=e.top-30+"px",this._div.style.left=e.right+"px",this._div.style.display="block",setTimeout((()=>document.addEventListener("mousedown",(e=>{this._div.contains(e.target)||this.closeMenu()}))),10)}closeMenu(){document.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}function M(e){const t=[];for(const i of e)0==t.length?t.push(i.toUpperCase()):i==i.toUpperCase()?t.push(" "+i):t.push(i);return t.join("")}class N{saveDrawings;drawingTool;div;hoverItem;items=[];constructor(e,t){this.saveDrawings=e,this.drawingTool=t,this._onRightClick=this._onRightClick.bind(this),this.div=document.createElement("div"),this.div.classList.add("context-menu"),document.body.appendChild(this.div),this.hoverItem=null,document.body.addEventListener("contextmenu",this._onRightClick)}_handleClick=e=>this._onClick(e);_onClick(e){e.target&&(this.div.contains(e.target)||(this.div.style.display="none",document.body.removeEventListener("click",this._handleClick)))}_onRightClick(e){if(!c.hoveredObject)return;for(const e of this.items)this.div.removeChild(e);this.items=[];for(const e of Object.keys(c.hoveredObject._options)){let t;if(e.toLowerCase().includes("color"))t=new I(this.saveDrawings,e);else{if("lineStyle"!==e)continue;t=new T(this.saveDrawings)}let i=e=>t.openMenu(e);this.menuItem(M(e),i,(()=>{document.removeEventListener("click",t.closeMenu),t._div.style.display="none"}))}this.separator(),this.menuItem("Delete Drawing",(()=>this.drawingTool.delete(c.lastHoveredObject))),e.preventDefault(),this.div.style.left=e.clientX+"px",this.div.style.top=e.clientY+"px",this.div.style.display="block",document.body.addEventListener("click",this._handleClick)}menuItem(e,t,i=null){const s=document.createElement("span");s.classList.add("context-menu-item"),this.div.appendChild(s);const n=document.createElement("span");if(n.innerText=e,n.style.pointerEvents="none",s.appendChild(n),i){let e=document.createElement("span");e.innerText="►",e.style.fontSize="8px",e.style.pointerEvents="none",s.appendChild(e)}if(s.addEventListener("mouseover",(()=>{this.hoverItem&&this.hoverItem.closeAction&&this.hoverItem.closeAction(),this.hoverItem={elem:n,action:t,closeAction:i}})),i){let e;s.addEventListener("mouseover",(()=>e=setTimeout((()=>t(s.getBoundingClientRect())),100))),s.addEventListener("mouseout",(()=>clearTimeout(e)))}else s.addEventListener("click",(e=>{t(e),this.div.style.display="none"}));this.items.push(s)}separator(){const e=document.createElement("div");e.style.width="90%",e.style.height="1px",e.style.margin="3px 0px",e.style.backgroundColor=window.pane.borderColor,this.div.appendChild(e),this.items.push(e)}}class R extends b{_type="RayLine";constructor(e,t){super({...e},t),this._point.time=e.time}updatePoints(...e){for(const t of e)t&&(this._point=t);this.requestUpdate()}_onDrag(e){this._addDiffToPoint(this._point,e.logical,e.price),this.requestUpdate()}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this.series.priceToCoordinate(this._point.price),s=this._point.time?this.chart.timeScale().timeToCoordinate(this._point.time):null;return!(!i||!s)&&(Math.abs(i-e.point.y)s-t)}}class B extends p{_point={x:null,y:null};constructor(e,t){super(t),this._point=e}draw(e){e.useBitmapCoordinateSpace((e=>{if(null==this._point.x)return;const t=e.context,i=this._point.x*e.horizontalPixelRatio;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,_(t,this._options.lineStyle),t.beginPath(),t.moveTo(i,0),t.lineTo(i,e.bitmapSize.height),t.stroke()}))}}class A extends g{_source;_point={x:null,y:null};constructor(e){super(e),this._source=e}update(){const e=this._source._point,t=this._source.chart.timeScale(),i=this._source.series;this._point.x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical),this._point.y=i.priceToCoordinate(e.price)}renderer(){return new B(this._point,this._source._options)}}class P{_source;_x=null;constructor(e){this._source=e}update(){if(!this._source.chart||!this._source._point)return;const e=this._source._point,t=this._source.chart.timeScale();this._x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical)}visible(){return!!this._source._options.text}tickVisible(){return!0}coordinate(){return this._x??0}text(){return this._source._options.text||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class O extends c{_type="VerticalLine";_paneViews;_timeAxisViews;_point;_callbackName;_startDragPoint=null;constructor(e,t,i=null){super(t),this._point=e,this._paneViews=[new A(this)],this._callbackName=i,this._timeAxisViews=[new P(this)]}updateAllViews(){this._paneViews.forEach((e=>e.update())),this._timeAxisViews.forEach((e=>e.update()))}timeAxisViews(){return this._timeAxisViews}updatePoints(...e){for(const t of e)t&&(!t.time&&t.logical&&(t.time=this.series.dataByIndex(t.logical)?.time||null),this._point=t);this.requestUpdate()}get points(){return[this._point]}_moveToState(e){switch(e){case h.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._addDiffToPoint(this._point,e.logical,0),this.requestUpdate()}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this.chart.timeScale();let s;return s=this._point.time?i.timeToCoordinate(this._point.time):i.logicalToCoordinate(this._point.logical),!!s&&Math.abs(s-e.point.x){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class ${static TREND_SVG='';static HORZ_SVG='';static RAY_SVG='';static BOX_SVG='';static VERT_SVG=$.RAY_SVG;div;activeIcon=null;buttons=[];_commandFunctions;_handlerID;_drawingTool;constructor(e,t,i,s){this._handlerID=e,this._commandFunctions=s,this._drawingTool=new x(t,i,(()=>this.removeActiveAndSave())),this.div=this._makeToolBox(),new N(this.saveDrawings,this._drawingTool),s.push((e=>{if((e.metaKey||e.ctrlKey)&&"KeyZ"===e.code){const e=this._drawingTool.drawings.pop();return e&&this._drawingTool.delete(e),!0}return!1}))}toJSON(){const{...e}=this;return e}_makeToolBox(){let e=document.createElement("div");e.classList.add("toolbox"),this.buttons.push(this._makeToolBoxElement(D,"KeyT",$.TREND_SVG)),this.buttons.push(this._makeToolBoxElement(b,"KeyH",$.HORZ_SVG)),this.buttons.push(this._makeToolBoxElement(R,"KeyR",$.RAY_SVG)),this.buttons.push(this._makeToolBoxElement(G,"KeyB",$.BOX_SVG)),this.buttons.push(this._makeToolBoxElement(O,"KeyV",$.VERT_SVG,!0));for(const t of this.buttons)e.appendChild(t);return e}_makeToolBoxElement(e,t,i,s=!1){const n=document.createElement("div");n.classList.add("toolbox-button");const o=document.createElementNS("http://www.w3.org/2000/svg","svg");o.setAttribute("width","29"),o.setAttribute("height","29");const r=document.createElementNS("http://www.w3.org/2000/svg","g");r.innerHTML=i,r.setAttribute("fill",window.pane.color),o.appendChild(r),n.appendChild(o);const a={div:n,group:r,type:e};return n.addEventListener("click",(()=>this._onIconClick(a))),this._commandFunctions.push((e=>this._handlerID===window.handlerInFocus&&(!(!e.altKey||e.code!==t)&&(e.preventDefault(),this._onIconClick(a),!0)))),1==s&&(o.style.transform="rotate(90deg)",o.style.transformBox="fill-box",o.style.transformOrigin="center"),n}_onIconClick(e){this.activeIcon&&(this.activeIcon.div.classList.remove("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.stopDrawing(),this.activeIcon===e)?this.activeIcon=null:(this.activeIcon=e,this.activeIcon.div.classList.add("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.beginDrawing(this.activeIcon.type))}removeActiveAndSave=()=>{window.setCursor("default"),this.activeIcon&&this.activeIcon.div.classList.remove("active-toolbox-button"),this.activeIcon=null,this.saveDrawings()};addNewDrawing(e){this._drawingTool.addNewDrawing(e)}clearDrawings(){this._drawingTool.clearDrawings()}saveDrawings=()=>{const e=[];for(const t of this._drawingTool.drawings)e.push({type:t._type,points:t.points,options:t._options});const t=JSON.stringify(e);window.callbackFunction(`save_drawings${this._handlerID}_~_${t}`)};loadDrawings(e){e.forEach((e=>{switch(e.type){case"Box":this._drawingTool.addNewDrawing(new G(e.points[0],e.points[1],e.options));break;case"TrendLine":this._drawingTool.addNewDrawing(new D(e.points[0],e.points[1],e.options));break;case"HorizontalLine":this._drawingTool.addNewDrawing(new b(e.points[0],e.options));break;case"RayLine":this._drawingTool.addNewDrawing(new R(e.points[0],e.options));break;case"VerticalLine":this._drawingTool.addNewDrawing(new O(e.points[0],e.options))}}))}}class F{makeButton;callbackName;div;isOpen=!1;widget;constructor(e,t,i,s,n,o){this.makeButton=e,this.callbackName=t,this.div=document.createElement("div"),this.div.classList.add("topbar-menu"),this.widget=this.makeButton(s+" ↓",null,n,!0,o),this.updateMenuItems(i),this.widget.elem.addEventListener("click",(()=>{if(this.isOpen=!this.isOpen,!this.isOpen)return void(this.div.style.display="none");let e=this.widget.elem.getBoundingClientRect();this.div.style.display="flex",this.div.style.flexDirection="column";let t=e.x+e.width/2;this.div.style.left=t-this.div.clientWidth/2+"px",this.div.style.top=e.y+e.height+"px"})),document.body.appendChild(this.div)}updateMenuItems(e){this.div.innerHTML="",e.forEach((e=>{let t=this.makeButton(e,null,!1,!1);t.elem.addEventListener("click",(()=>{this._clickHandler(t.elem.innerText)})),t.elem.style.margin="4px 4px",t.elem.style.padding="2px 2px",this.div.appendChild(t.elem)})),this.widget.elem.innerText=e[0]+" ↓"}_clickHandler(e){this.widget.elem.innerText=e+" ↓",window.callbackFunction(`${this.callbackName}_~_${e}`),this.div.style.display="none",this.isOpen=!1}}class V{_handler;_div;left;right;constructor(e){this._handler=e,this._div=document.createElement("div"),this._div.classList.add("topbar");const t=e=>{const t=document.createElement("div");return t.classList.add("topbar-container"),t.style.justifyContent=e,this._div.appendChild(t),t};this.left=t("flex-start"),this.right=t("flex-end")}makeSwitcher(e,t,i,s="left"){const n=document.createElement("div");let o;n.style.margin="4px 12px";const r={elem:n,callbackName:i,intervalElements:e.map((e=>{const i=document.createElement("button");i.classList.add("topbar-button"),i.classList.add("switcher-button"),i.style.margin="0px 2px",i.innerText=e,e==t&&(o=i,i.classList.add("active-switcher-button"));const s=V.getClientWidth(i);return i.style.minWidth=s+1+"px",i.addEventListener("click",(()=>r.onItemClicked(i))),n.appendChild(i),i})),onItemClicked:e=>{e!=o&&(o.classList.remove("active-switcher-button"),e.classList.add("active-switcher-button"),o=e,window.callbackFunction(`${r.callbackName}_~_${e.innerText}`))}};return this.appendWidget(n,s,!0),r}makeTextBoxWidget(e,t="left",i=null){if(i){const s=document.createElement("input");return s.classList.add("topbar-textbox-input"),s.value=e,s.style.width=`${s.value.length+2}ch`,s.addEventListener("focus",(()=>{window.textBoxFocused=!0})),s.addEventListener("input",(e=>{e.preventDefault(),s.style.width=`${s.value.length+2}ch`})),s.addEventListener("keydown",(e=>{"Enter"==e.key&&(e.preventDefault(),s.blur())})),s.addEventListener("blur",(()=>{window.callbackFunction(`${i}_~_${s.value}`),window.textBoxFocused=!1})),this.appendWidget(s,t,!0),s}{const i=document.createElement("div");return i.classList.add("topbar-textbox"),i.innerText=e,this.appendWidget(i,t,!0),i}}makeMenu(e,t,i,s,n){return new F(this.makeButton.bind(this),s,e,t,i,n)}makeButton(e,t,i,s=!0,n="left",o=!1){let r=document.createElement("button");r.classList.add("topbar-button"),r.innerText=e,document.body.appendChild(r),r.style.minWidth=r.clientWidth+1+"px",document.body.removeChild(r);let a={elem:r,callbackName:t};if(t){let e;if(o){let t=!1;e=()=>{t=!t,window.callbackFunction(`${a.callbackName}_~_${t}`),r.style.backgroundColor=t?"var(--active-bg-color)":"",r.style.color=t?"var(--active-color)":""}}else e=()=>window.callbackFunction(`${a.callbackName}_~_${r.innerText}`);r.addEventListener("click",e)}return s&&this.appendWidget(r,n,i),a}makeSeparator(e="left"){const t=document.createElement("div");t.classList.add("topbar-seperator");("left"==e?this.left:this.right).appendChild(t)}appendWidget(e,t,i){const s="left"==t?this.left:this.right;i?("left"==t&&s.appendChild(e),this.makeSeparator(t),"right"==t&&s.appendChild(e)):s.appendChild(e),this._handler.reSize()}static getClientWidth(e){document.body.appendChild(e);const t=e.clientWidth;return document.body.removeChild(e),t}}s();return e.Box=G,e.Handler=class{id;commandFunctions=[];wrapper;div;chart;scale;precision=2;series;volumeSeries;legend;_topBar;toolBox;spinner;_seriesList=[];constructor(e,t,i,s,n){this.reSize=this.reSize.bind(this),this.id=e,this.scale={width:t,height:i},this.wrapper=document.createElement("div"),this.wrapper.classList.add("handler"),this.wrapper.style.float=s,this.div=document.createElement("div"),this.div.style.position="relative",this.wrapper.appendChild(this.div),window.containerDiv.append(this.wrapper),this.chart=this._createChart(),this.series=this.createCandlestickSeries(),this.volumeSeries=this.createVolumeSeries(),this.legend=new r(this),document.addEventListener("keydown",(e=>{for(let t=0;twindow.handlerInFocus=this.id)),this.reSize(),n&&window.addEventListener("resize",(()=>this.reSize()))}reSize(){let e=0!==this.scale.height&&this._topBar?._div.offsetHeight||0;this.chart.resize(window.innerWidth*this.scale.width,window.innerHeight*this.scale.height-e),this.wrapper.style.width=100*this.scale.width+"%",this.wrapper.style.height=100*this.scale.height+"%",0===this.scale.height||0===this.scale.width?this.toolBox&&(this.toolBox.div.style.display="none"):this.toolBox&&(this.toolBox.div.style.display="flex")}_createChart(){return t.createChart(this.div,{width:window.innerWidth*this.scale.width,height:window.innerHeight*this.scale.height,layout:{textColor:window.pane.color,background:{color:"#000000",type:t.ColorType.Solid},fontSize:12},rightPriceScale:{scaleMargins:{top:.3,bottom:.25}},timeScale:{timeVisible:!0,secondsVisible:!1},crosshair:{mode:t.CrosshairMode.Normal,vertLine:{labelBackgroundColor:"rgb(46, 46, 46)"},horzLine:{labelBackgroundColor:"rgb(55, 55, 55)"}},grid:{vertLines:{color:"rgba(29, 30, 38, 5)"},horzLines:{color:"rgba(29, 30, 58, 5)"}},handleScroll:{vertTouchDrag:!0}})}createCandlestickSeries(){const e="rgba(39, 157, 130, 100)",t="rgba(200, 97, 100, 100)",i=this.chart.addCandlestickSeries({upColor:e,borderUpColor:e,wickUpColor:e,downColor:t,borderDownColor:t,wickDownColor:t});return i.priceScale().applyOptions({scaleMargins:{top:.2,bottom:.2}}),i}createVolumeSeries(){const e=this.chart.addHistogramSeries({color:"#26a69a",priceFormat:{type:"volume"},priceScaleId:"volume_scale"});return e.priceScale().applyOptions({scaleMargins:{top:.8,bottom:0}}),e}createLineSeries(e,t){const{group:i,legendSymbol:s="▨",...n}=t,o=this.chart.addLineSeries(n);this._seriesList.push(o);const r=o.options().color||"rgba(255,0,0,1)",a=r.startsWith("rgba")?r.replace(/[^,]+(?=\))/,"1"):r;if(i&&""!==i){const t=this.legend._groups.find((e=>e.name===i));t?(t.names.push(e),t.seriesList.push(o),t.solidColors.push(a),t.legendSymbols.push(s)):this.legend.makeSeriesGroup(i,[e],[o],[a],[s])}else this.legend.makeSeriesRow(e,o,[s],[a]);return{name:e,series:o}}createHistogramSeries(e,t){const{group:i,legendSymbol:s="▨",...n}=t,o=this.chart.addHistogramSeries(n);this._seriesList.push(o);const r=o.options().color||"rgba(255,0,0,1)",a=r.startsWith("rgba")?r.replace(/[^,]+(?=\))/,"1"):r;if(i&&""!==i){const t=this.legend._groups.find((e=>e.name===i));t?(t.names.push(e),t.seriesList.push(o),t.solidColors.push(a),t.legendSymbols.push(s)):this.legend.makeSeriesGroup(i,[e],[o],[a],[s])}else this.legend.makeSeriesRow(e,o,[s],[a]);return{name:e,series:o}}createAreaSeries(e,t){const{group:i,legendSymbol:s="▨",...n}=t,o=this.chart.addAreaSeries(n);this._seriesList.push(o);const r=o.options().color||"rgba(255,0,0,1)",a=r.startsWith("rgba")?r.replace(/[^,]+(?=\))/,"1"):r;if(i&&""!==i){const t=this.legend._groups.find((e=>e.name===i));t?(t.names.push(e),t.seriesList.push(o),t.solidColors.push(a),t.legendSymbols.push(s)):this.legend.makeSeriesGroup(i,[e],[o],[a],[s])}else this.legend.makeSeriesRow(e,o,[s],[a]);return{name:e,series:o}}createBarSeries(e,t){const{group:i,legendSymbol:s=["▨","▨"],...n}=t,o=this.chart.addBarSeries(n);this._seriesList.push(o);const r=o.options().upColor||"rgba(0,255,0,1)",a=o.options().downColor||"rgba(255,0,0,1)";if(i&&""!==i){const t=this.legend._groups.find((e=>e.name===i));t?(t.names.push(e),t.seriesList.push(o),t.solidColors.push(r,a),t.legendSymbols.push(s[0],s[1])):this.legend.makeSeriesGroup(i,[e],[o],[r,a],s)}else this.legend.makeSeriesRow(e,o,s,[r,a]);return{name:e,series:o}}createToolBox(){this.toolBox=new $(this.id,this.chart,this.series,this.commandFunctions),this.div.appendChild(this.toolBox.div)}createTopBar(){return this._topBar=new V(this),this.wrapper.prepend(this._topBar._div),this._topBar}toJSON(){const{chart:e,...t}=this;return t}static syncCharts(e,t,i=!1){function s(e,t){t?(e.chart.setCrosshairPosition(t.value||t.close,t.time,e.series),e.legend.legendHandler(t,!0)):e.chart.clearCrosshairPosition()}function n(e,t){return t.time&&t.seriesData.get(e)||null}const o=e.chart.timeScale(),r=t.chart.timeScale(),a=e=>{e&&o.setVisibleLogicalRange(e)},l=e=>{e&&r.setVisibleLogicalRange(e)},d=i=>{s(t,n(e.series,i))},h=i=>{s(e,n(t.series,i))};let c=t;function p(e,t,s,n,o,r){e.wrapper.addEventListener("mouseover",(()=>{c!==e&&(c=e,t.chart.unsubscribeCrosshairMove(s),e.chart.subscribeCrosshairMove(n),i||(t.chart.timeScale().unsubscribeVisibleLogicalRangeChange(o),e.chart.timeScale().subscribeVisibleLogicalRangeChange(r)))}))}p(t,e,d,h,l,a),p(e,t,h,d,a,l),t.chart.subscribeCrosshairMove(h);const u=r.getVisibleLogicalRange();u&&o.setVisibleLogicalRange(u),i||t.chart.timeScale().subscribeVisibleLogicalRangeChange(a)}static makeSearchBox(e){const t=document.createElement("div");t.classList.add("searchbox"),t.style.display="none";const i=document.createElement("div");i.innerHTML='';const s=document.createElement("input");return s.type="text",t.appendChild(i),t.appendChild(s),e.div.appendChild(t),e.commandFunctions.push((i=>window.handlerInFocus===e.id&&!window.textBoxFocused&&("none"===t.style.display?!!/^[a-zA-Z0-9]$/.test(i.key)&&(t.style.display="flex",s.focus(),!0):("Enter"===i.key||"Escape"===i.key)&&("Enter"===i.key&&window.callbackFunction(`search${e.id}_~_${s.value}`),t.style.display="none",s.value="",!0)))),s.addEventListener("input",(()=>s.value=s.value.toUpperCase())),{window:t,box:s}}static makeSpinner(e){e.spinner=document.createElement("div"),e.spinner.classList.add("spinner"),e.wrapper.appendChild(e.spinner);let t=0;!function i(){e.spinner&&(t+=10,e.spinner.style.transform=`translate(-50%, -50%) rotate(${t}deg)`,requestAnimationFrame(i))}()}static _styleMap={"--bg-color":"backgroundColor","--hover-bg-color":"hoverBackgroundColor","--click-bg-color":"clickBackgroundColor","--active-bg-color":"activeBackgroundColor","--muted-bg-color":"mutedBackgroundColor","--border-color":"borderColor","--color":"color","--active-color":"activeColor"};static setRootStyles(e){const t=document.documentElement.style;for(const[i,s]of Object.entries(this._styleMap))t.setProperty(i,e[s])}},e.HorizontalLine=b,e.Legend=r,e.RayLine=R,e.Table=class{_div;callbackName;borderColor;borderWidth;table;rows={};headings;widths;alignments;footer;header;constructor(e,t,i,s,n,o,r=!1,a,l,d,h,c){this._div=document.createElement("div"),this.callbackName=null,this.borderColor=l,this.borderWidth=d,r?(this._div.style.position="absolute",this._div.style.cursor="move"):(this._div.style.position="relative",this._div.style.float=o),this._div.style.zIndex="2000",this.reSize(e,t),this._div.style.display="flex",this._div.style.flexDirection="column",this._div.style.borderRadius="5px",this._div.style.color="white",this._div.style.fontSize="12px",this._div.style.fontVariantNumeric="tabular-nums",this.table=document.createElement("table"),this.table.style.width="100%",this.table.style.borderCollapse="collapse",this._div.style.overflow="hidden",this.headings=i,this.widths=s.map((e=>100*e+"%")),this.alignments=n;let p=this.table.createTHead().insertRow();for(let e=0;e0?c[e]:a,t.style.color=h[e],p.appendChild(t)}let u,_,m=document.createElement("div");if(m.style.overflowY="auto",m.style.overflowX="hidden",m.style.backgroundColor=a,m.appendChild(this.table),this._div.appendChild(m),window.containerDiv.appendChild(this._div),!r)return;let g=e=>{this._div.style.left=e.clientX-u+"px",this._div.style.top=e.clientY-_+"px"},v=()=>{document.removeEventListener("mousemove",g),document.removeEventListener("mouseup",v)};this._div.addEventListener("mousedown",(e=>{u=e.clientX-this._div.offsetLeft,_=e.clientY-this._div.offsetTop,document.addEventListener("mousemove",g),document.addEventListener("mouseup",v)}))}divToButton(e,t){e.addEventListener("mouseover",(()=>e.style.backgroundColor="rgba(60, 60, 60, 0.6)")),e.addEventListener("mouseout",(()=>e.style.backgroundColor="transparent")),e.addEventListener("mousedown",(()=>e.style.backgroundColor="rgba(60, 60, 60)")),e.addEventListener("click",(()=>window.callbackFunction(t))),e.addEventListener("mouseup",(()=>e.style.backgroundColor="rgba(60, 60, 60, 0.6)"))}newRow(e,t=!1){let i=this.table.insertRow();i.style.cursor="default";for(let s=0;s{e&&(window.cursor=e),document.body.style.cursor=window.cursor},e}({},LightweightCharts); From d3aa4860b385e6efa7fabbf8289ce07a27f71b21 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Tue, 12 Nov 2024 00:27:59 -0800 Subject: [PATCH 16/89] Re-Implement Legend Groups Cleaned up, restructured for easier interaction adding/removing --- src/general/legend.ts | 567 ++++++++++++++++++++++++++---------------- 1 file changed, 346 insertions(+), 221 deletions(-) diff --git a/src/general/legend.ts b/src/general/legend.ts index 0296d6b..fc23d1a 100644 --- a/src/general/legend.ts +++ b/src/general/legend.ts @@ -1,29 +1,33 @@ -import {AreaData, BarData, HistogramData, ISeriesApi, LineData, Logical, MouseEventParams, PriceFormatBuiltIn, PriceFormat, SeriesType } from "lightweight-charts"; +import {AreaData, BarData, HistogramData, ISeriesApi, LineData, Logical, MouseEventParams, PriceFormatBuiltIn, SeriesType } from "lightweight-charts"; +import { CustomCandleSeriesData } from "../custom-candle-series/data"; import { Handler } from "./handler"; +import { LegendItem } from "./global-params"; +type LegendEntry = LegendSeries | LegendGroup; -// Interfaces for the legend elements -interface LineElement { + + +interface LegendGroup { name: string; + seriesList: LegendItem[]; // Each `LegendItem` contains `colors`, `legendSymbol`, and `seriesType` div: HTMLDivElement; row: HTMLDivElement; toggle: HTMLDivElement; - series: ISeriesApi; - solid: string; - legendSymbol: string; // Add legend symbol for individual series } -// Interface for a group of series in the legend -interface LegendGroup { - name: string; - seriesList: ISeriesApi[]; +interface LegendSeries extends LegendItem { div: HTMLDivElement; row: HTMLDivElement; toggle: HTMLDivElement; - solidColors: string[]; - names: string[]; - legendSymbols: string[]; // Add array of legend symbols for grouped series } -// Define the SVG path data +const lastSeriesDataCache = new Map, any>(); +const lastGroupDataCache = new Map(); + + +function getLastData(series: ISeriesApi) { + return lastSeriesDataCache.get(series) || null; +} + + const openEye = ` - `; + export class Legend { private handler: Handler; public div: HTMLDivElement; @@ -69,7 +74,8 @@ export class Legend { private text: HTMLSpanElement; private candle: HTMLDivElement; - public _lines: LineElement[] = []; + private _items: LegendEntry[] = []; + public _lines: LegendSeries[] = []; public _groups: LegendGroup[] = []; constructor(handler: Handler) { @@ -102,10 +108,13 @@ export class Legend { this.div.appendChild(seriesWrapper); this.handler.div.appendChild(this.div); } - - legendItemFormat(num: number, decimal: number) { + legendItemFormat(num: number | undefined, decimal: number): string { + if (typeof num !== 'number' || isNaN(num)) { + return '-'; // Default display when data is missing + } return num.toFixed(decimal).toString().padStart(8, ' '); } + shorthandFormat(num: number) { const absNum = Math.abs(num); @@ -113,22 +122,36 @@ export class Legend { absNum >= 1000 ? (num / 1000).toFixed(1) + 'K' : num.toString().padStart(8, ' '); } - makeSeriesRow( - name: string, - series: ISeriesApi, - legendSymbol: string[] = ['▨'], - colors: string[] - ): HTMLDivElement { + makeSeriesRow(line: LegendItem): HTMLDivElement { const row = document.createElement('div'); row.style.display = 'flex'; row.style.alignItems = 'center'; const div = document.createElement('div'); - // Iterate over colors and symbols for multi-color support - div.innerHTML = legendSymbol - .map((symbol, index) => `${symbol}`) - .join(' ') + ` ${name}`; + const displayOCvalues = ['Bar', 'customCandle', 'htfCandle'].includes(line.seriesType || ''); + + if (displayOCvalues) { + const openPrice = '-'; + const closePrice = '-'; + + const upSymbol = line.legendSymbol[0] || '▨'; + const downSymbol = line.legendSymbol[1] || upSymbol; + const upColor = line.colors[0] || '#00FF00'; + const downColor = line.colors[1] || '#FF0000'; + + div.innerHTML = ` + ${upSymbol} + ${downSymbol} + ${line.name}: O ${openPrice}, + C ${closePrice} + `; + } else { + div.innerHTML = line.legendSymbol + .map((symbol, index) => `${symbol}`) + .join(' ') + ` ${line.name}`; + } + // Toggle visibility icon const toggle = document.createElement('div'); toggle.classList.add('legend-toggle-switch'); @@ -139,7 +162,7 @@ export class Legend { let visible = true; toggle.addEventListener('click', () => { visible = !visible; - series.applyOptions({ visible }); + line.series.applyOptions({ visible }); toggle.innerHTML = ''; toggle.appendChild(visible ? onIcon.cloneNode(true) : offIcon.cloneNode(true)); }); @@ -148,109 +171,214 @@ export class Legend { row.appendChild(toggle); this.seriesContainer.appendChild(row); - // Push the row and related information to the `_lines` array - this._lines.push({ - name, + // Add row, div, and toggle to line (LegendItem) + const legendSeries: LegendSeries = { + ...line, div, row, toggle, - series, - solid: colors[0], // Assume the first color is the main color for the series - legendSymbol: legendSymbol[0], // Store the primary legend symbol - }); + }; + + this._lines.push(legendSeries); return row; } - makeSeriesGroup( - groupName: string, - names: string[], - seriesList: ISeriesApi[], - colors: string[], - legendSymbols: string[] - ): HTMLDivElement { - const row = document.createElement('div'); - row.style.display = 'flex'; - row.style.alignItems = 'center'; + addLegendItem(item: LegendItem): HTMLDivElement { + let entry: LegendEntry; + + if (item.group) { + // Check if the group already exists + let group = this._groups.find(g => g.name === item.group); + if (group) { + // Add the item to the existing group + group.seriesList.push(item); + // Update the group's div content + entry = group; + return group.row; + } else { + // Create a new group with the item + const groupRow = this.makeSeriesGroup(item.group, [item]); + entry = this._groups[this._groups.length - 1]; // Get the newly added group + return groupRow; + } + } else { + // Add as an individual series + const seriesRow = this.makeSeriesRow(item); + entry = this._lines[this._lines.length - 1]; // Get the newly added series + return seriesRow; + } - const div = document.createElement('div'); - div.innerHTML = `${groupName}:`; + // Add the entry to _items + this._items.push(entry); + } + deleteLegendEntry(seriesName?: string, groupName?: string): void { + if (groupName && !seriesName) { + // Remove entire group + const groupIndex = this._groups.findIndex(group => group.name === groupName); + if (groupIndex !== -1) { + const legendGroup = this._groups[groupIndex]; - const toggle = document.createElement('div'); - toggle.classList.add('legend-toggle-switch'); + // Remove the group's DOM elements + this.seriesContainer.removeChild(legendGroup.row); - const onIcon = this.createSvgIcon(openEye); - const offIcon = this.createSvgIcon(closedEye); - toggle.appendChild(onIcon.cloneNode(true)); // Default to visible + // Optionally, remove all series in the group from the chart + // legendGroup.seriesList.forEach(item => item.series.remove()); - let visible = true; - toggle.addEventListener('click', () => { - visible = !visible; - seriesList.forEach(series => series.applyOptions({ visible })); - toggle.innerHTML = ''; // Clear toggle before appending new icon - toggle.appendChild(visible ? onIcon.cloneNode(true) : offIcon.cloneNode(true)); - }); + // Remove from the _groups array + this._groups.splice(groupIndex, 1); - // Build the legend content for each series in the group - let colorIndex = 0; // Separate index for colors and symbols to account for bar pairs - names.forEach((name, index) => { - const series = seriesList[index]; - const isBarSeries = series && series.seriesType() === 'Bar'; - - if (isBarSeries) { - // Use current color index for the up symbol/color, then increment to down symbol/color - const upSymbol = legendSymbols[colorIndex] || '▨'; - const downSymbol = legendSymbols[colorIndex + 1] || '▨'; - const upColor = colors[colorIndex]; - const downColor = colors[colorIndex + 1]; - - // Dual symbol and color formatting for bar series - div.innerHTML += ` - ${upSymbol} - ${downSymbol} - ${name}: - - `; + // Also remove from _items array + this._items = this._items.filter(entry => entry !== legendGroup); - // Increment color index by 2 for bar series (to account for up/down pair) - colorIndex += 2; + console.log(`Group "${groupName}" removed.`); } else { - // Single symbol and color for non-bar series - const singleSymbol = legendSymbols[colorIndex] || '▨'; - const singleColor = colors[colorIndex]; + console.warn(`Legend group with name "${groupName}" not found.`); + } + } else if (seriesName) { + // Remove individual series + let removed = false; + + if (groupName) { + // Remove from specific group + const group = this._groups.find(g => g.name === groupName); + if (group) { + const itemIndex = group.seriesList.findIndex(item => item.name === seriesName); + if (itemIndex !== -1) { + const seriesItem = group.seriesList[itemIndex]; + + // Remove from the group's seriesList + group.seriesList.splice(itemIndex, 1); + + // Update the group's legend content + + // If the group is now empty, remove it + if (group.seriesList.length === 0) { + this.seriesContainer.removeChild(group.row); + this._groups = this._groups.filter(g => g !== group); + this._items = this._items.filter(entry => entry !== group); + console.log(`Group "${groupName}" is empty and has been removed.`); + } + + // Optionally, remove the series from the chart + // seriesItem.series.remove(); + + removed = true; + console.log(`Series "${seriesName}" removed from group "${groupName}".`); + } + } else { + console.warn(`Legend group with name "${groupName}" not found.`); + } + } - div.innerHTML += ` - ${singleSymbol} - ${name}: - - `; + if (!removed) { + // Remove from _lines (individual legend items) + const seriesIndex = this._lines.findIndex(series => series.name === seriesName); + if (seriesIndex !== -1) { + const legendSeries = this._lines[seriesIndex]; - // Increment color index by 1 for non-bar series - colorIndex += 1; - } - }); + // Remove the DOM elements + this.seriesContainer.removeChild(legendSeries.row); - // Add div and toggle to row - row.appendChild(div); - row.appendChild(toggle); + // Remove from the _lines array + this._lines.splice(seriesIndex, 1); - // Append row to the series container - this.seriesContainer.appendChild(row); + // Also remove from _items array + this._items = this._items.filter(entry => entry !== legendSeries); - // Store group data - this._groups.push({ - name: groupName, - seriesList, - div, - row, - toggle, - solidColors: colors, - names, - legendSymbols, - }); + // Optionally, remove the series from the chart + // legendSeries.series.remove(); - return row; + removed = true; + console.log(`Series "${seriesName}" removed.`); + } + } + + if (!removed) { + console.warn(`Legend item with name "${seriesName}" not found.`); + } + } else { + console.warn(`No seriesName or groupName provided for deletion.`); + } } + makeSeriesGroup(groupName: string, items: LegendItem[]): HTMLDivElement { + // Check if the group already exists + let group = this._groups.find(g => g.name === groupName); + + if (group) { + // Add items to the existing group + group.seriesList.push(...items); + return group.row; + } else { + // Create a new group + const row = document.createElement('div'); + row.style.display = 'flex'; + row.style.alignItems = 'center'; + + const div = document.createElement('div'); + div.innerHTML = `${groupName}:`; + + const toggle = document.createElement('div'); + toggle.classList.add('legend-toggle-switch'); + const onIcon = this.createSvgIcon(openEye); + const offIcon = this.createSvgIcon(closedEye); + toggle.appendChild(onIcon.cloneNode(true)); + + let visible = true; + toggle.addEventListener('click', () => { + visible = !visible; + items.forEach(item => item.series.applyOptions({ visible })); + toggle.innerHTML = ''; + toggle.appendChild(visible ? onIcon.cloneNode(true) : offIcon.cloneNode(true)); + }); + + items.forEach(item => { + + const displayOCvalues = item.seriesType === 'Bar' || item.seriesType === 'customCandle' || item.seriesType === 'htfCandle'; + + if (displayOCvalues) { + const [upSymbol, downSymbol] = item.legendSymbol; + const [upColor, downColor] = item.colors; + // Dummy values (to be updated dynamically in the legend handler) + const openPrice = '-'; + const closePrice = '-'; + div.innerHTML += ` + ${upSymbol} + ${downSymbol} + ${name}: + O ${openPrice}, + C ${closePrice} + + `; + } else { + const color = item.colors[0] || '#000'; + const symbol = item.legendSymbol[0] || '▨'; + const price = '-'; // Dummy price + div.innerHTML += ` + ${symbol} + ${name}: ${price} + `; } + }); + + row.appendChild(div); + row.appendChild(toggle); + this.seriesContainer.appendChild(row); + + // Add the new group to `_groups` + const newGroup: LegendGroup = { + name: groupName, + seriesList: items, + div, + row, + toggle + }; + this._groups.push(newGroup); + + return row; + } + } private createSvgIcon(svgContent: string): SVGElement { const tempContainer = document.createElement('div'); @@ -258,20 +386,18 @@ export class Legend { const svgElement = tempContainer.querySelector('svg'); return svgElement as SVGElement; } - legendHandler(param: MouseEventParams, usingPoint = false) { if (!this.ohlcEnabled && !this.linesEnabled && !this.percentEnabled) return; - const options: any = this.handler.series.options(); if (!param.time) { this.candle.style.color = 'transparent'; this.candle.innerHTML = this.candle.innerHTML.replace(options['upColor'], '').replace(options['downColor'], ''); return; } - + let data: any; let logical: Logical | null = null; - + if (usingPoint) { const timeScale = this.handler.chart.timeScale(); const coordinate = timeScale.timeToCoordinate(param.time); @@ -280,18 +406,18 @@ export class Legend { } else { data = param.seriesData.get(this.handler.series); } - + + // Update the main candle legend content let str = ''; if (data) { - // OHLC Data if (this.ohlcEnabled) { str += `O ${this.legendItemFormat(data.open, this.handler.precision)} `; str += `| H ${this.legendItemFormat(data.high, this.handler.precision)} `; str += `| L ${this.legendItemFormat(data.low, this.handler.precision)} `; str += `| C ${this.legendItemFormat(data.close, this.handler.precision)} `; } - - // Percentage Movement + + // Display percentage move if enabled if (this.percentEnabled) { const percentMove = ((data.close - data.open) / data.open) * 100; const color = percentMove > 0 ? options['upColor'] : options['downColor']; @@ -299,13 +425,15 @@ export class Legend { str += this.colorBasedOnCandle ? `| ${percentStr}` : `| ${percentStr}`; } } + this.candle.innerHTML = str + ''; - - this.updateGroupLegend(param, logical, usingPoint); - this.updateSeriesLegend(param, logical, usingPoint); + + // Update group legend and series legend + this.updateGroupDisplay(param, logical, usingPoint); + this.updateSeriesDisplay(param, logical, usingPoint); } - private updateGroupLegend(param: MouseEventParams, logical: Logical | null, usingPoint: boolean) { + private updateGroupDisplay(param: MouseEventParams, logical: Logical | null, usingPoint: boolean) { this._groups.forEach((group) => { if (!this.linesEnabled) { group.row.style.display = 'none'; @@ -313,137 +441,134 @@ export class Legend { } group.row.style.display = 'flex'; - // Start building the legend text with the group name - let legendText = `${group.name}:`; - - // Track color index for bar-specific colors and symbols - let colorIndex = 0; - - // Iterate over each series in the group - group.seriesList.forEach((series, idx) => { - const seriesType = series.seriesType(); - let data; - - // Get data based on the current logical point or series data - if (usingPoint && logical) { - data = series.dataByIndex(logical); - } else { - data = param.seriesData.get(series); - } - - if (!data) return; // Skip if no data is available for this series + let legendText = `${group.name}: `; - // Retrieve price format for precision - const priceFormat = series.options().priceFormat as PriceFormatBuiltIn; - const name = group.names[idx]; - - if (seriesType === 'Bar') { - // Handle Bar series with open and close values and separate up/down symbols and colors - const barData = data as BarData; - const openPrice = this.legendItemFormat(barData.open, priceFormat.precision); - const closePrice = this.legendItemFormat(barData.close, priceFormat.precision); - - const upSymbol = group.legendSymbols[colorIndex] || '▨'; - const downSymbol = group.legendSymbols[colorIndex + 1] || '▨'; - const upColor = group.solidColors[colorIndex]; - const downColor = group.solidColors[colorIndex + 1]; + group.seriesList.forEach((seriesItem) => { + const data = param.seriesData.get(seriesItem.series) || getLastData(seriesItem.series); + if (!data) return; + + const seriesType = seriesItem.seriesType || 'Line'; + const name = seriesItem.name; + const priceFormat = seriesItem.series.options().priceFormat as PriceFormatBuiltIn; + + // Check if the series type supports OHLC values + const isOHLC = seriesType === 'Bar' || seriesType === 'customCandle' || seriesType === 'htfCandle'; + if (isOHLC) { + // Ensure properties are available or skip + const { open, close, high, low } = data; + if (open == null || close == null || high == null || low == null) return; + + //const { open, close } = data as CustomCandleSeriesData || {}; + if (open == null || close == null) { + legendText += `${name}: - `; + return; + } + + const openPrice = this.legendItemFormat(open, priceFormat.precision); + const closePrice = this.legendItemFormat(close, priceFormat.precision); + const isUp = close > open; + const color = isUp ? seriesItem.colors[0] : seriesItem.colors[1]; + const symbol = isUp ? seriesItem.legendSymbol[0] : seriesItem.legendSymbol[1]; - // Append Bar series info with open and close prices, and separate symbols/colors legendText += ` - ${upSymbol} - ${downSymbol} - ${name}: O ${openPrice}, C ${closePrice} - `; - - colorIndex += 2; // Increment color index by 2 for Bar series + ${symbol || '▨'} + ${name}: + O ${openPrice}, + C ${closePrice} + `; } else { - // Handle other series types that use a single `value` - const otherData = data as LineData | AreaData | HistogramData; - const price = this.legendItemFormat(otherData.value, priceFormat.precision); - - const symbol = group.legendSymbols[colorIndex] || '▨'; - const color = group.solidColors[colorIndex]; + // Handle series types with a single 'value' property + const valueData = data as LineData | AreaData | HistogramData || {}; + const value = valueData.value; + if (value == null) { + legendText += `${name}: - `; + return; + } + + const priceFormat = seriesItem.series.options().priceFormat as PriceFormatBuiltIn; + const formattedValue = this.legendItemFormat(value, priceFormat.precision); + const color = seriesItem.colors[0]; + const symbol = seriesItem.legendSymbol[0] || '▨'; - // Append non-Bar series info with single symbol and color legendText += ` - ${symbol} - ${name}: ${price} - `; - colorIndex += 1; // Increment color index by 1 for non-Bar series + ${symbol} + ${name}: ${formattedValue} `; } }); - // Update the group legend div with the constructed legend text + // Update the group's div content group.div.innerHTML = legendText; }); } - private updateSeriesLegend(param: MouseEventParams, logical: Logical | null, usingPoint: boolean) { + private updateSeriesDisplay(param: MouseEventParams, logical: Logical | null, usingPoint: boolean) { if (!this._lines || !this._lines.length) { console.error("No lines available to update legend."); return; } this._lines.forEach((e) => { - // Check if the line row should be displayed if (!this.linesEnabled) { e.row.style.display = 'none'; return; } e.row.style.display = 'flex'; - - // Determine series type and get the appropriate data - const seriesType = e.series.seriesType(); - let data; - - if (usingPoint && logical) { - data = e.series.dataByIndex(logical); - } else { - data = param.seriesData.get(e.series); - } - - // If no data is available, show a placeholder and continue + + const data = param.seriesData.get(e.series) || getLastData(e.series); if (!data) { e.div.innerHTML = `${e.name}: -`; return; } - - const priceFormat = e.series.options().priceFormat as PriceFormatBuiltIn; - let legendContent: string; - console.log(`Series: ${e.name}, Type: ${seriesType}, Data:`, data); - - if (seriesType === 'Bar') { - // Handle Bar series with open and close values - const barData = data as BarData; - const openPrice = this.legendItemFormat(barData.open, priceFormat.precision); - const closePrice = this.legendItemFormat(barData.close, priceFormat.precision); - - // Use specific symbols and colors for Bar series open/close display - const upSymbol = e.legendSymbol[0] || '▨'; - const downSymbol = e.legendSymbol[1] || '▨'; - const upColor = e.solid[0]; - const downColor = e.solid[1]; - - legendContent = ` - ${upSymbol} - ${downSymbol} - ${e.name}: O ${openPrice}, C ${closePrice} + + const seriesType = e.seriesType || 'Line'; + + // Check if the series type supports OHLC values + const isOHLC = ['Bar', 'customCandle', 'htfCandle'].includes(seriesType); + + if (isOHLC) { + const { open, close } = data as CustomCandleSeriesData; + if (open == null || close == null) { + e.div.innerHTML = `${e.name}: -`; + return; + } + + const priceFormat = e.series.options().priceFormat as PriceFormatBuiltIn; + const openPrice = this.legendItemFormat(open, priceFormat.precision); + const closePrice = this.legendItemFormat(close, priceFormat.precision); + const isUp = close > open; + const color = isUp ? e.colors[0] : e.colors[1]; + const symbol = isUp ? e.legendSymbol[0] : e.legendSymbol[1]; + + e.div.innerHTML = ` + ${symbol || '▨'} + ${e.name}: + O ${openPrice}, + C ${closePrice} `; } else if (seriesType === 'Histogram') { - // Handle Histogram with shorthand format const histogramData = data as HistogramData; + if (histogramData.value == null) { + e.div.innerHTML = `${e.name}: -`; + return; + } + const price = this.shorthandFormat(histogramData.value); - - legendContent = `${e.legendSymbol || '▨'} ${e.name}: ${price}`; + e.div.innerHTML = ` + ${e.legendSymbol[0] || '▨'} + ${e.name}: ${price}`; } else { - // Handle Line, Area, and other series types with a single value - const otherData = data as LineData | AreaData; - const price = this.legendItemFormat(otherData.value, priceFormat.precision); - - legendContent = `${e.legendSymbol || '▨'} ${e.name}: ${price}`; + // Handle series types with a single 'value' property + const valueData = data as LineData | AreaData; + if (valueData.value == null) { + e.div.innerHTML = `${e.name}: -`; + return; + } + + const priceFormat = e.series.options().priceFormat as PriceFormatBuiltIn; + const value = this.legendItemFormat(valueData.value, priceFormat.precision); + e.div.innerHTML = ` + ${e.legendSymbol[0] || '▨'} + ${e.name}: ${value}`; } - - // Update the legend row content - e.div.innerHTML = legendContent; }); - }} - + } +} From 24cbb2d5ea977887d012dc44e3844ddf5bc116d4 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Tue, 12 Nov 2024 00:31:33 -0800 Subject: [PATCH 17/89] Export LegendItem interface for use in handler, legend --- src/general/global-params.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/general/global-params.ts b/src/general/global-params.ts index dfc5717..1fca7a8 100644 --- a/src/general/global-params.ts +++ b/src/general/global-params.ts @@ -1,3 +1,5 @@ +import { ISeriesApi, SeriesType } from "lightweight-charts"; + export interface GlobalParams extends Window { pane: paneStyle; // TODO shouldnt need this cause of css variables handlerInFocus: string; @@ -19,6 +21,14 @@ interface paneStyle { activeColor: string; } +export interface LegendItem { + name: string; + series: ISeriesApi; + colors: string[]; + legendSymbol: string[]; + seriesType?: string; + group?: string; // Optional attribute to indicate the group the item belongs to +} export const paneStyleDefault: paneStyle = { backgroundColor: '#0c0d0f', hoverBackgroundColor: '#3c434c', From cb4146d02352a5aa35aae786a716ba2003990db8 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Tue, 12 Nov 2024 00:36:21 -0800 Subject: [PATCH 18/89] Adapt to changes made in legend.ts --- src/general/handler.ts | 430 ++++++++++++++++++++++------------------- 1 file changed, 226 insertions(+), 204 deletions(-) diff --git a/src/general/handler.ts b/src/general/handler.ts index 6355b18..acaca40 100644 --- a/src/general/handler.ts +++ b/src/general/handler.ts @@ -1,5 +1,6 @@ import { AreaStyleOptions, + BarData, BarStyleOptions, ColorType, CrosshairMode, @@ -7,6 +8,7 @@ import { HistogramStyleOptions, IChartApi, ISeriesApi, + ISeriesPrimitive, LineStyleOptions, LogicalRange, LogicalRangeChangeEventHandler, @@ -15,19 +17,21 @@ import { SeriesOptionsCommon, SeriesType, Time, - createChart + createChart, } from "lightweight-charts"; -import { GlobalParams, globalParamInit } from "./global-params"; +import { GlobalParams, globalParamInit, LegendItem } from "./global-params"; import { Legend } from "./legend"; import { ToolBox } from "./toolbox"; import { TopBar } from "./topbar"; - +//import { ProbabilityConeOverlay, ProbabilityConeOptions } from "../probability-cone/probability-cone"; export interface Scale{ width: number, height: number, } + + // Define specific options interfaces with optional group and legendSymbol properties interface LineSeriesOptions extends DeepPartial { group?: string; @@ -50,6 +54,8 @@ interface BarSeriesOptions extends DeepPartial[] = []; + public seriesMap: Map> = new Map(); // TODO find a better solution rather than the 'position' parameter constructor( @@ -120,223 +127,238 @@ export class Handler { } - reSize() { - let topBarOffset = this.scale.height !== 0 ? this._topBar?._div.offsetHeight || 0 : 0 - this.chart.resize(window.innerWidth * this.scale.width, (window.innerHeight * this.scale.height) - topBarOffset) - this.wrapper.style.width = `${100 * this.scale.width}%` - this.wrapper.style.height = `${100 * this.scale.height}%` - - // TODO definitely a better way to do this - if (this.scale.height === 0 || this.scale.width === 0) { - // if (this.legend.div.style.display == 'flex') this.legend.div.style.display = 'none' - if (this.toolBox) { - this.toolBox.div.style.display = 'none' + reSize() { + let topBarOffset = this.scale.height !== 0 ? this._topBar?._div.offsetHeight || 0 : 0 + this.chart.resize(window.innerWidth * this.scale.width, (window.innerHeight * this.scale.height) - topBarOffset) + this.wrapper.style.width = `${100 * this.scale.width}%` + this.wrapper.style.height = `${100 * this.scale.height}%` + + // TODO definitely a better way to do this + if (this.scale.height === 0 || this.scale.width === 0) { + // if (this.legend.div.style.display == 'flex') this.legend.div.style.display = 'none' + if (this.toolBox) { + this.toolBox.div.style.display = 'none' + } } - } - else { - // this.legend.div.style.display = 'flex' - if (this.toolBox) { - this.toolBox.div.style.display = 'flex' + else { + // this.legend.div.style.display = 'flex' + if (this.toolBox) { + this.toolBox.div.style.display = 'flex' + } } } - } - - private _createChart() { - return createChart(this.div, { - width: window.innerWidth * this.scale.width, - height: window.innerHeight * this.scale.height, - layout:{ - textColor: window.pane.color, - background: { - color: '#000000', - type: ColorType.Solid, + public primitives: Map = new Map(); // Map of plugin primitive instances by series name + private _createChart() { + return createChart(this.div, { + width: window.innerWidth * this.scale.width, + height: window.innerHeight * this.scale.height, + layout:{ + textColor: window.pane.color, + background: { + color: '#000000', + type: ColorType.Solid, + }, + fontSize: 12 }, - fontSize: 12 - }, - rightPriceScale: { - scaleMargins: {top: 0.3, bottom: 0.25}, - }, - timeScale: {timeVisible: true, secondsVisible: false}, - crosshair: { - mode: CrosshairMode.Normal, - vertLine: { - labelBackgroundColor: 'rgb(46, 46, 46)' + rightPriceScale: { + scaleMargins: {top: 0.3, bottom: 0.25}, }, - horzLine: { - labelBackgroundColor: 'rgb(55, 55, 55)' - } - }, - grid: { - vertLines: {color: 'rgba(29, 30, 38, 5)'}, - horzLines: {color: 'rgba(29, 30, 58, 5)'}, - }, - handleScroll: {vertTouchDrag: true}, - }) - } + timeScale: {timeVisible: true, secondsVisible: false}, + crosshair: { + mode: CrosshairMode.Normal, + vertLine: { + labelBackgroundColor: 'rgb(46, 46, 46)' + }, + horzLine: { + labelBackgroundColor: 'rgb(55, 55, 55)' + } + }, + grid: { + vertLines: {color: 'rgba(29, 30, 38, 5)'}, + horzLines: {color: 'rgba(29, 30, 58, 5)'}, + }, + handleScroll: {vertTouchDrag: true}, + }) + } - createCandlestickSeries() { - const up = 'rgba(39, 157, 130, 100)' - const down = 'rgba(200, 97, 100, 100)' - const candleSeries = this.chart.addCandlestickSeries({ - upColor: up, borderUpColor: up, wickUpColor: up, - downColor: down, borderDownColor: down, wickDownColor: down - }); - candleSeries.priceScale().applyOptions({ - scaleMargins: {top: 0.2, bottom: 0.2}, - }); - return candleSeries; - } + createCandlestickSeries() { + const up = 'rgba(39, 157, 130, 100)' + const down = 'rgba(200, 97, 100, 100)' + const candleSeries = this.chart.addCandlestickSeries({ + upColor: up, borderUpColor: up, wickUpColor: up, + downColor: down, borderDownColor: down, wickDownColor: down + }); + candleSeries.priceScale().applyOptions({ + scaleMargins: {top: 0.2, bottom: 0.2}, + }); + return candleSeries; + } - createVolumeSeries() { - const volumeSeries = this.chart.addHistogramSeries({ - color: '#26a69a', - priceFormat: {type: 'volume'}, - priceScaleId: 'volume_scale', - }) - volumeSeries.priceScale().applyOptions({ - scaleMargins: {top: 0.8, bottom: 0}, - }); - return volumeSeries; - } + createVolumeSeries() { + const volumeSeries = this.chart.addHistogramSeries({ + color: '#26a69a', + priceFormat: {type: 'volume'}, + priceScaleId: 'volume_scale', + }) + volumeSeries.priceScale().applyOptions({ + scaleMargins: {top: 0.8, bottom: 0}, + }); + return volumeSeries; + } - createLineSeries( - name: string, - options: LineSeriesOptions - ): { name: string; series: ISeriesApi } { - const { group, legendSymbol = '▨', ...lineOptions } = options; - const line = this.chart.addLineSeries(lineOptions); - this._seriesList.push(line); - - const color = line.options().color || 'rgba(255,0,0,1)'; - const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; - - if (!group || group === '') { - this.legend.makeSeriesRow(name, line, [legendSymbol], [solidColor]); - } else { - const existingGroup = this.legend._groups.find(g => g.name === group); - if (existingGroup) { - existingGroup.names.push(name); - existingGroup.seriesList.push(line); - existingGroup.solidColors.push(solidColor); // Single color - existingGroup.legendSymbols.push(legendSymbol); // Single symbol - } else { - this.legend.makeSeriesGroup(group, [name], [line], [solidColor], [legendSymbol]); - } + createLineSeries( + name: string, + options: LineSeriesOptions + ): { name: string; series: ISeriesApi } { + const { group, legendSymbol = '▨', ...lineOptions } = options; + const line = this.chart.addLineSeries(lineOptions); + this._seriesList.push(line); + this.seriesMap.set(name, line); + + const color = line.options().color || 'rgba(255,0,0,1)'; + const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; + + const legendItem: LegendItem = { + name, + series: line, + colors: [solidColor], + legendSymbol: [legendSymbol], + seriesType: "Line", + group, + }; + + this.legend.addLegendItem(legendItem); + + return { name, series: line }; } - - return { name, series: line }; - } - - createHistogramSeries( - name: string, - options: HistogramSeriesOptions - ): { name: string; series: ISeriesApi } { - const { group, legendSymbol = '▨', ...histogramOptions } = options; - const histogram = this.chart.addHistogramSeries(histogramOptions); - this._seriesList.push(histogram); - - const color = histogram.options().color || 'rgba(255,0,0,1)'; - const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; - - if (!group || group === '') { - this.legend.makeSeriesRow(name, histogram, [legendSymbol], [solidColor]); - } else { - const existingGroup = this.legend._groups.find(g => g.name === group); - if (existingGroup) { - existingGroup.names.push(name); - existingGroup.seriesList.push(histogram); - existingGroup.solidColors.push(solidColor); // Single color - existingGroup.legendSymbols.push(legendSymbol); // Single symbol - } else { - this.legend.makeSeriesGroup(group, [name], [histogram], [solidColor], [legendSymbol]); - } + + + createHistogramSeries( + name: string, + options: HistogramSeriesOptions + ): { name: string; series: ISeriesApi } { + const { group, legendSymbol = '▨', ...histogramOptions } = options; + const histogram = this.chart.addHistogramSeries(histogramOptions); + this._seriesList.push(histogram); + this.seriesMap.set(name, histogram); + + const color = histogram.options().color || 'rgba(255,0,0,1)'; + const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; + + const legendItem: LegendItem = { + name, + series: histogram, + colors: [solidColor], + legendSymbol: [legendSymbol], + seriesType: "Histogram", + group, + }; + + this.legend.addLegendItem(legendItem); + + return { name, series: histogram }; } - - return { name, series: histogram }; - } - - createAreaSeries( - name: string, - options: AreaSeriesOptions - ): { name: string; series: ISeriesApi } { - const { group, legendSymbol = '▨', ...areaOptions } = options; - const area = this.chart.addAreaSeries(areaOptions); - this._seriesList.push(area); - - const color = area.options().color || 'rgba(255,0,0,1)'; - const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; - - if (!group || group === '') { - this.legend.makeSeriesRow(name, area, [legendSymbol], [solidColor]); - } else { - const existingGroup = this.legend._groups.find(g => g.name === group); - if (existingGroup) { - existingGroup.names.push(name); - existingGroup.seriesList.push(area); - existingGroup.solidColors.push(solidColor); // Single color - existingGroup.legendSymbols.push(legendSymbol); // Single symbol - } else { - this.legend.makeSeriesGroup(group, [name], [area], [solidColor], [legendSymbol]); - } + + + createAreaSeries( + name: string, + options: AreaSeriesOptions + ): { name: string; series: ISeriesApi } { + const { group, legendSymbol = '▨', ...areaOptions } = options; + const area = this.chart.addAreaSeries(areaOptions); + this._seriesList.push(area); + this.seriesMap.set(name, area); + + const color = area.options().lineColor || 'rgba(255,0,0,1)'; + const solidColor = color.startsWith('rgba') ? color.replace(/[^,]+(?=\))/, '1') : color; + + const legendItem: LegendItem = { + name, + series: area, + colors: [solidColor], + legendSymbol: [legendSymbol], + seriesType: "Area", + group, + }; + + this.legend.addLegendItem(legendItem); + + return { name, series: area }; } - - return { name, series: area }; - } - - createBarSeries( - name: string, - options: BarSeriesOptions - ): { name: string; series: ISeriesApi } { - const { group, legendSymbol = ['▨', '▨'], ...barOptions } = options; - const bar = this.chart.addBarSeries(barOptions); - this._seriesList.push(bar); - - // Extract upColor and downColor, with default values - const upColor = (bar.options() as any).upColor || 'rgba(0,255,0,1)'; // Default green - const downColor = (bar.options() as any).downColor || 'rgba(255,0,0,1)'; // Default red - - if (!group || group === '') { - // Pass both symbols and colors to makeSeriesRow for standalone bars - this.legend.makeSeriesRow(name, bar, legendSymbol, [upColor, downColor]); - } else { - const existingGroup = this.legend._groups.find(g => g.name === group); - if (existingGroup) { - existingGroup.names.push(name); - existingGroup.seriesList.push(bar); - existingGroup.solidColors.push(upColor, downColor); // Add both colors - existingGroup.legendSymbols.push(legendSymbol[0], legendSymbol[1]); // Add both symbols + + + + createBarSeries( + name: string, + options: BarSeriesOptions + ): { name: string; series: ISeriesApi } { + const { group, legendSymbol = ['▨', '▨'], ...barOptions } = options; + const bar = this.chart.addBarSeries(barOptions); + this._seriesList.push(bar); + this.seriesMap.set(name, bar); + + const upColor = (bar.options() as any).upColor || 'rgba(0,255,0,1)'; + const downColor = (bar.options() as any).downColor || 'rgba(255,0,0,1)'; + + const legendItem: LegendItem = { + name, + series: bar, + colors: [upColor, downColor], + legendSymbol: legendSymbol, + seriesType: "Bar", + group, + }; + + this.legend.addLegendItem(legendItem); + + return { name, series: bar }; + } + + removeSeries(seriesName: string): void { + const series = this.seriesMap.get(seriesName); + if (series) { + // Remove the series from the chart + this.chart.removeSeries(series); + + // Remove from _seriesList + this._seriesList = this._seriesList.filter(s => s !== series); + + // Remove from seriesMap + this.seriesMap.delete(seriesName); + + // Remove from legend + this.legend.deleteLegendEntry(seriesName); + + // Remove any associated primitives + ['Tooltip', 'DeltaTooltip', 'probabilityCone'].forEach(primitiveType => { + this.detachPrimitive(seriesName, primitiveType as any); + }); + + console.log(`Series "${seriesName}" removed.`); } else { - this.legend.makeSeriesGroup( - group, - [name], - [bar], - [upColor, downColor], // Two colors for up/down - legendSymbol // Two symbols for up/down - ); + console.warn(`Series "${seriesName}" not found.`); } } - - return { name, series: bar }; - } - - - - createToolBox() { - this.toolBox = new ToolBox(this.id, this.chart, this.series, this.commandFunctions); - this.div.appendChild(this.toolBox.div); - } + - createTopBar() { - this._topBar = new TopBar(this); - this.wrapper.prepend(this._topBar._div) - return this._topBar; - } - toJSON() { - // Exclude the chart attribute from serialization - const {chart, ...serialized} = this; - return serialized; - } + createToolBox() { + this.toolBox = new ToolBox(this.id, this.chart, this.series, this.commandFunctions); + this.div.appendChild(this.toolBox.div); + } + + createTopBar() { + this._topBar = new TopBar(this); + this.wrapper.prepend(this._topBar._div) + return this._topBar; + } + + toJSON() { + // Exclude the chart attribute from serialization + const {chart, ...serialized} = this; + return serialized; + } + public static syncCharts(childChart:Handler, parentChart: Handler, crosshairOnly = false) { function crosshairHandler(chart: Handler, point: any) {//point: BarData | LineData) { From a02e92dde46c6d208c9967e43f85a89b73d08795 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Sat, 16 Nov 2024 14:17:20 +0000 Subject: [PATCH 19/89] Add Custom Candle Series Renderer with Aggregation and Advanced Drawing Features --- lightweight_charts/abstract.py | 140 ++++++- lightweight_charts/js/bundle.js | 2 +- lightweight_charts/util.py | 1 + package-lock.json | 323 +++++++++++++-- package.json | 12 +- src/general/handler.ts | 60 ++- src/general/legend.ts | 20 +- src/index.ts | 3 +- src/ohlc-series/helpers.ts | 52 +++ src/ohlc-series/ohlc-series.ts | 110 +++++ src/ohlc-series/renderer.ts | 703 ++++++++++++++++++++++++++++++++ 11 files changed, 1375 insertions(+), 51 deletions(-) create mode 100644 src/ohlc-series/helpers.ts create mode 100644 src/ohlc-series/ohlc-series.ts create mode 100644 src/ohlc-series/renderer.ts diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 6447696..28fddbf 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -12,7 +12,7 @@ from .topbar import TopBar from .util import ( BulkRunScript, Pane, Events, IDGen, as_enum, jbool, js_json, TIME, NUM, FLOAT, - LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, + LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CANDLE_SHAPE CROSSHAIR_MODE, PRICE_SCALE_MODE, marker_position, marker_shape, js_data, ) @@ -665,6 +665,90 @@ def delete(self): delete {self.id} ''') +class CustomCandle(SeriesCommon): + def __init__( + self, + chart, + name: str, + up_color: str , + down_color: str , + border_up_color: str, + border_down_color: str , + wick_up_color: str , + wick_down_color: str , + wick_visible: bool = True, + border_visible: bool= True, + radius: Optional[str] = 30, + shape: str = 'Rectangle', + combineCandles: int = 1, + line_width: int = 1, + line_style: LINE_STYLE = 'solid', + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: Union[str, List[str]] = ['⬤', '⬤'], + price_scale_id: Optional[str] = None, + + ): + super().__init__(chart, name) + self.up_color = up_color + self.down_color = down_color + self.group = group # Store group for legend grouping + self.legend_symbol = legend_symbol if isinstance(legend_symbol, list) else [legend_symbol, legend_symbol] + radius_value = radius if radius is not None else 3 + + # Define the radius function as a JavaScript function string if none provided + radius_func = f"function(barSpacing) {{ return barSpacing < {radius_value} ? 0 : barSpacing / {radius_value}; }}" + + # Run the JavaScript to initialize the series with the provided options + self.run_script(f''' + {self.id} = {chart.id}.createCustomOHLCSeries( + "{name}", + {{ + group: '{group}', + upColor: '{up_color}', + downColor: '{down_color}', + borderUpColor: '{border_up_color}', + borderDownColor: '{border_down_color}', + wickUpColor: '{wick_up_color or border_up_color}', + wickDownColor: '{wick_down_color or border_down_color}', + wickVisible: {jbool(wick_visible)}, + borderVisible: {jbool(border_visible)}, + radius: {radius_func}, + shape: '{shape}', + lastValueVisible: {jbool(price_label)}, + priceLineVisible: {jbool(price_line)}, + legendSymbol: {json.dumps(self.legend_symbol)}, + priceScaleId: {f'"{price_scale_id}"' if price_scale_id else 'undefined'}, + seriesType: "customCandle", + chandelierSize: {combineCandles}, + lineStyle: {as_enum(line_style, LINE_STYLE)}, + lineWidth: {line_width}, + + }} + ) + null''') + + def set(self, df: Optional[pd.DataFrame] = None): + if df is None or df.empty: + self.run_script(f'{self.id}.series.setData([])') + self.data = pd.DataFrame() + return + df = self._df_datetime_format(df) + self.data = df.copy() + self._last_bar = df.iloc[-1] + self.run_script(f'{self.id}.series.setData({js_data(df)})') + + def update(self, series: pd.Series): + series = self._series_datetime_format(series) + if series['time'] != self._last_bar['time']: + self.data.loc[self.data.index[-1]] = self._last_bar + self.data = pd.concat([self.data, series.to_frame().T], ignore_index=True) + self._chart.events.new_bar._emit(self) + + self._last_bar = series + self.run_script(f'{self.id}.series.update({js_data(series)})') + class Candlestick(SeriesCommon): def __init__(self, chart: 'AbstractChart'): @@ -966,7 +1050,59 @@ def create_bar( price_line, price_label, group, legend_symbol, price_scale_id ) - + def create_custom_candle( + self, + name: str = '', + up_color: str = None, + down_color: str = None, + border_up_color='rgba(0,255,0,1)', + border_down_color='rgba(255,0,0,1)', + wick_up_color='rgba(0,255,0,1)', + wick_down_color='rgba(255,0,0,1)', + wick_visible: bool = True, + border_visible: bool = True, + rounded_radius: Union[float, int] = 100, + shape: Literal[CANDLE_SHAPE] = "Rectangle", + combineCandles: int = 1, + line_width: int = 1, + line_style: LINE_STYLE = 'solid', + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: Union[str, List[str]] = ['⑃', '⑂'], + price_scale_id: Optional[str] = None, + ) -> CustomCandle: + """ + Creates and returns a CustomCandle object. + """ + # Validate that legend_symbol is either a string or a list of two strings + if not isinstance(legend_symbol, (str, list)): + raise TypeError("legend_symbol must be a string or list of strings for CustomCandle series.") + if isinstance(legend_symbol, list) and len(legend_symbol) != 2: + raise ValueError("legend_symbol list must contain exactly two symbols for CustomCandle series.") + + return CustomCandle( + self, + name=name, + up_color=up_color or border_up_color, + down_color=down_color or border_down_color, + border_up_color=border_up_color or up_color, + border_down_color=border_down_color or down_color, + wick_up_color=wick_up_color or border_up_color or border_up_color, + wick_down_color=wick_down_color or border_down_color or border_down_color, + wick_visible=wick_visible, + border_visible=border_visible, + radius=rounded_radius, + shape=shape, + combineCandles=combineCandles, + line_style= line_style, + line_width= line_width, + price_line=price_line, + price_label=price_label, + group=group, + legend_symbol=legend_symbol, + price_scale_id=price_scale_id, + ) def lines(self) -> List[Line]: diff --git a/lightweight_charts/js/bundle.js b/lightweight_charts/js/bundle.js index 3544737..6e5c40d 100644 --- a/lightweight_charts/js/bundle.js +++ b/lightweight_charts/js/bundle.js @@ -1 +1 @@ -var Lib=function(e,t){"use strict";const i={backgroundColor:"#0c0d0f",hoverBackgroundColor:"#3c434c",clickBackgroundColor:"#50565E",activeBackgroundColor:"rgba(0, 122, 255, 0.7)",mutedBackgroundColor:"rgba(0, 122, 255, 0.3)",borderColor:"#3C434C",color:"#d8d9db",activeColor:"#ececed"};function s(){window.pane={...i},window.containerDiv=document.getElementById("container")||document.createElement("div"),window.setCursor=e=>{e&&(window.cursor=e),document.body.style.cursor=window.cursor},window.cursor="default",window.textBoxFocused=!1}const n='\n\n \n \n\n',o='\n\n \n \n \n\n';class r{handler;div;seriesContainer;ohlcEnabled=!1;percentEnabled=!1;linesEnabled=!1;colorBasedOnCandle=!1;text;candle;_lines=[];_groups=[];constructor(e){this.handler=e,this.div=document.createElement("div"),this.div.classList.add("legend"),this.seriesContainer=document.createElement("div"),this.text=document.createElement("span"),this.candle=document.createElement("div"),this.setupLegend(),this.legendHandler=this.legendHandler.bind(this),e.chart.subscribeCrosshairMove(this.legendHandler)}setupLegend(){this.div.style.maxWidth=100*this.handler.scale.width-8+"vw",this.div.style.display="none";const e=document.createElement("div");e.style.display="flex",e.style.flexDirection="row",this.seriesContainer.classList.add("series-container"),this.text.style.lineHeight="1.8",e.appendChild(this.seriesContainer),this.div.appendChild(this.text),this.div.appendChild(this.candle),this.div.appendChild(e),this.handler.div.appendChild(this.div)}legendItemFormat(e,t){return e.toFixed(t).toString().padStart(8," ")}shorthandFormat(e){const t=Math.abs(e);return t>=1e6?(e/1e6).toFixed(1)+"M":t>=1e3?(e/1e3).toFixed(1)+"K":e.toString().padStart(8," ")}makeSeriesRow(e,t,i=["▨"],s){const r=document.createElement("div");r.style.display="flex",r.style.alignItems="center";const a=document.createElement("div");a.innerHTML=i.map(((e,t)=>`${e}`)).join(" ")+` ${e}`;const l=document.createElement("div");l.classList.add("legend-toggle-switch");const d=this.createSvgIcon(n),h=this.createSvgIcon(o);l.appendChild(d.cloneNode(!0));let c=!0;return l.addEventListener("click",(()=>{c=!c,t.applyOptions({visible:c}),l.innerHTML="",l.appendChild(c?d.cloneNode(!0):h.cloneNode(!0))})),r.appendChild(a),r.appendChild(l),this.seriesContainer.appendChild(r),this._lines.push({name:e,div:a,row:r,toggle:l,series:t,solid:s[0],legendSymbol:i[0]}),r}makeSeriesGroup(e,t,i,s,r){const a=document.createElement("div");a.style.display="flex",a.style.alignItems="center";const l=document.createElement("div");l.innerHTML=`${e}:`;const d=document.createElement("div");d.classList.add("legend-toggle-switch");const h=this.createSvgIcon(n),c=this.createSvgIcon(o);d.appendChild(h.cloneNode(!0));let p=!0;d.addEventListener("click",(()=>{p=!p,i.forEach((e=>e.applyOptions({visible:p}))),d.innerHTML="",d.appendChild(p?h.cloneNode(!0):c.cloneNode(!0))}));let u=0;return t.forEach(((e,t)=>{const n=i[t];if(n&&"Bar"===n.seriesType()){const t=r[u]||"▨",i=r[u+1]||"▨",n=s[u],o=s[u+1];l.innerHTML+=`\n ${t}\n ${i}\n ${e}: -\n `,u+=2}else{const t=r[u]||"▨",i=s[u];l.innerHTML+=`\n ${t}\n ${e}: -\n `,u+=1}})),a.appendChild(l),a.appendChild(d),this.seriesContainer.appendChild(a),this._groups.push({name:e,seriesList:i,div:l,row:a,toggle:d,solidColors:s,names:t,legendSymbols:r}),a}createSvgIcon(e){const t=document.createElement("div");t.innerHTML=e.trim();return t.querySelector("svg")}legendHandler(e,t=!1){if(!this.ohlcEnabled&&!this.linesEnabled&&!this.percentEnabled)return;const i=this.handler.series.options();if(!e.time)return this.candle.style.color="transparent",void(this.candle.innerHTML=this.candle.innerHTML.replace(i.upColor,"").replace(i.downColor,""));let s,n=null;if(t){const t=this.handler.chart.timeScale(),i=t.timeToCoordinate(e.time);i&&(n=t.coordinateToLogical(i.valueOf())),n&&(s=this.handler.series.dataByIndex(n.valueOf()))}else s=e.seriesData.get(this.handler.series);let o='';if(s&&(this.ohlcEnabled&&(o+=`O ${this.legendItemFormat(s.open,this.handler.precision)} `,o+=`| H ${this.legendItemFormat(s.high,this.handler.precision)} `,o+=`| L ${this.legendItemFormat(s.low,this.handler.precision)} `,o+=`| C ${this.legendItemFormat(s.close,this.handler.precision)} `),this.percentEnabled)){const e=(s.close-s.open)/s.open*100,t=e>0?i.upColor:i.downColor,n=`${e>=0?"+":""}${e.toFixed(2)} %`;o+=this.colorBasedOnCandle?`| ${n}`:`| ${n}`}this.candle.innerHTML=o+"",this.updateGroupLegend(e,n,t),this.updateSeriesLegend(e,n,t)}updateGroupLegend(e,t,i){this._groups.forEach((s=>{if(!this.linesEnabled)return void(s.row.style.display="none");s.row.style.display="flex";let n=`${s.name}:`,o=0;s.seriesList.forEach(((r,a)=>{const l=r.seriesType();let d;if(d=i&&t?r.dataByIndex(t):e.seriesData.get(r),!d)return;const h=r.options().priceFormat,c=s.names[a];if("Bar"===l){const e=d,t=this.legendItemFormat(e.open,h.precision),i=this.legendItemFormat(e.close,h.precision),r=s.legendSymbols[o]||"▨",a=s.legendSymbols[o+1]||"▨",l=s.solidColors[o],p=s.solidColors[o+1];n+=`\n ${r}\n ${a}\n ${c}: O ${t}, C ${i}\n `,o+=2}else{const e=d,t=this.legendItemFormat(e.value,h.precision),i=s.legendSymbols[o]||"▨",r=s.solidColors[o];n+=`\n ${i}\n ${c}: ${t}\n `,o+=1}})),s.div.innerHTML=n}))}updateSeriesLegend(e,t,i){this._lines&&this._lines.length?this._lines.forEach((s=>{if(!this.linesEnabled)return void(s.row.style.display="none");s.row.style.display="flex";const n=s.series.seriesType();let o;if(o=i&&t?s.series.dataByIndex(t):e.seriesData.get(s.series),!o)return void(s.div.innerHTML=`${s.name}: -`);const r=s.series.options().priceFormat;let a;if(console.log(`Series: ${s.name}, Type: ${n}, Data:`,o),"Bar"===n){const e=o,t=this.legendItemFormat(e.open,r.precision),i=this.legendItemFormat(e.close,r.precision),n=s.legendSymbol[0]||"▨",l=s.legendSymbol[1]||"▨";a=`\n ${n}\n ${l}\n ${s.name}: O ${t}, C ${i}\n `}else if("Histogram"===n){const e=o,t=this.shorthandFormat(e.value);a=`${s.legendSymbol||"▨"} ${s.name}: ${t}`}else{const e=o,t=this.legendItemFormat(e.value,r.precision);a=`${s.legendSymbol||"▨"} ${s.name}: ${t}`}s.div.innerHTML=a})):console.error("No lines available to update legend.")}}function a(e){if(void 0===e)throw new Error("Value is undefined");return e}class l{_chart=void 0;_series=void 0;requestUpdate(){this._requestUpdate&&this._requestUpdate()}_requestUpdate;attached({chart:e,series:t,requestUpdate:i}){this._chart=e,this._series=t,this._series.subscribeDataChanged(this._fireDataUpdated),this._requestUpdate=i,this.requestUpdate()}detached(){this._chart=void 0,this._series=void 0,this._requestUpdate=void 0}get chart(){return a(this._chart)}get series(){return a(this._series)}_fireDataUpdated(e){this.dataUpdated&&this.dataUpdated(e)}}const d={lineColor:"#1E80F0",lineStyle:t.LineStyle.Solid,width:4};var h;!function(e){e[e.NONE=0]="NONE",e[e.HOVERING=1]="HOVERING",e[e.DRAGGING=2]="DRAGGING",e[e.DRAGGINGP1=3]="DRAGGINGP1",e[e.DRAGGINGP2=4]="DRAGGINGP2",e[e.DRAGGINGP3=5]="DRAGGINGP3",e[e.DRAGGINGP4=6]="DRAGGINGP4"}(h||(h={}));class c extends l{_paneViews=[];_options;_points=[];_state=h.NONE;_startDragPoint=null;_latestHoverPoint=null;static _mouseIsDown=!1;static hoveredObject=null;static lastHoveredObject=null;_listeners=[];constructor(e){super(),this._options={...d,...e}}updateAllViews(){this._paneViews.forEach((e=>e.update()))}paneViews(){return this._paneViews}applyOptions(e){this._options={...this._options,...e},this.requestUpdate()}updatePoints(...e){for(let t=0;ti.name===e&&i.listener===t));this._listeners.splice(this._listeners.indexOf(i),1)}_handleHoverInteraction(e){if(this._latestHoverPoint=e.point,c._mouseIsDown)this._handleDragInteraction(e);else if(this._mouseIsOverDrawing(e)){if(this._state!=h.NONE)return;this._moveToState(h.HOVERING),c.hoveredObject=c.lastHoveredObject=this}else{if(this._state==h.NONE)return;this._moveToState(h.NONE),c.hoveredObject===this&&(c.hoveredObject=null)}}static _eventToPoint(e,t){if(!t||!e.point||!e.logical)return null;const i=t.coordinateToPrice(e.point.y);return null==i?null:{time:e.time||null,logical:e.logical,price:i.valueOf()}}static _getDiff(e,t){return{logical:e.logical-t.logical,price:e.price-t.price}}_addDiffToPoint(e,t,i){e&&(e.logical=e.logical+t,e.price=e.price+i,e.time=this.series.dataByIndex(e.logical)?.time||null)}_handleMouseDownInteraction=()=>{c._mouseIsDown=!0,this._onMouseDown()};_handleMouseUpInteraction=()=>{c._mouseIsDown=!1,this._moveToState(h.HOVERING)};_handleDragInteraction(e){if(this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1&&this._state!=h.DRAGGINGP2&&this._state!=h.DRAGGINGP3&&this._state!=h.DRAGGINGP4)return;const t=c._eventToPoint(e,this.series);if(!t)return;this._startDragPoint=this._startDragPoint||t;const i=c._getDiff(t,this._startDragPoint);this._onDrag(i),this.requestUpdate(),this._startDragPoint=t}}class p{_options;constructor(e){this._options=e}}class u extends p{_p1;_p2;_hovered;constructor(e,t,i,s){super(i),this._p1=e,this._p2=t,this._hovered=s}_getScaledCoordinates(e){return null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y?null:{x1:Math.round(this._p1.x*e.horizontalPixelRatio),y1:Math.round(this._p1.y*e.verticalPixelRatio),x2:Math.round(this._p2.x*e.horizontalPixelRatio),y2:Math.round(this._p2.y*e.verticalPixelRatio)}}_drawEndCircle(e,t,i){e.context.fillStyle="#000",e.context.beginPath(),e.context.arc(t,i,9,0,2*Math.PI),e.context.stroke(),e.context.fill()}}function _(e,i){const s={[t.LineStyle.Solid]:[],[t.LineStyle.Dotted]:[e.lineWidth,e.lineWidth],[t.LineStyle.Dashed]:[2*e.lineWidth,2*e.lineWidth],[t.LineStyle.LargeDashed]:[6*e.lineWidth,6*e.lineWidth],[t.LineStyle.SparseDotted]:[e.lineWidth,4*e.lineWidth]}[i];e.setLineDash(s)}class m extends p{_point={x:null,y:null};constructor(e,t){super(t),this._point=e}draw(e){e.useBitmapCoordinateSpace((e=>{if(null==this._point.y)return;const t=e.context,i=Math.round(this._point.y*e.verticalPixelRatio),s=this._point.x?this._point.x*e.horizontalPixelRatio:0;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,_(t,this._options.lineStyle),t.beginPath(),t.moveTo(s,i),t.lineTo(e.bitmapSize.width,i),t.stroke()}))}}class g{_source;constructor(e){this._source=e}}class v extends g{_p1={x:null,y:null};_p2={x:null,y:null};_source;constructor(e){super(e),this._source=e}update(){if(!this._source.p1||!this._source.p2)return;const e=this._source.series,t=e.priceToCoordinate(this._source.p1.price),i=e.priceToCoordinate(this._source.p2.price),s=this._getX(this._source.p1),n=this._getX(this._source.p2);this._p1={x:s,y:t},this._p2={x:n,y:i}}_getX(e){return this._source.chart.timeScale().logicalToCoordinate(e.logical)}}class y extends g{_source;_point={x:null,y:null};constructor(e){super(e),this._source=e}update(){const e=this._source._point,t=this._source.chart.timeScale(),i=this._source.series;"RayLine"==this._source._type&&(this._point.x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical)),this._point.y=i.priceToCoordinate(e.price)}renderer(){return new m(this._point,this._source._options)}}class w{_source;_y=null;_price=null;constructor(e){this._source=e}update(){if(!this._source.series||!this._source._point)return;this._y=this._source.series.priceToCoordinate(this._source._point.price);const e=this._source.series.options().priceFormat.precision;this._price=this._source._point.price.toFixed(e).toString()}visible(){return!0}tickVisible(){return!0}coordinate(){return this._y??0}text(){return this._source._options.text||this._price||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class b extends c{_type="HorizontalLine";_paneViews;_point;_callbackName;_priceAxisViews;_startDragPoint=null;constructor(e,t,i=null){super(t),this._point=e,this._point.time=null,this._paneViews=[new y(this)],this._priceAxisViews=[new w(this)],this._callbackName=i}get points(){return[this._point]}updatePoints(...e){for(const t of e)t&&(this._point.price=t.price);this.requestUpdate()}updateAllViews(){this._paneViews.forEach((e=>e.update())),this._priceAxisViews.forEach((e=>e.update()))}priceAxisViews(){return this._priceAxisViews}_moveToState(e){switch(e){case h.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._addDiffToPoint(this._point,0,e.price),this.requestUpdate()}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this.series.priceToCoordinate(this._point.price);return!!i&&Math.abs(i-e.point.y){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class x{_chart;_series;_finishDrawingCallback=null;_drawings=[];_activeDrawing=null;_isDrawing=!1;_drawingType=null;constructor(e,t,i=null){this._chart=e,this._series=t,this._finishDrawingCallback=i,this._chart.subscribeClick(this._clickHandler),this._chart.subscribeCrosshairMove(this._moveHandler)}_clickHandler=e=>this._onClick(e);_moveHandler=e=>this._onMouseMove(e);beginDrawing(e){this._drawingType=e,this._isDrawing=!0}stopDrawing(){this._isDrawing=!1,this._activeDrawing=null}get drawings(){return this._drawings}addNewDrawing(e){this._series.attachPrimitive(e),this._drawings.push(e)}delete(e){if(null==e)return;const t=this._drawings.indexOf(e);-1!=t&&(this._drawings.splice(t,1),e.detach())}clearDrawings(){for(const e of this._drawings)e.detach();this._drawings=[]}repositionOnTime(){for(const e of this.drawings){const t=[];for(const i of e.points){if(!i){t.push(i);continue}const e=i.time?this._chart.timeScale().coordinateToLogical(this._chart.timeScale().timeToCoordinate(i.time)||0):i.logical;t.push({time:i.time,logical:e,price:i.price})}e.updatePoints(...t)}}_onClick(e){if(!this._isDrawing)return;const t=c._eventToPoint(e,this._series);if(t)if(null==this._activeDrawing){if(null==this._drawingType)return;this._activeDrawing=new this._drawingType(t,t),this._series.attachPrimitive(this._activeDrawing),this._drawingType==b&&this._onClick(e)}else{if(this._drawings.push(this._activeDrawing),this.stopDrawing(),!this._finishDrawingCallback)return;this._finishDrawingCallback()}}_onMouseMove(e){if(!e)return;for(const t of this._drawings)t._handleHoverInteraction(e);if(!this._isDrawing||!this._activeDrawing)return;const t=c._eventToPoint(e,this._series);t&&this._activeDrawing.updatePoints(null,t)}}class C extends u{constructor(e,t,i,s){super(e,t,i,s)}draw(e){e.useBitmapCoordinateSpace((e=>{if(null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y)return;const t=e.context,i=this._getScaledCoordinates(e);i&&(t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,_(t,this._options.lineStyle),t.beginPath(),t.moveTo(i.x1,i.y1),t.lineTo(i.x2,i.y2),t.stroke(),this._hovered&&(this._drawEndCircle(e,i.x1,i.y1),this._drawEndCircle(e,i.x2,i.y2)))}))}}class f extends v{constructor(e){super(e)}renderer(){return new C(this._p1,this._p2,this._source._options,this._source.hovered)}}class k extends c{_paneViews=[];_hovered=!1;constructor(e,t,i){super(),this.points.push(e),this.points.push(t),this._options={...d,...i}}setFirstPoint(e){this.updatePoints(e)}setSecondPoint(e){this.updatePoints(null,e)}get p1(){return this.points[0]}get p2(){return this.points[1]}get hovered(){return this._hovered}}class D extends k{_type="TrendLine";constructor(e,t,i){super(e,t,i),this._paneViews=[new f(this)]}_moveToState(e){switch(e){case h.NONE:document.body.style.cursor="default",this._hovered=!1,this.requestUpdate(),this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this.requestUpdate(),this._subscribe("mousedown",this._handleMouseDownInteraction),this._unsubscribe("mouseup",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGINGP1:case h.DRAGGINGP2:case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1||this._addDiffToPoint(this.p1,e.logical,e.price),this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP2||this._addDiffToPoint(this.p2,e.logical,e.price)}_onMouseDown(){this._startDragPoint=null;const e=this._latestHoverPoint;if(!e)return;const t=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(t.x&&i.x&&t.y&&i.y))return this._moveToState(h.DRAGGING);Math.abs(e.x-t.x)<10&&Math.abs(e.y-t.y)<10?this._moveToState(h.DRAGGINGP1):Math.abs(e.x-i.x)<10&&Math.abs(e.y-i.y)<10?this._moveToState(h.DRAGGINGP2):this._moveToState(h.DRAGGING)}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this._paneViews[0]._p1.x,s=this._paneViews[0]._p1.y,n=this._paneViews[0]._p2.x,o=this._paneViews[0]._p2.y;if(!(i&&n&&s&&o))return!1;const r=e.point.x,a=e.point.y;if(r<=Math.min(i,n)-t||r>=Math.max(i,n)+t)return!1;return Math.abs((o-s)*r-(n-i)*a+n*s-o*i)/Math.sqrt((o-s)**2+(n-i)**2)<=t}}class L extends u{constructor(e,t,i,s){super(e,t,i,s)}draw(e){e.useBitmapCoordinateSpace((e=>{const t=e.context,i=this._getScaledCoordinates(e);if(!i)return;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,_(t,this._options.lineStyle),t.fillStyle=this._options.fillColor;const s=Math.min(i.x1,i.x2),n=Math.min(i.y1,i.y2),o=Math.abs(i.x1-i.x2),r=Math.abs(i.y1-i.y2);t.strokeRect(s,n,o,r),t.fillRect(s,n,o,r),this._hovered&&(this._drawEndCircle(e,s,n),this._drawEndCircle(e,s+o,n),this._drawEndCircle(e,s+o,n+r),this._drawEndCircle(e,s,n+r))}))}}class S extends v{constructor(e){super(e)}renderer(){return new L(this._p1,this._p2,this._source._options,this._source.hovered)}}const E={fillEnabled:!0,fillColor:"rgba(255, 255, 255, 0.2)",...d};class G extends k{_type="Box";constructor(e,t,i){super(e,t,i),this._options={...E,...i},this._paneViews=[new S(this)]}_moveToState(e){switch(e){case h.NONE:document.body.style.cursor="default",this._hovered=!1,this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this._unsubscribe("mouseup",this._handleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGINGP1:case h.DRAGGINGP2:case h.DRAGGINGP3:case h.DRAGGINGP4:case h.DRAGGING:document.body.style.cursor="grabbing",document.body.addEventListener("mouseup",this._handleMouseUpInteraction),this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP1||this._addDiffToPoint(this.p1,e.logical,e.price),this._state!=h.DRAGGING&&this._state!=h.DRAGGINGP2||this._addDiffToPoint(this.p2,e.logical,e.price),this._state!=h.DRAGGING&&(this._state==h.DRAGGINGP3&&(this._addDiffToPoint(this.p1,e.logical,0),this._addDiffToPoint(this.p2,0,e.price)),this._state==h.DRAGGINGP4&&(this._addDiffToPoint(this.p1,0,e.price),this._addDiffToPoint(this.p2,e.logical,0)))}_onMouseDown(){this._startDragPoint=null;const e=this._latestHoverPoint,t=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(t.x&&i.x&&t.y&&i.y))return this._moveToState(h.DRAGGING);const s=10;Math.abs(e.x-t.x)l-p&&rd-p&&ai.appendChild(this.makeColorBox(e))));let s=document.createElement("div");s.style.backgroundColor=window.pane.borderColor,s.style.height="1px",s.style.width="130px";let n=document.createElement("div");n.style.margin="10px";let o=document.createElement("div");o.style.color="lightgray",o.style.fontSize="12px",o.innerText="Opacity",this._opacityLabel=document.createElement("div"),this._opacityLabel.style.color="lightgray",this._opacityLabel.style.fontSize="12px",this._opacitySlider=document.createElement("input"),this._opacitySlider.type="range",this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%",this._opacitySlider.oninput=()=>{this._opacityLabel.innerText=this._opacitySlider.value+"%",this.opacity=parseInt(this._opacitySlider.value)/100,this.updateColor()},n.appendChild(o),n.appendChild(this._opacitySlider),n.appendChild(this._opacityLabel),this._div.appendChild(i),this._div.appendChild(s),this._div.appendChild(n),window.containerDiv.appendChild(this._div)}_updateOpacitySlider(){this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%"}makeColorBox(e){const t=document.createElement("div");t.style.width="18px",t.style.height="18px",t.style.borderRadius="3px",t.style.margin="3px",t.style.boxSizing="border-box",t.style.backgroundColor=e,t.addEventListener("mouseover",(()=>t.style.border="2px solid lightgray")),t.addEventListener("mouseout",(()=>t.style.border="none"));const i=I.extractRGBA(e);return t.addEventListener("click",(()=>{this.rgba=i,this.updateColor()})),t}static extractRGBA(e){const t=document.createElement("div");t.style.color=e,document.body.appendChild(t);const i=getComputedStyle(t).color;document.body.removeChild(t);const s=i.match(/\d+/g)?.map(Number);if(!s)return[];let n=i.includes("rgba")?parseFloat(i.split(",")[3]):1;return[s[0],s[1],s[2],n]}updateColor(){if(!c.lastHoveredObject||!this.rgba)return;const e=`rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`;c.lastHoveredObject.applyOptions({[this.colorOption]:e}),this.saveDrawings()}openMenu(e){c.lastHoveredObject&&(this.rgba=I.extractRGBA(c.lastHoveredObject._options[this.colorOption]),this.opacity=this.rgba[3],this._updateOpacitySlider(),this._div.style.top=e.top-30+"px",this._div.style.left=e.right+"px",this._div.style.display="flex",setTimeout((()=>document.addEventListener("mousedown",(e=>{this._div.contains(e.target)||this.closeMenu()}))),10))}closeMenu(){document.body.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}class T{static _styles=[{name:"Solid",var:t.LineStyle.Solid},{name:"Dotted",var:t.LineStyle.Dotted},{name:"Dashed",var:t.LineStyle.Dashed},{name:"Large Dashed",var:t.LineStyle.LargeDashed},{name:"Sparse Dotted",var:t.LineStyle.SparseDotted}];_div;_saveDrawings;constructor(e){this._saveDrawings=e,this._div=document.createElement("div"),this._div.classList.add("context-menu"),T._styles.forEach((e=>{this._div.appendChild(this._makeTextBox(e.name,e.var))})),window.containerDiv.appendChild(this._div)}_makeTextBox(e,t){const i=document.createElement("span");return i.classList.add("context-menu-item"),i.innerText=e,i.addEventListener("click",(()=>{c.lastHoveredObject?.applyOptions({lineStyle:t}),this._saveDrawings()})),i}openMenu(e){this._div.style.top=e.top-30+"px",this._div.style.left=e.right+"px",this._div.style.display="block",setTimeout((()=>document.addEventListener("mousedown",(e=>{this._div.contains(e.target)||this.closeMenu()}))),10)}closeMenu(){document.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}function M(e){const t=[];for(const i of e)0==t.length?t.push(i.toUpperCase()):i==i.toUpperCase()?t.push(" "+i):t.push(i);return t.join("")}class N{saveDrawings;drawingTool;div;hoverItem;items=[];constructor(e,t){this.saveDrawings=e,this.drawingTool=t,this._onRightClick=this._onRightClick.bind(this),this.div=document.createElement("div"),this.div.classList.add("context-menu"),document.body.appendChild(this.div),this.hoverItem=null,document.body.addEventListener("contextmenu",this._onRightClick)}_handleClick=e=>this._onClick(e);_onClick(e){e.target&&(this.div.contains(e.target)||(this.div.style.display="none",document.body.removeEventListener("click",this._handleClick)))}_onRightClick(e){if(!c.hoveredObject)return;for(const e of this.items)this.div.removeChild(e);this.items=[];for(const e of Object.keys(c.hoveredObject._options)){let t;if(e.toLowerCase().includes("color"))t=new I(this.saveDrawings,e);else{if("lineStyle"!==e)continue;t=new T(this.saveDrawings)}let i=e=>t.openMenu(e);this.menuItem(M(e),i,(()=>{document.removeEventListener("click",t.closeMenu),t._div.style.display="none"}))}this.separator(),this.menuItem("Delete Drawing",(()=>this.drawingTool.delete(c.lastHoveredObject))),e.preventDefault(),this.div.style.left=e.clientX+"px",this.div.style.top=e.clientY+"px",this.div.style.display="block",document.body.addEventListener("click",this._handleClick)}menuItem(e,t,i=null){const s=document.createElement("span");s.classList.add("context-menu-item"),this.div.appendChild(s);const n=document.createElement("span");if(n.innerText=e,n.style.pointerEvents="none",s.appendChild(n),i){let e=document.createElement("span");e.innerText="►",e.style.fontSize="8px",e.style.pointerEvents="none",s.appendChild(e)}if(s.addEventListener("mouseover",(()=>{this.hoverItem&&this.hoverItem.closeAction&&this.hoverItem.closeAction(),this.hoverItem={elem:n,action:t,closeAction:i}})),i){let e;s.addEventListener("mouseover",(()=>e=setTimeout((()=>t(s.getBoundingClientRect())),100))),s.addEventListener("mouseout",(()=>clearTimeout(e)))}else s.addEventListener("click",(e=>{t(e),this.div.style.display="none"}));this.items.push(s)}separator(){const e=document.createElement("div");e.style.width="90%",e.style.height="1px",e.style.margin="3px 0px",e.style.backgroundColor=window.pane.borderColor,this.div.appendChild(e),this.items.push(e)}}class R extends b{_type="RayLine";constructor(e,t){super({...e},t),this._point.time=e.time}updatePoints(...e){for(const t of e)t&&(this._point=t);this.requestUpdate()}_onDrag(e){this._addDiffToPoint(this._point,e.logical,e.price),this.requestUpdate()}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this.series.priceToCoordinate(this._point.price),s=this._point.time?this.chart.timeScale().timeToCoordinate(this._point.time):null;return!(!i||!s)&&(Math.abs(i-e.point.y)s-t)}}class B extends p{_point={x:null,y:null};constructor(e,t){super(t),this._point=e}draw(e){e.useBitmapCoordinateSpace((e=>{if(null==this._point.x)return;const t=e.context,i=this._point.x*e.horizontalPixelRatio;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,_(t,this._options.lineStyle),t.beginPath(),t.moveTo(i,0),t.lineTo(i,e.bitmapSize.height),t.stroke()}))}}class A extends g{_source;_point={x:null,y:null};constructor(e){super(e),this._source=e}update(){const e=this._source._point,t=this._source.chart.timeScale(),i=this._source.series;this._point.x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical),this._point.y=i.priceToCoordinate(e.price)}renderer(){return new B(this._point,this._source._options)}}class P{_source;_x=null;constructor(e){this._source=e}update(){if(!this._source.chart||!this._source._point)return;const e=this._source._point,t=this._source.chart.timeScale();this._x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical)}visible(){return!!this._source._options.text}tickVisible(){return!0}coordinate(){return this._x??0}text(){return this._source._options.text||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class O extends c{_type="VerticalLine";_paneViews;_timeAxisViews;_point;_callbackName;_startDragPoint=null;constructor(e,t,i=null){super(t),this._point=e,this._paneViews=[new A(this)],this._callbackName=i,this._timeAxisViews=[new P(this)]}updateAllViews(){this._paneViews.forEach((e=>e.update())),this._timeAxisViews.forEach((e=>e.update()))}timeAxisViews(){return this._timeAxisViews}updatePoints(...e){for(const t of e)t&&(!t.time&&t.logical&&(t.time=this.series.dataByIndex(t.logical)?.time||null),this._point=t);this.requestUpdate()}get points(){return[this._point]}_moveToState(e){switch(e){case h.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case h.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case h.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._addDiffToPoint(this._point,e.logical,0),this.requestUpdate()}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this.chart.timeScale();let s;return s=this._point.time?i.timeToCoordinate(this._point.time):i.logicalToCoordinate(this._point.logical),!!s&&Math.abs(s-e.point.x){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class ${static TREND_SVG='';static HORZ_SVG='';static RAY_SVG='';static BOX_SVG='';static VERT_SVG=$.RAY_SVG;div;activeIcon=null;buttons=[];_commandFunctions;_handlerID;_drawingTool;constructor(e,t,i,s){this._handlerID=e,this._commandFunctions=s,this._drawingTool=new x(t,i,(()=>this.removeActiveAndSave())),this.div=this._makeToolBox(),new N(this.saveDrawings,this._drawingTool),s.push((e=>{if((e.metaKey||e.ctrlKey)&&"KeyZ"===e.code){const e=this._drawingTool.drawings.pop();return e&&this._drawingTool.delete(e),!0}return!1}))}toJSON(){const{...e}=this;return e}_makeToolBox(){let e=document.createElement("div");e.classList.add("toolbox"),this.buttons.push(this._makeToolBoxElement(D,"KeyT",$.TREND_SVG)),this.buttons.push(this._makeToolBoxElement(b,"KeyH",$.HORZ_SVG)),this.buttons.push(this._makeToolBoxElement(R,"KeyR",$.RAY_SVG)),this.buttons.push(this._makeToolBoxElement(G,"KeyB",$.BOX_SVG)),this.buttons.push(this._makeToolBoxElement(O,"KeyV",$.VERT_SVG,!0));for(const t of this.buttons)e.appendChild(t);return e}_makeToolBoxElement(e,t,i,s=!1){const n=document.createElement("div");n.classList.add("toolbox-button");const o=document.createElementNS("http://www.w3.org/2000/svg","svg");o.setAttribute("width","29"),o.setAttribute("height","29");const r=document.createElementNS("http://www.w3.org/2000/svg","g");r.innerHTML=i,r.setAttribute("fill",window.pane.color),o.appendChild(r),n.appendChild(o);const a={div:n,group:r,type:e};return n.addEventListener("click",(()=>this._onIconClick(a))),this._commandFunctions.push((e=>this._handlerID===window.handlerInFocus&&(!(!e.altKey||e.code!==t)&&(e.preventDefault(),this._onIconClick(a),!0)))),1==s&&(o.style.transform="rotate(90deg)",o.style.transformBox="fill-box",o.style.transformOrigin="center"),n}_onIconClick(e){this.activeIcon&&(this.activeIcon.div.classList.remove("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.stopDrawing(),this.activeIcon===e)?this.activeIcon=null:(this.activeIcon=e,this.activeIcon.div.classList.add("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.beginDrawing(this.activeIcon.type))}removeActiveAndSave=()=>{window.setCursor("default"),this.activeIcon&&this.activeIcon.div.classList.remove("active-toolbox-button"),this.activeIcon=null,this.saveDrawings()};addNewDrawing(e){this._drawingTool.addNewDrawing(e)}clearDrawings(){this._drawingTool.clearDrawings()}saveDrawings=()=>{const e=[];for(const t of this._drawingTool.drawings)e.push({type:t._type,points:t.points,options:t._options});const t=JSON.stringify(e);window.callbackFunction(`save_drawings${this._handlerID}_~_${t}`)};loadDrawings(e){e.forEach((e=>{switch(e.type){case"Box":this._drawingTool.addNewDrawing(new G(e.points[0],e.points[1],e.options));break;case"TrendLine":this._drawingTool.addNewDrawing(new D(e.points[0],e.points[1],e.options));break;case"HorizontalLine":this._drawingTool.addNewDrawing(new b(e.points[0],e.options));break;case"RayLine":this._drawingTool.addNewDrawing(new R(e.points[0],e.options));break;case"VerticalLine":this._drawingTool.addNewDrawing(new O(e.points[0],e.options))}}))}}class F{makeButton;callbackName;div;isOpen=!1;widget;constructor(e,t,i,s,n,o){this.makeButton=e,this.callbackName=t,this.div=document.createElement("div"),this.div.classList.add("topbar-menu"),this.widget=this.makeButton(s+" ↓",null,n,!0,o),this.updateMenuItems(i),this.widget.elem.addEventListener("click",(()=>{if(this.isOpen=!this.isOpen,!this.isOpen)return void(this.div.style.display="none");let e=this.widget.elem.getBoundingClientRect();this.div.style.display="flex",this.div.style.flexDirection="column";let t=e.x+e.width/2;this.div.style.left=t-this.div.clientWidth/2+"px",this.div.style.top=e.y+e.height+"px"})),document.body.appendChild(this.div)}updateMenuItems(e){this.div.innerHTML="",e.forEach((e=>{let t=this.makeButton(e,null,!1,!1);t.elem.addEventListener("click",(()=>{this._clickHandler(t.elem.innerText)})),t.elem.style.margin="4px 4px",t.elem.style.padding="2px 2px",this.div.appendChild(t.elem)})),this.widget.elem.innerText=e[0]+" ↓"}_clickHandler(e){this.widget.elem.innerText=e+" ↓",window.callbackFunction(`${this.callbackName}_~_${e}`),this.div.style.display="none",this.isOpen=!1}}class V{_handler;_div;left;right;constructor(e){this._handler=e,this._div=document.createElement("div"),this._div.classList.add("topbar");const t=e=>{const t=document.createElement("div");return t.classList.add("topbar-container"),t.style.justifyContent=e,this._div.appendChild(t),t};this.left=t("flex-start"),this.right=t("flex-end")}makeSwitcher(e,t,i,s="left"){const n=document.createElement("div");let o;n.style.margin="4px 12px";const r={elem:n,callbackName:i,intervalElements:e.map((e=>{const i=document.createElement("button");i.classList.add("topbar-button"),i.classList.add("switcher-button"),i.style.margin="0px 2px",i.innerText=e,e==t&&(o=i,i.classList.add("active-switcher-button"));const s=V.getClientWidth(i);return i.style.minWidth=s+1+"px",i.addEventListener("click",(()=>r.onItemClicked(i))),n.appendChild(i),i})),onItemClicked:e=>{e!=o&&(o.classList.remove("active-switcher-button"),e.classList.add("active-switcher-button"),o=e,window.callbackFunction(`${r.callbackName}_~_${e.innerText}`))}};return this.appendWidget(n,s,!0),r}makeTextBoxWidget(e,t="left",i=null){if(i){const s=document.createElement("input");return s.classList.add("topbar-textbox-input"),s.value=e,s.style.width=`${s.value.length+2}ch`,s.addEventListener("focus",(()=>{window.textBoxFocused=!0})),s.addEventListener("input",(e=>{e.preventDefault(),s.style.width=`${s.value.length+2}ch`})),s.addEventListener("keydown",(e=>{"Enter"==e.key&&(e.preventDefault(),s.blur())})),s.addEventListener("blur",(()=>{window.callbackFunction(`${i}_~_${s.value}`),window.textBoxFocused=!1})),this.appendWidget(s,t,!0),s}{const i=document.createElement("div");return i.classList.add("topbar-textbox"),i.innerText=e,this.appendWidget(i,t,!0),i}}makeMenu(e,t,i,s,n){return new F(this.makeButton.bind(this),s,e,t,i,n)}makeButton(e,t,i,s=!0,n="left",o=!1){let r=document.createElement("button");r.classList.add("topbar-button"),r.innerText=e,document.body.appendChild(r),r.style.minWidth=r.clientWidth+1+"px",document.body.removeChild(r);let a={elem:r,callbackName:t};if(t){let e;if(o){let t=!1;e=()=>{t=!t,window.callbackFunction(`${a.callbackName}_~_${t}`),r.style.backgroundColor=t?"var(--active-bg-color)":"",r.style.color=t?"var(--active-color)":""}}else e=()=>window.callbackFunction(`${a.callbackName}_~_${r.innerText}`);r.addEventListener("click",e)}return s&&this.appendWidget(r,n,i),a}makeSeparator(e="left"){const t=document.createElement("div");t.classList.add("topbar-seperator");("left"==e?this.left:this.right).appendChild(t)}appendWidget(e,t,i){const s="left"==t?this.left:this.right;i?("left"==t&&s.appendChild(e),this.makeSeparator(t),"right"==t&&s.appendChild(e)):s.appendChild(e),this._handler.reSize()}static getClientWidth(e){document.body.appendChild(e);const t=e.clientWidth;return document.body.removeChild(e),t}}s();return e.Box=G,e.Handler=class{id;commandFunctions=[];wrapper;div;chart;scale;precision=2;series;volumeSeries;legend;_topBar;toolBox;spinner;_seriesList=[];constructor(e,t,i,s,n){this.reSize=this.reSize.bind(this),this.id=e,this.scale={width:t,height:i},this.wrapper=document.createElement("div"),this.wrapper.classList.add("handler"),this.wrapper.style.float=s,this.div=document.createElement("div"),this.div.style.position="relative",this.wrapper.appendChild(this.div),window.containerDiv.append(this.wrapper),this.chart=this._createChart(),this.series=this.createCandlestickSeries(),this.volumeSeries=this.createVolumeSeries(),this.legend=new r(this),document.addEventListener("keydown",(e=>{for(let t=0;twindow.handlerInFocus=this.id)),this.reSize(),n&&window.addEventListener("resize",(()=>this.reSize()))}reSize(){let e=0!==this.scale.height&&this._topBar?._div.offsetHeight||0;this.chart.resize(window.innerWidth*this.scale.width,window.innerHeight*this.scale.height-e),this.wrapper.style.width=100*this.scale.width+"%",this.wrapper.style.height=100*this.scale.height+"%",0===this.scale.height||0===this.scale.width?this.toolBox&&(this.toolBox.div.style.display="none"):this.toolBox&&(this.toolBox.div.style.display="flex")}_createChart(){return t.createChart(this.div,{width:window.innerWidth*this.scale.width,height:window.innerHeight*this.scale.height,layout:{textColor:window.pane.color,background:{color:"#000000",type:t.ColorType.Solid},fontSize:12},rightPriceScale:{scaleMargins:{top:.3,bottom:.25}},timeScale:{timeVisible:!0,secondsVisible:!1},crosshair:{mode:t.CrosshairMode.Normal,vertLine:{labelBackgroundColor:"rgb(46, 46, 46)"},horzLine:{labelBackgroundColor:"rgb(55, 55, 55)"}},grid:{vertLines:{color:"rgba(29, 30, 38, 5)"},horzLines:{color:"rgba(29, 30, 58, 5)"}},handleScroll:{vertTouchDrag:!0}})}createCandlestickSeries(){const e="rgba(39, 157, 130, 100)",t="rgba(200, 97, 100, 100)",i=this.chart.addCandlestickSeries({upColor:e,borderUpColor:e,wickUpColor:e,downColor:t,borderDownColor:t,wickDownColor:t});return i.priceScale().applyOptions({scaleMargins:{top:.2,bottom:.2}}),i}createVolumeSeries(){const e=this.chart.addHistogramSeries({color:"#26a69a",priceFormat:{type:"volume"},priceScaleId:"volume_scale"});return e.priceScale().applyOptions({scaleMargins:{top:.8,bottom:0}}),e}createLineSeries(e,t){const{group:i,legendSymbol:s="▨",...n}=t,o=this.chart.addLineSeries(n);this._seriesList.push(o);const r=o.options().color||"rgba(255,0,0,1)",a=r.startsWith("rgba")?r.replace(/[^,]+(?=\))/,"1"):r;if(i&&""!==i){const t=this.legend._groups.find((e=>e.name===i));t?(t.names.push(e),t.seriesList.push(o),t.solidColors.push(a),t.legendSymbols.push(s)):this.legend.makeSeriesGroup(i,[e],[o],[a],[s])}else this.legend.makeSeriesRow(e,o,[s],[a]);return{name:e,series:o}}createHistogramSeries(e,t){const{group:i,legendSymbol:s="▨",...n}=t,o=this.chart.addHistogramSeries(n);this._seriesList.push(o);const r=o.options().color||"rgba(255,0,0,1)",a=r.startsWith("rgba")?r.replace(/[^,]+(?=\))/,"1"):r;if(i&&""!==i){const t=this.legend._groups.find((e=>e.name===i));t?(t.names.push(e),t.seriesList.push(o),t.solidColors.push(a),t.legendSymbols.push(s)):this.legend.makeSeriesGroup(i,[e],[o],[a],[s])}else this.legend.makeSeriesRow(e,o,[s],[a]);return{name:e,series:o}}createAreaSeries(e,t){const{group:i,legendSymbol:s="▨",...n}=t,o=this.chart.addAreaSeries(n);this._seriesList.push(o);const r=o.options().color||"rgba(255,0,0,1)",a=r.startsWith("rgba")?r.replace(/[^,]+(?=\))/,"1"):r;if(i&&""!==i){const t=this.legend._groups.find((e=>e.name===i));t?(t.names.push(e),t.seriesList.push(o),t.solidColors.push(a),t.legendSymbols.push(s)):this.legend.makeSeriesGroup(i,[e],[o],[a],[s])}else this.legend.makeSeriesRow(e,o,[s],[a]);return{name:e,series:o}}createBarSeries(e,t){const{group:i,legendSymbol:s=["▨","▨"],...n}=t,o=this.chart.addBarSeries(n);this._seriesList.push(o);const r=o.options().upColor||"rgba(0,255,0,1)",a=o.options().downColor||"rgba(255,0,0,1)";if(i&&""!==i){const t=this.legend._groups.find((e=>e.name===i));t?(t.names.push(e),t.seriesList.push(o),t.solidColors.push(r,a),t.legendSymbols.push(s[0],s[1])):this.legend.makeSeriesGroup(i,[e],[o],[r,a],s)}else this.legend.makeSeriesRow(e,o,s,[r,a]);return{name:e,series:o}}createToolBox(){this.toolBox=new $(this.id,this.chart,this.series,this.commandFunctions),this.div.appendChild(this.toolBox.div)}createTopBar(){return this._topBar=new V(this),this.wrapper.prepend(this._topBar._div),this._topBar}toJSON(){const{chart:e,...t}=this;return t}static syncCharts(e,t,i=!1){function s(e,t){t?(e.chart.setCrosshairPosition(t.value||t.close,t.time,e.series),e.legend.legendHandler(t,!0)):e.chart.clearCrosshairPosition()}function n(e,t){return t.time&&t.seriesData.get(e)||null}const o=e.chart.timeScale(),r=t.chart.timeScale(),a=e=>{e&&o.setVisibleLogicalRange(e)},l=e=>{e&&r.setVisibleLogicalRange(e)},d=i=>{s(t,n(e.series,i))},h=i=>{s(e,n(t.series,i))};let c=t;function p(e,t,s,n,o,r){e.wrapper.addEventListener("mouseover",(()=>{c!==e&&(c=e,t.chart.unsubscribeCrosshairMove(s),e.chart.subscribeCrosshairMove(n),i||(t.chart.timeScale().unsubscribeVisibleLogicalRangeChange(o),e.chart.timeScale().subscribeVisibleLogicalRangeChange(r)))}))}p(t,e,d,h,l,a),p(e,t,h,d,a,l),t.chart.subscribeCrosshairMove(h);const u=r.getVisibleLogicalRange();u&&o.setVisibleLogicalRange(u),i||t.chart.timeScale().subscribeVisibleLogicalRangeChange(a)}static makeSearchBox(e){const t=document.createElement("div");t.classList.add("searchbox"),t.style.display="none";const i=document.createElement("div");i.innerHTML='';const s=document.createElement("input");return s.type="text",t.appendChild(i),t.appendChild(s),e.div.appendChild(t),e.commandFunctions.push((i=>window.handlerInFocus===e.id&&!window.textBoxFocused&&("none"===t.style.display?!!/^[a-zA-Z0-9]$/.test(i.key)&&(t.style.display="flex",s.focus(),!0):("Enter"===i.key||"Escape"===i.key)&&("Enter"===i.key&&window.callbackFunction(`search${e.id}_~_${s.value}`),t.style.display="none",s.value="",!0)))),s.addEventListener("input",(()=>s.value=s.value.toUpperCase())),{window:t,box:s}}static makeSpinner(e){e.spinner=document.createElement("div"),e.spinner.classList.add("spinner"),e.wrapper.appendChild(e.spinner);let t=0;!function i(){e.spinner&&(t+=10,e.spinner.style.transform=`translate(-50%, -50%) rotate(${t}deg)`,requestAnimationFrame(i))}()}static _styleMap={"--bg-color":"backgroundColor","--hover-bg-color":"hoverBackgroundColor","--click-bg-color":"clickBackgroundColor","--active-bg-color":"activeBackgroundColor","--muted-bg-color":"mutedBackgroundColor","--border-color":"borderColor","--color":"color","--active-color":"activeColor"};static setRootStyles(e){const t=document.documentElement.style;for(const[i,s]of Object.entries(this._styleMap))t.setProperty(i,e[s])}},e.HorizontalLine=b,e.Legend=r,e.RayLine=R,e.Table=class{_div;callbackName;borderColor;borderWidth;table;rows={};headings;widths;alignments;footer;header;constructor(e,t,i,s,n,o,r=!1,a,l,d,h,c){this._div=document.createElement("div"),this.callbackName=null,this.borderColor=l,this.borderWidth=d,r?(this._div.style.position="absolute",this._div.style.cursor="move"):(this._div.style.position="relative",this._div.style.float=o),this._div.style.zIndex="2000",this.reSize(e,t),this._div.style.display="flex",this._div.style.flexDirection="column",this._div.style.borderRadius="5px",this._div.style.color="white",this._div.style.fontSize="12px",this._div.style.fontVariantNumeric="tabular-nums",this.table=document.createElement("table"),this.table.style.width="100%",this.table.style.borderCollapse="collapse",this._div.style.overflow="hidden",this.headings=i,this.widths=s.map((e=>100*e+"%")),this.alignments=n;let p=this.table.createTHead().insertRow();for(let e=0;e0?c[e]:a,t.style.color=h[e],p.appendChild(t)}let u,_,m=document.createElement("div");if(m.style.overflowY="auto",m.style.overflowX="hidden",m.style.backgroundColor=a,m.appendChild(this.table),this._div.appendChild(m),window.containerDiv.appendChild(this._div),!r)return;let g=e=>{this._div.style.left=e.clientX-u+"px",this._div.style.top=e.clientY-_+"px"},v=()=>{document.removeEventListener("mousemove",g),document.removeEventListener("mouseup",v)};this._div.addEventListener("mousedown",(e=>{u=e.clientX-this._div.offsetLeft,_=e.clientY-this._div.offsetTop,document.addEventListener("mousemove",g),document.addEventListener("mouseup",v)}))}divToButton(e,t){e.addEventListener("mouseover",(()=>e.style.backgroundColor="rgba(60, 60, 60, 0.6)")),e.addEventListener("mouseout",(()=>e.style.backgroundColor="transparent")),e.addEventListener("mousedown",(()=>e.style.backgroundColor="rgba(60, 60, 60)")),e.addEventListener("click",(()=>window.callbackFunction(t))),e.addEventListener("mouseup",(()=>e.style.backgroundColor="rgba(60, 60, 60, 0.6)"))}newRow(e,t=!1){let i=this.table.insertRow();i.style.cursor="default";for(let s=0;s{e&&(window.cursor=e),document.body.style.cursor=window.cursor},e}({},LightweightCharts); +var Lib=function(e,t){"use strict";const i={backgroundColor:"#0c0d0f",hoverBackgroundColor:"#3c434c",clickBackgroundColor:"#50565E",activeBackgroundColor:"rgba(0, 122, 255, 0.7)",mutedBackgroundColor:"rgba(0, 122, 255, 0.3)",borderColor:"#3C434C",color:"#d8d9db",activeColor:"#ececed"};function s(){window.pane={...i},window.containerDiv=document.getElementById("container")||document.createElement("div"),window.setCursor=e=>{e&&(window.cursor=e),document.body.style.cursor=window.cursor},window.cursor="default",window.textBoxFocused=!1}const o=new Map;function n(e){return o.get(e)||null}const r='\n\n \n \n\n',a='\n\n \n \n \n\n';class l{handler;div;seriesContainer;ohlcEnabled=!1;percentEnabled=!1;linesEnabled=!1;colorBasedOnCandle=!1;text;candle;_items=[];_lines=[];_groups=[];constructor(e){this.handler=e,this.div=document.createElement("div"),this.div.classList.add("legend"),this.seriesContainer=document.createElement("div"),this.text=document.createElement("span"),this.candle=document.createElement("div"),this.setupLegend(),this.legendHandler=this.legendHandler.bind(this),e.chart.subscribeCrosshairMove(this.legendHandler)}setupLegend(){this.div.style.maxWidth=100*this.handler.scale.width-8+"vw",this.div.style.display="none";const e=document.createElement("div");e.style.display="flex",e.style.flexDirection="row",this.seriesContainer.classList.add("series-container"),this.text.style.lineHeight="1.8",e.appendChild(this.seriesContainer),this.div.appendChild(this.text),this.div.appendChild(this.candle),this.div.appendChild(e),this.handler.div.appendChild(this.div)}legendItemFormat(e,t){return"number"!=typeof e||isNaN(e)?"-":e.toFixed(t).toString().padStart(8," ")}shorthandFormat(e){const t=Math.abs(e);return t>=1e6?(e/1e6).toFixed(1)+"M":t>=1e3?(e/1e3).toFixed(1)+"K":e.toString().padStart(8," ")}makeSeriesRow(e){const t=document.createElement("div");t.style.display="flex",t.style.alignItems="center";const i=document.createElement("div");if(["Bar","ohlc"].includes(e.seriesType||"")){const t="-",s="-",o=e.legendSymbol[0]||"▨",n=e.legendSymbol[1]||o,r=e.colors[0]||"#00FF00",a=e.colors[1]||"#FF0000";i.innerHTML=`\n ${o}\n ${n}\n ${e.name}: O ${t}, \n C ${s}\n `}else i.innerHTML=e.legendSymbol.map(((t,i)=>`${t}`)).join(" ")+` ${e.name}`;const s=document.createElement("div");s.classList.add("legend-toggle-switch");const o=this.createSvgIcon(r),n=this.createSvgIcon(a);s.appendChild(o.cloneNode(!0));let l=!0;s.addEventListener("click",(()=>{l=!l,e.series.applyOptions({visible:l}),s.innerHTML="",s.appendChild(l?o.cloneNode(!0):n.cloneNode(!0))})),t.appendChild(i),t.appendChild(s),this.seriesContainer.appendChild(t);const h={...e,div:i,row:t,toggle:s};return this._lines.push(h),t}addLegendItem(e){if(e.group){let t=this._groups.find((t=>t.name===e.group));if(t)return t.seriesList.push(e),this._items.push(e),t.row;{const t=this.makeSeriesGroup(e.group,[e]);return this._groups[this._groups.length-1],t}}{const t=this.makeSeriesRow(e);return this._lines[this._lines.length-1],this._items.push(e),t}}deleteLegendEntry(e,t){if(t&&!e){const e=this._groups.findIndex((e=>e.name===t));if(-1!==e){const t=this._groups[e];this.seriesContainer.removeChild(t.row),this._groups.splice(e,1),this._items=this._items.filter((e=>e!==t))}else console.warn(`Legend group with name "${t}" not found.`)}else if(e){let i=!1;if(t){const s=this._groups.find((e=>e.name===t));if(s){const o=s.seriesList.findIndex((t=>t.name===e));-1!==o&&(s.seriesList[o],s.seriesList.splice(o,1),0===s.seriesList.length&&(this.seriesContainer.removeChild(s.row),this._groups=this._groups.filter((e=>e!==s)),this._items=this._items.filter((e=>e!==s)),console.log(`Group "${t}" is empty and has been removed.`)),i=!0,console.log(`Series "${e}" removed from group "${t}".`))}else console.warn(`Legend group with name "${t}" not found.`)}if(!i){const t=this._lines.findIndex((t=>t.name===e));if(-1!==t){const s=this._lines[t];this.seriesContainer.removeChild(s.row),this._lines.splice(t,1),this._items=this._items.filter((e=>e!==s)),i=!0,console.log(`Series "${e}" removed.`)}}i||console.warn(`Legend item with name "${e}" not found.`)}else console.warn("No seriesName or groupName provided for deletion.")}makeSeriesGroup(e,t){let i=this._groups.find((t=>t.name===e));if(i)return i.seriesList.push(...t),i.row;{const i=document.createElement("div");i.style.display="flex",i.style.alignItems="center";const s=document.createElement("div");s.innerHTML=`${e}:`;const o=document.createElement("div");o.classList.add("legend-toggle-switch");const n=this.createSvgIcon(r),l=this.createSvgIcon(a);o.appendChild(n.cloneNode(!0));let h=!0;o.addEventListener("click",(()=>{h=!h,t.forEach((e=>e.series.applyOptions({visible:h}))),o.innerHTML="",o.appendChild(h?n.cloneNode(!0):l.cloneNode(!0))})),t.forEach((e=>{if("Bar"===e.seriesType||"ohlc"===e.seriesType){const[t,i]=e.legendSymbol,[o,n]=e.colors,r="-",a="-";s.innerHTML+=`\n ${t}\n ${i}\n ${name}: \n O ${r}, \n C ${a}\n \n `}else{const t=e.colors[0]||"#000",i=e.legendSymbol[0]||"▨",o="-";s.innerHTML+=`\n ${i}\n ${name}: ${o}\n `}})),i.appendChild(s),i.appendChild(o),this.seriesContainer.appendChild(i);const d={name:e,seriesList:t,div:s,row:i,toggle:o};return this._groups.push(d),i}}createSvgIcon(e){const t=document.createElement("div");t.innerHTML=e.trim();return t.querySelector("svg")}legendHandler(e,t=!1){if(!this.ohlcEnabled&&!this.linesEnabled&&!this.percentEnabled)return;const i=this.handler.series.options();if(!e.time)return this.candle.style.color="transparent",void(this.candle.innerHTML=this.candle.innerHTML.replace(i.upColor,"").replace(i.downColor,""));let s,o=null;if(t){const t=this.handler.chart.timeScale(),i=t.timeToCoordinate(e.time);i&&(o=t.coordinateToLogical(i.valueOf())),o&&(s=this.handler.series.dataByIndex(o.valueOf()))}else s=e.seriesData.get(this.handler.series);let n='';if(s&&(this.ohlcEnabled&&(n+=`O ${this.legendItemFormat(s.open,this.handler.precision)} `,n+=`| H ${this.legendItemFormat(s.high,this.handler.precision)} `,n+=`| L ${this.legendItemFormat(s.low,this.handler.precision)} `,n+=`| C ${this.legendItemFormat(s.close,this.handler.precision)} `),this.percentEnabled)){const e=(s.close-s.open)/s.open*100,t=e>0?i.upColor:i.downColor,o=`${e>=0?"+":""}${e.toFixed(2)} %`;n+=this.colorBasedOnCandle?`| ${o}`:`| ${o}`}this.candle.innerHTML=n+"",this.updateGroupDisplay(e,o,t),this.updateSeriesDisplay(e,o,t)}updateGroupDisplay(e,t,i){this._groups.forEach((t=>{if(!this.linesEnabled)return void(t.row.style.display="none");t.row.style.display="flex";let i=`${t.name}: `;t.seriesList.forEach((t=>{const s=e.seriesData.get(t.series)||n(t.series);if(!s)return;const o=t.seriesType||"Line",r=t.name,a=t.series.options().priceFormat;if("Bar"===o||"ohlc"===o||"htfCandle"===o){const{open:e,close:o,high:n,low:l}=s;if(null==e||null==o||null==n||null==l)return;if(null==e||null==o)return void(i+=`${r}: - `);const h=this.legendItemFormat(e,a.precision),d=this.legendItemFormat(o,a.precision),c=o>e,p=c?t.colors[0]:t.colors[1],u=c?t.legendSymbol[0]:t.legendSymbol[1];i+=`\n ${u||"▨"}\n ${r}: \n O ${h}, \n C ${d}\n `}else{const e=(s||{}).value;if(null==e)return void(i+=`${r}: - `);const o=t.series.options().priceFormat,n=this.legendItemFormat(e,o.precision),a=t.colors[0],l=t.legendSymbol[0]||"▨";i+=`\n ${l}\n ${r}: ${n} `}})),t.div.innerHTML=i}))}updateSeriesDisplay(e,t,i){this._lines&&this._lines.length?this._lines.forEach((t=>{if(!this.linesEnabled)return void(t.row.style.display="none");t.row.style.display="flex";const i=e.seriesData.get(t.series)||n(t.series);if(!i)return void(t.div.innerHTML=`${t.name}: -`);const s=t.seriesType||"Line";if(["Bar","ohlc","htfCandle"].includes(s)){const{open:e,close:s}=i;if(null==e||null==s)return void(t.div.innerHTML=`${t.name}: -`);const o=t.series.options().priceFormat,n=this.legendItemFormat(e,o.precision),r=this.legendItemFormat(s,o.precision),a=s>e,l=a?t.colors[0]:t.colors[1],h=a?t.legendSymbol[0]:t.legendSymbol[1];t.div.innerHTML=`\n ${h||"▨"}\n ${t.name}: \n O ${n}, \n C ${r}\n `}else if("Histogram"===s){const e=i;if(null==e.value)return void(t.div.innerHTML=`${t.name}: -`);const s=this.shorthandFormat(e.value);t.div.innerHTML=`\n ${t.legendSymbol[0]||"▨"} \n ${t.name}: ${s}`}else{const e=i;if(null==e.value)return void(t.div.innerHTML=`${t.name}: -`);const s=t.series.options().priceFormat,o=this.legendItemFormat(e.value,s.precision);t.div.innerHTML=`\n ${t.legendSymbol[0]||"▨"} \n ${t.name}: ${o}`}})):console.error("No lines available to update legend.")}}function h(e){if(void 0===e)throw new Error("Value is undefined");return e}class d{_chart=void 0;_series=void 0;requestUpdate(){this._requestUpdate&&this._requestUpdate()}_requestUpdate;attached({chart:e,series:t,requestUpdate:i}){this._chart=e,this._series=t,this._series.subscribeDataChanged(this._fireDataUpdated),this._requestUpdate=i,this.requestUpdate()}detached(){this._chart=void 0,this._series=void 0,this._requestUpdate=void 0}get chart(){return h(this._chart)}get series(){return h(this._series)}_fireDataUpdated(e){this.dataUpdated&&this.dataUpdated(e)}}const c={lineColor:"#1E80F0",lineStyle:t.LineStyle.Solid,width:4};var p;!function(e){e[e.NONE=0]="NONE",e[e.HOVERING=1]="HOVERING",e[e.DRAGGING=2]="DRAGGING",e[e.DRAGGINGP1=3]="DRAGGINGP1",e[e.DRAGGINGP2=4]="DRAGGINGP2",e[e.DRAGGINGP3=5]="DRAGGINGP3",e[e.DRAGGINGP4=6]="DRAGGINGP4"}(p||(p={}));class u extends d{_paneViews=[];_options;_points=[];_state=p.NONE;_startDragPoint=null;_latestHoverPoint=null;static _mouseIsDown=!1;static hoveredObject=null;static lastHoveredObject=null;_listeners=[];constructor(e){super(),this._options={...c,...e}}updateAllViews(){this._paneViews.forEach((e=>e.update()))}paneViews(){return this._paneViews}applyOptions(e){this._options={...this._options,...e},this.requestUpdate()}updatePoints(...e){for(let t=0;ti.name===e&&i.listener===t));this._listeners.splice(this._listeners.indexOf(i),1)}_handleHoverInteraction(e){if(this._latestHoverPoint=e.point,u._mouseIsDown)this._handleDragInteraction(e);else if(this._mouseIsOverDrawing(e)){if(this._state!=p.NONE)return;this._moveToState(p.HOVERING),u.hoveredObject=u.lastHoveredObject=this}else{if(this._state==p.NONE)return;this._moveToState(p.NONE),u.hoveredObject===this&&(u.hoveredObject=null)}}static _eventToPoint(e,t){if(!t||!e.point||!e.logical)return null;const i=t.coordinateToPrice(e.point.y);return null==i?null:{time:e.time||null,logical:e.logical,price:i.valueOf()}}static _getDiff(e,t){return{logical:e.logical-t.logical,price:e.price-t.price}}_addDiffToPoint(e,t,i){e&&(e.logical=e.logical+t,e.price=e.price+i,e.time=this.series.dataByIndex(e.logical)?.time||null)}_handleMouseDownInteraction=()=>{u._mouseIsDown=!0,this._onMouseDown()};_handleMouseUpInteraction=()=>{u._mouseIsDown=!1,this._moveToState(p.HOVERING)};_handleDragInteraction(e){if(this._state!=p.DRAGGING&&this._state!=p.DRAGGINGP1&&this._state!=p.DRAGGINGP2&&this._state!=p.DRAGGINGP3&&this._state!=p.DRAGGINGP4)return;const t=u._eventToPoint(e,this.series);if(!t)return;this._startDragPoint=this._startDragPoint||t;const i=u._getDiff(t,this._startDragPoint);this._onDrag(i),this.requestUpdate(),this._startDragPoint=t}}class _{_options;constructor(e){this._options=e}}class m extends _{_p1;_p2;_hovered;constructor(e,t,i,s){super(i),this._p1=e,this._p2=t,this._hovered=s}_getScaledCoordinates(e){return null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y?null:{x1:Math.round(this._p1.x*e.horizontalPixelRatio),y1:Math.round(this._p1.y*e.verticalPixelRatio),x2:Math.round(this._p2.x*e.horizontalPixelRatio),y2:Math.round(this._p2.y*e.verticalPixelRatio)}}_drawEndCircle(e,t,i){e.context.fillStyle="#000",e.context.beginPath(),e.context.arc(t,i,9,0,2*Math.PI),e.context.stroke(),e.context.fill()}}function g(e,i){const s={[t.LineStyle.Solid]:[],[t.LineStyle.Dotted]:[e.lineWidth,e.lineWidth],[t.LineStyle.Dashed]:[2*e.lineWidth,2*e.lineWidth],[t.LineStyle.LargeDashed]:[6*e.lineWidth,6*e.lineWidth],[t.LineStyle.SparseDotted]:[e.lineWidth,4*e.lineWidth]}[i];e.setLineDash(s)}class w extends _{_point={x:null,y:null};constructor(e,t){super(t),this._point=e}draw(e){e.useBitmapCoordinateSpace((e=>{if(null==this._point.y)return;const t=e.context,i=Math.round(this._point.y*e.verticalPixelRatio),s=this._point.x?this._point.x*e.horizontalPixelRatio:0;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,g(t,this._options.lineStyle),t.beginPath(),t.moveTo(s,i),t.lineTo(e.bitmapSize.width,i),t.stroke()}))}}class v{_source;constructor(e){this._source=e}}class y extends v{_p1={x:null,y:null};_p2={x:null,y:null};_source;constructor(e){super(e),this._source=e}update(){if(!this._source.p1||!this._source.p2)return;const e=this._source.series,t=e.priceToCoordinate(this._source.p1.price),i=e.priceToCoordinate(this._source.p2.price),s=this._getX(this._source.p1),o=this._getX(this._source.p2);this._p1={x:s,y:t},this._p2={x:o,y:i}}_getX(e){return this._source.chart.timeScale().logicalToCoordinate(e.logical)}}class b extends v{_source;_point={x:null,y:null};constructor(e){super(e),this._source=e}update(){const e=this._source._point,t=this._source.chart.timeScale(),i=this._source.series;"RayLine"==this._source._type&&(this._point.x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical)),this._point.y=i.priceToCoordinate(e.price)}renderer(){return new w(this._point,this._source._options)}}class f{_source;_y=null;_price=null;constructor(e){this._source=e}update(){if(!this._source.series||!this._source._point)return;this._y=this._source.series.priceToCoordinate(this._source._point.price);const e=this._source.series.options().priceFormat.precision;this._price=this._source._point.price.toFixed(e).toString()}visible(){return!0}tickVisible(){return!0}coordinate(){return this._y??0}text(){return this._source._options.text||this._price||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class x extends u{_type="HorizontalLine";_paneViews;_point;_callbackName;_priceAxisViews;_startDragPoint=null;constructor(e,t,i=null){super(t),this._point=e,this._point.time=null,this._paneViews=[new b(this)],this._priceAxisViews=[new f(this)],this._callbackName=i}get points(){return[this._point]}updatePoints(...e){for(const t of e)t&&(this._point.price=t.price);this.requestUpdate()}updateAllViews(){this._paneViews.forEach((e=>e.update())),this._priceAxisViews.forEach((e=>e.update()))}priceAxisViews(){return this._priceAxisViews}_moveToState(e){switch(e){case p.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case p.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case p.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._addDiffToPoint(this._point,0,e.price),this.requestUpdate()}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this.series.priceToCoordinate(this._point.price);return!!i&&Math.abs(i-e.point.y){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class C{_chart;_series;_finishDrawingCallback=null;_drawings=[];_activeDrawing=null;_isDrawing=!1;_drawingType=null;constructor(e,t,i=null){this._chart=e,this._series=t,this._finishDrawingCallback=i,this._chart.subscribeClick(this._clickHandler),this._chart.subscribeCrosshairMove(this._moveHandler)}_clickHandler=e=>this._onClick(e);_moveHandler=e=>this._onMouseMove(e);beginDrawing(e){this._drawingType=e,this._isDrawing=!0}stopDrawing(){this._isDrawing=!1,this._activeDrawing=null}get drawings(){return this._drawings}addNewDrawing(e){this._series.attachPrimitive(e),this._drawings.push(e)}delete(e){if(null==e)return;const t=this._drawings.indexOf(e);-1!=t&&(this._drawings.splice(t,1),e.detach())}clearDrawings(){for(const e of this._drawings)e.detach();this._drawings=[]}repositionOnTime(){for(const e of this.drawings){const t=[];for(const i of e.points){if(!i){t.push(i);continue}const e=i.time?this._chart.timeScale().coordinateToLogical(this._chart.timeScale().timeToCoordinate(i.time)||0):i.logical;t.push({time:i.time,logical:e,price:i.price})}e.updatePoints(...t)}}_onClick(e){if(!this._isDrawing)return;const t=u._eventToPoint(e,this._series);if(t)if(null==this._activeDrawing){if(null==this._drawingType)return;this._activeDrawing=new this._drawingType(t,t),this._series.attachPrimitive(this._activeDrawing),this._drawingType==x&&this._onClick(e)}else{if(this._drawings.push(this._activeDrawing),this.stopDrawing(),!this._finishDrawingCallback)return;this._finishDrawingCallback()}}_onMouseMove(e){if(!e)return;for(const t of this._drawings)t._handleHoverInteraction(e);if(!this._isDrawing||!this._activeDrawing)return;const t=u._eventToPoint(e,this._series);t&&this._activeDrawing.updatePoints(null,t)}}class k extends m{constructor(e,t,i,s){super(e,t,i,s)}draw(e){e.useBitmapCoordinateSpace((e=>{if(null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y)return;const t=e.context,i=this._getScaledCoordinates(e);i&&(t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,g(t,this._options.lineStyle),t.beginPath(),t.moveTo(i.x1,i.y1),t.lineTo(i.x2,i.y2),t.stroke(),this._hovered&&(this._drawEndCircle(e,i.x1,i.y1),this._drawEndCircle(e,i.x2,i.y2)))}))}}class S extends y{constructor(e){super(e)}renderer(){return new k(this._p1,this._p2,this._source._options,this._source.hovered)}}class D extends u{_paneViews=[];_hovered=!1;constructor(e,t,i){super(),this.points.push(e),this.points.push(t),this._options={...c,...i}}setFirstPoint(e){this.updatePoints(e)}setSecondPoint(e){this.updatePoints(null,e)}get p1(){return this.points[0]}get p2(){return this.points[1]}get hovered(){return this._hovered}}class L extends D{_type="TrendLine";constructor(e,t,i){super(e,t,i),this._paneViews=[new S(this)]}_moveToState(e){switch(e){case p.NONE:document.body.style.cursor="default",this._hovered=!1,this.requestUpdate(),this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case p.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this.requestUpdate(),this._subscribe("mousedown",this._handleMouseDownInteraction),this._unsubscribe("mouseup",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case p.DRAGGINGP1:case p.DRAGGINGP2:case p.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._state!=p.DRAGGING&&this._state!=p.DRAGGINGP1||this._addDiffToPoint(this.p1,e.logical,e.price),this._state!=p.DRAGGING&&this._state!=p.DRAGGINGP2||this._addDiffToPoint(this.p2,e.logical,e.price)}_onMouseDown(){this._startDragPoint=null;const e=this._latestHoverPoint;if(!e)return;const t=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(t.x&&i.x&&t.y&&i.y))return this._moveToState(p.DRAGGING);Math.abs(e.x-t.x)<10&&Math.abs(e.y-t.y)<10?this._moveToState(p.DRAGGINGP1):Math.abs(e.x-i.x)<10&&Math.abs(e.y-i.y)<10?this._moveToState(p.DRAGGINGP2):this._moveToState(p.DRAGGING)}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this._paneViews[0]._p1.x,s=this._paneViews[0]._p1.y,o=this._paneViews[0]._p2.x,n=this._paneViews[0]._p2.y;if(!(i&&o&&s&&n))return!1;const r=e.point.x,a=e.point.y;if(r<=Math.min(i,o)-t||r>=Math.max(i,o)+t)return!1;return Math.abs((n-s)*r-(o-i)*a+o*s-n*i)/Math.sqrt((n-s)**2+(o-i)**2)<=t}}class T extends m{constructor(e,t,i,s){super(e,t,i,s)}draw(e){e.useBitmapCoordinateSpace((e=>{const t=e.context,i=this._getScaledCoordinates(e);if(!i)return;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,g(t,this._options.lineStyle),t.fillStyle=this._options.fillColor;const s=Math.min(i.x1,i.x2),o=Math.min(i.y1,i.y2),n=Math.abs(i.x1-i.x2),r=Math.abs(i.y1-i.y2);t.strokeRect(s,o,n,r),t.fillRect(s,o,n,r),this._hovered&&(this._drawEndCircle(e,s,o),this._drawEndCircle(e,s+n,o),this._drawEndCircle(e,s+n,o+r),this._drawEndCircle(e,s,o+r))}))}}class E extends y{constructor(e){super(e)}renderer(){return new T(this._p1,this._p2,this._source._options,this._source.hovered)}}const I={fillEnabled:!0,fillColor:"rgba(255, 255, 255, 0.2)",...c};class M extends D{_type="Box";constructor(e,t,i){super(e,t,i),this._options={...I,...i},this._paneViews=[new E(this)]}_moveToState(e){switch(e){case p.NONE:document.body.style.cursor="default",this._hovered=!1,this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case p.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this._unsubscribe("mouseup",this._handleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case p.DRAGGINGP1:case p.DRAGGINGP2:case p.DRAGGINGP3:case p.DRAGGINGP4:case p.DRAGGING:document.body.style.cursor="grabbing",document.body.addEventListener("mouseup",this._handleMouseUpInteraction),this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._state!=p.DRAGGING&&this._state!=p.DRAGGINGP1||this._addDiffToPoint(this.p1,e.logical,e.price),this._state!=p.DRAGGING&&this._state!=p.DRAGGINGP2||this._addDiffToPoint(this.p2,e.logical,e.price),this._state!=p.DRAGGING&&(this._state==p.DRAGGINGP3&&(this._addDiffToPoint(this.p1,e.logical,0),this._addDiffToPoint(this.p2,0,e.price)),this._state==p.DRAGGINGP4&&(this._addDiffToPoint(this.p1,0,e.price),this._addDiffToPoint(this.p2,e.logical,0)))}_onMouseDown(){this._startDragPoint=null;const e=this._latestHoverPoint,t=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(t.x&&i.x&&t.y&&i.y))return this._moveToState(p.DRAGGING);const s=10;Math.abs(e.x-t.x)l-p&&rh-p&&ai.appendChild(this.makeColorBox(e))));let s=document.createElement("div");s.style.backgroundColor=window.pane.borderColor,s.style.height="1px",s.style.width="130px";let o=document.createElement("div");o.style.margin="10px";let n=document.createElement("div");n.style.color="lightgray",n.style.fontSize="12px",n.innerText="Opacity",this._opacityLabel=document.createElement("div"),this._opacityLabel.style.color="lightgray",this._opacityLabel.style.fontSize="12px",this._opacitySlider=document.createElement("input"),this._opacitySlider.type="range",this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%",this._opacitySlider.oninput=()=>{this._opacityLabel.innerText=this._opacitySlider.value+"%",this.opacity=parseInt(this._opacitySlider.value)/100,this.updateColor()},o.appendChild(n),o.appendChild(this._opacitySlider),o.appendChild(this._opacityLabel),this._div.appendChild(i),this._div.appendChild(s),this._div.appendChild(o),window.containerDiv.appendChild(this._div)}_updateOpacitySlider(){this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%"}makeColorBox(e){const t=document.createElement("div");t.style.width="18px",t.style.height="18px",t.style.borderRadius="3px",t.style.margin="3px",t.style.boxSizing="border-box",t.style.backgroundColor=e,t.addEventListener("mouseover",(()=>t.style.border="2px solid lightgray")),t.addEventListener("mouseout",(()=>t.style.border="none"));const i=G.extractRGBA(e);return t.addEventListener("click",(()=>{this.rgba=i,this.updateColor()})),t}static extractRGBA(e){const t=document.createElement("div");t.style.color=e,document.body.appendChild(t);const i=getComputedStyle(t).color;document.body.removeChild(t);const s=i.match(/\d+/g)?.map(Number);if(!s)return[];let o=i.includes("rgba")?parseFloat(i.split(",")[3]):1;return[s[0],s[1],s[2],o]}updateColor(){if(!u.lastHoveredObject||!this.rgba)return;const e=`rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`;u.lastHoveredObject.applyOptions({[this.colorOption]:e}),this.saveDrawings()}openMenu(e){u.lastHoveredObject&&(this.rgba=G.extractRGBA(u.lastHoveredObject._options[this.colorOption]),this.opacity=this.rgba[3],this._updateOpacitySlider(),this._div.style.top=e.top-30+"px",this._div.style.left=e.right+"px",this._div.style.display="flex",setTimeout((()=>document.addEventListener("mousedown",(e=>{this._div.contains(e.target)||this.closeMenu()}))),10))}closeMenu(){document.body.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}class P{static _styles=[{name:"Solid",var:t.LineStyle.Solid},{name:"Dotted",var:t.LineStyle.Dotted},{name:"Dashed",var:t.LineStyle.Dashed},{name:"Large Dashed",var:t.LineStyle.LargeDashed},{name:"Sparse Dotted",var:t.LineStyle.SparseDotted}];_div;_saveDrawings;constructor(e){this._saveDrawings=e,this._div=document.createElement("div"),this._div.classList.add("context-menu"),P._styles.forEach((e=>{this._div.appendChild(this._makeTextBox(e.name,e.var))})),window.containerDiv.appendChild(this._div)}_makeTextBox(e,t){const i=document.createElement("span");return i.classList.add("context-menu-item"),i.innerText=e,i.addEventListener("click",(()=>{u.lastHoveredObject?.applyOptions({lineStyle:t}),this._saveDrawings()})),i}openMenu(e){this._div.style.top=e.top-30+"px",this._div.style.left=e.right+"px",this._div.style.display="block",setTimeout((()=>document.addEventListener("mousedown",(e=>{this._div.contains(e.target)||this.closeMenu()}))),10)}closeMenu(){document.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}function N(e){const t=[];for(const i of e)0==t.length?t.push(i.toUpperCase()):i==i.toUpperCase()?t.push(" "+i):t.push(i);return t.join("")}class R{saveDrawings;drawingTool;div;hoverItem;items=[];constructor(e,t){this.saveDrawings=e,this.drawingTool=t,this._onRightClick=this._onRightClick.bind(this),this.div=document.createElement("div"),this.div.classList.add("context-menu"),document.body.appendChild(this.div),this.hoverItem=null,document.body.addEventListener("contextmenu",this._onRightClick)}_handleClick=e=>this._onClick(e);_onClick(e){e.target&&(this.div.contains(e.target)||(this.div.style.display="none",document.body.removeEventListener("click",this._handleClick)))}_onRightClick(e){if(!u.hoveredObject)return;for(const e of this.items)this.div.removeChild(e);this.items=[];for(const e of Object.keys(u.hoveredObject._options)){let t;if(e.toLowerCase().includes("color"))t=new G(this.saveDrawings,e);else{if("lineStyle"!==e)continue;t=new P(this.saveDrawings)}let i=e=>t.openMenu(e);this.menuItem(N(e),i,(()=>{document.removeEventListener("click",t.closeMenu),t._div.style.display="none"}))}this.separator(),this.menuItem("Delete Drawing",(()=>this.drawingTool.delete(u.lastHoveredObject))),e.preventDefault(),this.div.style.left=e.clientX+"px",this.div.style.top=e.clientY+"px",this.div.style.display="block",document.body.addEventListener("click",this._handleClick)}menuItem(e,t,i=null){const s=document.createElement("span");s.classList.add("context-menu-item"),this.div.appendChild(s);const o=document.createElement("span");if(o.innerText=e,o.style.pointerEvents="none",s.appendChild(o),i){let e=document.createElement("span");e.innerText="►",e.style.fontSize="8px",e.style.pointerEvents="none",s.appendChild(e)}if(s.addEventListener("mouseover",(()=>{this.hoverItem&&this.hoverItem.closeAction&&this.hoverItem.closeAction(),this.hoverItem={elem:o,action:t,closeAction:i}})),i){let e;s.addEventListener("mouseover",(()=>e=setTimeout((()=>t(s.getBoundingClientRect())),100))),s.addEventListener("mouseout",(()=>clearTimeout(e)))}else s.addEventListener("click",(e=>{t(e),this.div.style.display="none"}));this.items.push(s)}separator(){const e=document.createElement("div");e.style.width="90%",e.style.height="1px",e.style.margin="3px 0px",e.style.backgroundColor=window.pane.borderColor,this.div.appendChild(e),this.items.push(e)}}class $ extends x{_type="RayLine";constructor(e,t){super({...e},t),this._point.time=e.time}updatePoints(...e){for(const t of e)t&&(this._point=t);this.requestUpdate()}_onDrag(e){this._addDiffToPoint(this._point,e.logical,e.price),this.requestUpdate()}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this.series.priceToCoordinate(this._point.price),s=this._point.time?this.chart.timeScale().timeToCoordinate(this._point.time):null;return!(!i||!s)&&(Math.abs(i-e.point.y)s-t)}}class B extends _{_point={x:null,y:null};constructor(e,t){super(t),this._point=e}draw(e){e.useBitmapCoordinateSpace((e=>{if(null==this._point.x)return;const t=e.context,i=this._point.x*e.horizontalPixelRatio;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,g(t,this._options.lineStyle),t.beginPath(),t.moveTo(i,0),t.lineTo(i,e.bitmapSize.height),t.stroke()}))}}class A extends v{_source;_point={x:null,y:null};constructor(e){super(e),this._source=e}update(){const e=this._source._point,t=this._source.chart.timeScale(),i=this._source.series;this._point.x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical),this._point.y=i.priceToCoordinate(e.price)}renderer(){return new B(this._point,this._source._options)}}class O{_source;_x=null;constructor(e){this._source=e}update(){if(!this._source.chart||!this._source._point)return;const e=this._source._point,t=this._source.chart.timeScale();this._x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical)}visible(){return!!this._source._options.text}tickVisible(){return!0}coordinate(){return this._x??0}text(){return this._source._options.text||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class F extends u{_type="VerticalLine";_paneViews;_timeAxisViews;_point;_callbackName;_startDragPoint=null;constructor(e,t,i=null){super(t),this._point=e,this._paneViews=[new A(this)],this._callbackName=i,this._timeAxisViews=[new O(this)]}updateAllViews(){this._paneViews.forEach((e=>e.update())),this._timeAxisViews.forEach((e=>e.update()))}timeAxisViews(){return this._timeAxisViews}updatePoints(...e){for(const t of e)t&&(!t.time&&t.logical&&(t.time=this.series.dataByIndex(t.logical)?.time||null),this._point=t);this.requestUpdate()}get points(){return[this._point]}_moveToState(e){switch(e){case p.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case p.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case p.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._addDiffToPoint(this._point,e.logical,0),this.requestUpdate()}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this.chart.timeScale();let s;return s=this._point.time?i.timeToCoordinate(this._point.time):i.logicalToCoordinate(this._point.logical),!!s&&Math.abs(s-e.point.x){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class H{static TREND_SVG='';static HORZ_SVG='';static RAY_SVG='';static BOX_SVG='';static VERT_SVG=H.RAY_SVG;div;activeIcon=null;buttons=[];_commandFunctions;_handlerID;_drawingTool;constructor(e,t,i,s){this._handlerID=e,this._commandFunctions=s,this._drawingTool=new C(t,i,(()=>this.removeActiveAndSave())),this.div=this._makeToolBox(),new R(this.saveDrawings,this._drawingTool),s.push((e=>{if((e.metaKey||e.ctrlKey)&&"KeyZ"===e.code){const e=this._drawingTool.drawings.pop();return e&&this._drawingTool.delete(e),!0}return!1}))}toJSON(){const{...e}=this;return e}_makeToolBox(){let e=document.createElement("div");e.classList.add("toolbox"),this.buttons.push(this._makeToolBoxElement(L,"KeyT",H.TREND_SVG)),this.buttons.push(this._makeToolBoxElement(x,"KeyH",H.HORZ_SVG)),this.buttons.push(this._makeToolBoxElement($,"KeyR",H.RAY_SVG)),this.buttons.push(this._makeToolBoxElement(M,"KeyB",H.BOX_SVG)),this.buttons.push(this._makeToolBoxElement(F,"KeyV",H.VERT_SVG,!0));for(const t of this.buttons)e.appendChild(t);return e}_makeToolBoxElement(e,t,i,s=!1){const o=document.createElement("div");o.classList.add("toolbox-button");const n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("width","29"),n.setAttribute("height","29");const r=document.createElementNS("http://www.w3.org/2000/svg","g");r.innerHTML=i,r.setAttribute("fill",window.pane.color),n.appendChild(r),o.appendChild(n);const a={div:o,group:r,type:e};return o.addEventListener("click",(()=>this._onIconClick(a))),this._commandFunctions.push((e=>this._handlerID===window.handlerInFocus&&(!(!e.altKey||e.code!==t)&&(e.preventDefault(),this._onIconClick(a),!0)))),1==s&&(n.style.transform="rotate(90deg)",n.style.transformBox="fill-box",n.style.transformOrigin="center"),o}_onIconClick(e){this.activeIcon&&(this.activeIcon.div.classList.remove("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.stopDrawing(),this.activeIcon===e)?this.activeIcon=null:(this.activeIcon=e,this.activeIcon.div.classList.add("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.beginDrawing(this.activeIcon.type))}removeActiveAndSave=()=>{window.setCursor("default"),this.activeIcon&&this.activeIcon.div.classList.remove("active-toolbox-button"),this.activeIcon=null,this.saveDrawings()};addNewDrawing(e){this._drawingTool.addNewDrawing(e)}clearDrawings(){this._drawingTool.clearDrawings()}saveDrawings=()=>{const e=[];for(const t of this._drawingTool.drawings)e.push({type:t._type,points:t.points,options:t._options});const t=JSON.stringify(e);window.callbackFunction(`save_drawings${this._handlerID}_~_${t}`)};loadDrawings(e){e.forEach((e=>{switch(e.type){case"Box":this._drawingTool.addNewDrawing(new M(e.points[0],e.points[1],e.options));break;case"TrendLine":this._drawingTool.addNewDrawing(new L(e.points[0],e.points[1],e.options));break;case"HorizontalLine":this._drawingTool.addNewDrawing(new x(e.points[0],e.options));break;case"RayLine":this._drawingTool.addNewDrawing(new $(e.points[0],e.options));break;case"VerticalLine":this._drawingTool.addNewDrawing(new F(e.points[0],e.options))}}))}}class V{makeButton;callbackName;div;isOpen=!1;widget;constructor(e,t,i,s,o,n){this.makeButton=e,this.callbackName=t,this.div=document.createElement("div"),this.div.classList.add("topbar-menu"),this.widget=this.makeButton(s+" ↓",null,o,!0,n),this.updateMenuItems(i),this.widget.elem.addEventListener("click",(()=>{if(this.isOpen=!this.isOpen,!this.isOpen)return void(this.div.style.display="none");let e=this.widget.elem.getBoundingClientRect();this.div.style.display="flex",this.div.style.flexDirection="column";let t=e.x+e.width/2;this.div.style.left=t-this.div.clientWidth/2+"px",this.div.style.top=e.y+e.height+"px"})),document.body.appendChild(this.div)}updateMenuItems(e){this.div.innerHTML="",e.forEach((e=>{let t=this.makeButton(e,null,!1,!1);t.elem.addEventListener("click",(()=>{this._clickHandler(t.elem.innerText)})),t.elem.style.margin="4px 4px",t.elem.style.padding="2px 2px",this.div.appendChild(t.elem)})),this.widget.elem.innerText=e[0]+" ↓"}_clickHandler(e){this.widget.elem.innerText=e+" ↓",window.callbackFunction(`${this.callbackName}_~_${e}`),this.div.style.display="none",this.isOpen=!1}}class U{_handler;_div;left;right;constructor(e){this._handler=e,this._div=document.createElement("div"),this._div.classList.add("topbar");const t=e=>{const t=document.createElement("div");return t.classList.add("topbar-container"),t.style.justifyContent=e,this._div.appendChild(t),t};this.left=t("flex-start"),this.right=t("flex-end")}makeSwitcher(e,t,i,s="left"){const o=document.createElement("div");let n;o.style.margin="4px 12px";const r={elem:o,callbackName:i,intervalElements:e.map((e=>{const i=document.createElement("button");i.classList.add("topbar-button"),i.classList.add("switcher-button"),i.style.margin="0px 2px",i.innerText=e,e==t&&(n=i,i.classList.add("active-switcher-button"));const s=U.getClientWidth(i);return i.style.minWidth=s+1+"px",i.addEventListener("click",(()=>r.onItemClicked(i))),o.appendChild(i),i})),onItemClicked:e=>{e!=n&&(n.classList.remove("active-switcher-button"),e.classList.add("active-switcher-button"),n=e,window.callbackFunction(`${r.callbackName}_~_${e.innerText}`))}};return this.appendWidget(o,s,!0),r}makeTextBoxWidget(e,t="left",i=null){if(i){const s=document.createElement("input");return s.classList.add("topbar-textbox-input"),s.value=e,s.style.width=`${s.value.length+2}ch`,s.addEventListener("focus",(()=>{window.textBoxFocused=!0})),s.addEventListener("input",(e=>{e.preventDefault(),s.style.width=`${s.value.length+2}ch`})),s.addEventListener("keydown",(e=>{"Enter"==e.key&&(e.preventDefault(),s.blur())})),s.addEventListener("blur",(()=>{window.callbackFunction(`${i}_~_${s.value}`),window.textBoxFocused=!1})),this.appendWidget(s,t,!0),s}{const i=document.createElement("div");return i.classList.add("topbar-textbox"),i.innerText=e,this.appendWidget(i,t,!0),i}}makeMenu(e,t,i,s,o){return new V(this.makeButton.bind(this),s,e,t,i,o)}makeButton(e,t,i,s=!0,o="left",n=!1){let r=document.createElement("button");r.classList.add("topbar-button"),r.innerText=e,document.body.appendChild(r),r.style.minWidth=r.clientWidth+1+"px",document.body.removeChild(r);let a={elem:r,callbackName:t};if(t){let e;if(n){let t=!1;e=()=>{t=!t,window.callbackFunction(`${a.callbackName}_~_${t}`),r.style.backgroundColor=t?"var(--active-bg-color)":"",r.style.color=t?"var(--active-color)":""}}else e=()=>window.callbackFunction(`${a.callbackName}_~_${r.innerText}`);r.addEventListener("click",e)}return s&&this.appendWidget(r,o,i),a}makeSeparator(e="left"){const t=document.createElement("div");t.classList.add("topbar-seperator");("left"==e?this.left:this.right).appendChild(t)}appendWidget(e,t,i){const s="left"==t?this.left:this.right;i?("left"==t&&s.appendChild(e),this.makeSeparator(t),"right"==t&&s.appendChild(e)):s.appendChild(e),this._handler.reSize()}static getClientWidth(e){document.body.appendChild(e);const t=e.clientWidth;return document.body.removeChild(e),t}}function z(e,t){if(e.startsWith("#"))return function(e,t){if(e=e.replace(/^#/,""),!/^([0-9A-F]{3}){1,2}$/i.test(e))throw new Error("Invalid hex color format.");const[i,s,o]=(e=>3===e.length?[parseInt(e[0]+e[0],16),parseInt(e[1]+e[1],16),parseInt(e[2]+e[2],16)]:[parseInt(e.slice(0,2),16),parseInt(e.slice(2,4),16),parseInt(e.slice(4,6),16)])(e);return`rgba(${i}, ${s}, ${o}, ${t})`}(e,t);if(e.startsWith("rgba")||e.startsWith("rgb"))return e.replace(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/,`rgba($1, $2, $3, ${t})`);throw new Error("Unsupported color format. Use hex, rgb, or rgba.")}function W(e,t=.2){let[i,s,o,n=1]=e.startsWith("#")?[...(r=e,3===(r=r.replace(/^#/,"")).length?[parseInt(r[0]+r[0],16),parseInt(r[1]+r[1],16),parseInt(r[2]+r[2],16)]:[parseInt(r.slice(0,2),16),parseInt(r.slice(2,4),16),parseInt(r.slice(4,6),16)]),1]:e.match(/\d+(\.\d+)?/g).map(Number);var r;return i=Math.max(0,Math.min(255,i*(1-t))),s=Math.max(0,Math.min(255,s*(1-t))),o=Math.max(0,Math.min(255,o*(1-t))),e.startsWith("#")?`#${((1<<24)+(Math.round(i)<<16)+(Math.round(s)<<8)+Math.round(o)).toString(16).slice(1)}`:`rgba(${Math.round(i)}, ${Math.round(s)}, ${Math.round(o)}, ${n})`}function j(e){return function(e){return Math.max(1,Math.floor(e))}(e)/e}class q{_data=null;_options=null;draw(e,t){e.useBitmapCoordinateSpace((e=>this._drawImpl(e,t)))}update(e,t){this._data=e,this._options=t}_seggregate(e,t){const i=this._options?.chandelierSize||1,s=[];for(let o=0;oe.originalData?.high??e.high))),d=Math.min(...e.map((e=>e.originalData?.low??e.low))),c=s(h)??0,p=s(d)??0,u=e[0].x,_=r>n,m=_?this._options?.upColor||"rgba(0,255,0,.333)":this._options?.downColor||"rgba(255,0,0,.333)",g=_?this._options?.borderUpColor||z(m,1):this._options?.borderDownColor||z(m,1);return{open:a,high:c,low:p,close:l,x:u,isUp:_,startIndex:t,endIndex:i,isInProgress:o,color:m,borderColor:g,wickColor:_?this._options?.wickUpColor||g:this._options?.wickDownColor||g}}_drawImpl(e,t){if(null===this._data||0===this._data.bars.length||null===this._data.visibleRange||null===this._options)return;let i=-1/0;const s=this._data.bars.map(((e,t)=>{const s=e.originalData.close>=e.originalData.open;i=e.originalData.close??i;const o=e.originalData.open??0,n=e.originalData.high??0,r=e.originalData.low??0,a=e.originalData.close??0,l=s?this._options?.downColor||"rgba(0,0,0,0)":this._options?.upColor||"rgba(0,0,0,0)",h=s?this._options?.borderDownColor||l:this._options?.borderUpColor||l,d=s?this._options?.wickDownColor||l:this._options?.wickUpColor||l;return{open:o,high:n,low:r,close:a,x:e.x,isUp:s,startIndex:t,endIndex:t,color:l,borderColor:h,wickColor:d}})),o=this._seggregate(s,t),n=this._options.radius(this._data.barSpacing),{horizontalPixelRatio:r,verticalPixelRatio:a}=e,l=this._data.barSpacing*r;this._drawCandles(e,o,this._data.visibleRange,n,l,r,a),this._drawWicks(e,o,this._data.visibleRange)}_drawWicks(e,t,i){if(null===this._data||null===this._options||!this._options?.wickVisible)return;if("3d"===this._options.shape)return;const{context:s,horizontalPixelRatio:o,verticalPixelRatio:n}=e,r=this._data.barSpacing*o,a=j(o);for(const e of t){if(e.startIndexi.to)continue;s.fillStyle=e.wickColor;const t=e.low*n,l=e.high*n,h=Math.min(e.open,e.close)*n,d=Math.max(e.open,e.close)*n,c=this._options?.barSpacing??.8;let p=e.x*o;const u=e.endIndex-e.startIndex;u&&u>1&&(p=p+r*Math.max(1,u)/2-(1-c)*r);let _=l,m=h,g=d,w=t;s.save(),s.lineWidth=this._options?.lineWidth??1,"Polygon"===this._options.shape&&(m=(l+h)/2,g=(t+d)/2);const v=m-_;v>0&&s.strokeRect(p-Math.floor(a/2),_,a,v);const y=w-g;y>0&&s.strokeRect(p-Math.floor(a/2),g,a,y),s.restore()}}_drawCandles(e,t,i,s,o,n,r){const{context:a}=e,l=this._options?.barSpacing??.8;for(const e of t){const t=e.endIndex-e.startIndex;let h=1!==this._options?.chandelierSize?o*Math.max(1,t+1)-(1-l)*o:o*l;const d=e.x*n,c=o*l;if(e.startIndexi.to)continue;const p=Math.min(e.open,e.close)*r,u=Math.max(e.open,e.close)*r,_=p-u,m=(p+u)/2;switch(a.save(),a.fillStyle=e.color,a.strokeStyle=e.borderColor,a.lineWidth=1.5,g(a,this._options?.lineStyle??1),a.lineWidth=this._options?.lineWidth??1,this._options?.shape){case"Rectangle":default:this._drawCandle(a,d,m,c,h,_);break;case"Rounded":this._drawRounded(a,d,u,c,h,_,s,n);break;case"Ellipse":this._drawEllipse(a,d,m,c,h,_);break;case"Arrow":this._drawArrow(a,d,p,u,c,h,e.high*r,e.low*r,e.isUp);break;case"3d":this._draw3d(a,d,e.high*r,e.low*r,e.open*r,e.close*r,c,h,e.color,e.borderColor,e.isUp,l);break;case"Polygon":this._drawPolygon(a,d,u+_,u,c,h,e.high*r,e.low*r,e.isUp)}a.restore()}}_drawCandle(e,t,i,s,o,n){const r=t-s/2,a=t-s/2+o,l=i-n/2,h=i+n/2;e.beginPath(),e.moveTo(r,l),e.lineTo(r,h),e.lineTo(a,h),e.lineTo(a,l),e.closePath(),e.fill(),e.stroke()}_drawEllipse(e,t,i,s,o,n){const r=o/2,a=n/2,l=t-s/2+o/2;e.beginPath(),e.ellipse(l,i,Math.abs(r),Math.abs(a),0,0,2*Math.PI),e.fill(),e.stroke()}_drawRounded(e,t,i,s,o,n,r,a){if(e.roundRect){const l=Math.abs(Math.min(r,.1*Math.min(s,n),5))*a;e.beginPath(),e.roundRect(t-s/2,i,o,n,l),e.stroke(),e.fill()}else e.strokeRect(t-s/2,i,o,n),e.fillRect(t-s/2,i,o,n)}_draw3d(e,t,i,s,o,n,r,a,l,h,d,c){const p=-Math.max(a,1)*(1-c),u=W(l,.666),_=W(l,.333),m=W(l,.2),g=t-r/2,w=t-r/2+a+p,v=g-p,y=w-p;let b,f,x,C;d?(b=o,f=s,x=i,C=n):(b=o,f=i,x=s,C=n),e.fillStyle=_,e.strokeStyle=h,e.fillStyle=m,d?(e.fillStyle=u,e.beginPath(),e.moveTo(g,f),e.lineTo(v,C),e.lineTo(y,C),e.lineTo(w,f),e.closePath(),e.fill(),e.stroke(),e.fillStyle=u,e.beginPath(),e.moveTo(g,b),e.lineTo(v,x),e.lineTo(v,C),e.lineTo(g,f),e.closePath(),e.fill(),e.stroke(),e.fillStyle=u,e.beginPath(),e.moveTo(w,b),e.lineTo(y,x),e.lineTo(y,C),e.lineTo(w,f),e.closePath(),e.fill(),e.stroke(),e.fillStyle=m,e.beginPath(),e.moveTo(g,b),e.lineTo(v,x),e.lineTo(y,x),e.lineTo(w,b),e.closePath(),e.fill(),e.stroke()):(e.fillStyle=m,e.beginPath(),e.moveTo(g,b),e.lineTo(v,x),e.lineTo(y,x),e.lineTo(w,b),e.closePath(),e.fill(),e.stroke(),e.fillStyle=_,e.beginPath(),e.moveTo(w,b),e.lineTo(y,x),e.lineTo(y,C),e.lineTo(w,f),e.closePath(),e.fill(),e.stroke(),e.fillStyle=_,e.beginPath(),e.moveTo(g,b),e.lineTo(v,x),e.lineTo(v,C),e.lineTo(g,f),e.closePath(),e.fill(),e.stroke(),e.fillStyle=_,e.beginPath(),e.moveTo(g,f),e.lineTo(v,C),e.lineTo(y,C),e.lineTo(w,f),e.closePath(),e.fill(),e.stroke())}_drawPolygon(e,t,i,s,o,n,r,a,l){e.beginPath(),l?(e.moveTo(t-o/2,i),e.lineTo(t+n-o/2,r),e.lineTo(t+n-o/2,s),e.lineTo(t-o/2,a)):(e.moveTo(t-o/2,r),e.lineTo(t+n-o/2,i),e.lineTo(t+n-o/2,a),e.lineTo(t-o/2,s)),e.closePath(),e.stroke(),e.fill()}_drawArrow(e,t,i,s,o,n,r,a,l){e.beginPath();const h=t-o/2,d=h+n,c=h+n/2;l?(e.moveTo(h,a),e.lineTo(h,i),e.lineTo(c,r),e.lineTo(d,i),e.lineTo(d,a),e.lineTo(c,s),e.lineTo(h,a)):(e.moveTo(h,r),e.lineTo(h,s),e.lineTo(c,a),e.lineTo(d,s),e.lineTo(d,r),e.lineTo(c,i),e.lineTo(h,r)),e.closePath(),e.fill(),e.stroke()}}const K={...t.customSeriesDefaultOptions,upColor:"#26a69a",downColor:"#ef5350",wickVisible:!0,borderVisible:!0,borderColor:"#378658",borderUpColor:"#26a69a",borderDownColor:"#ef5350",wickColor:"#737375",wickUpColor:"#26a69a",wickDownColor:"#ef5350",radius:function(e){return e<4?0:e/3},shape:"Rectangle",chandelierSize:1,barSpacing:.8,lineStyle:0,lineWidth:1};class X{_renderer;constructor(){this._renderer=new q}priceValueBuilder(e){return[e.high,e.low,e.close]}renderer(){return this._renderer}isWhitespace(e){return void 0===e.close}update(e,t){this._renderer.update(e,t)}defaultOptions(){return K}}s();return e.Box=M,e.Handler=class{id;commandFunctions=[];wrapper;div;chart;scale;precision=2;series;volumeSeries;legend;_topBar;toolBox;spinner;_seriesList=[];seriesMap=new Map;constructor(e,t,i,s,o){this.reSize=this.reSize.bind(this),this.id=e,this.scale={width:t,height:i},this.wrapper=document.createElement("div"),this.wrapper.classList.add("handler"),this.wrapper.style.float=s,this.div=document.createElement("div"),this.div.style.position="relative",this.wrapper.appendChild(this.div),window.containerDiv.append(this.wrapper),this.chart=this._createChart(),this.series=this.createCandlestickSeries(),this.volumeSeries=this.createVolumeSeries(),this.legend=new l(this),document.addEventListener("keydown",(e=>{for(let t=0;twindow.handlerInFocus=this.id)),this.reSize(),o&&window.addEventListener("resize",(()=>this.reSize()))}reSize(){let e=0!==this.scale.height&&this._topBar?._div.offsetHeight||0;this.chart.resize(window.innerWidth*this.scale.width,window.innerHeight*this.scale.height-e),this.wrapper.style.width=100*this.scale.width+"%",this.wrapper.style.height=100*this.scale.height+"%",0===this.scale.height||0===this.scale.width?this.toolBox&&(this.toolBox.div.style.display="none"):this.toolBox&&(this.toolBox.div.style.display="flex")}primitives=new Map;_createChart(){return t.createChart(this.div,{width:window.innerWidth*this.scale.width,height:window.innerHeight*this.scale.height,layout:{textColor:window.pane.color,background:{color:"#000000",type:t.ColorType.Solid},fontSize:12},rightPriceScale:{scaleMargins:{top:.3,bottom:.25}},timeScale:{timeVisible:!0,secondsVisible:!1},crosshair:{mode:t.CrosshairMode.Normal,vertLine:{labelBackgroundColor:"rgb(46, 46, 46)"},horzLine:{labelBackgroundColor:"rgb(55, 55, 55)"}},grid:{vertLines:{color:"rgba(29, 30, 38, 5)"},horzLines:{color:"rgba(29, 30, 58, 5)"}},handleScroll:{vertTouchDrag:!0}})}createCandlestickSeries(){const e="rgba(39, 157, 130, 100)",t="rgba(200, 97, 100, 100)",i=this.chart.addCandlestickSeries({upColor:e,borderUpColor:e,wickUpColor:e,downColor:t,borderDownColor:t,wickDownColor:t});return i.priceScale().applyOptions({scaleMargins:{top:.2,bottom:.2}}),i}createVolumeSeries(){const e=this.chart.addHistogramSeries({color:"#26a69a",priceFormat:{type:"volume"},priceScaleId:"volume_scale"});return e.priceScale().applyOptions({scaleMargins:{top:.8,bottom:0}}),e}createLineSeries(e,t){const{group:i,legendSymbol:s="▨",...o}=t,n=this.chart.addLineSeries(o);this._seriesList.push(n),this.seriesMap.set(e,n);const r=n.options().color||"rgba(255,0,0,1)",a={name:e,series:n,colors:[r.startsWith("rgba")?r.replace(/[^,]+(?=\))/,"1"):r],legendSymbol:[s],seriesType:"Line",group:i};return this.legend.addLegendItem(a),{name:e,series:n}}createHistogramSeries(e,t){const{group:i,legendSymbol:s="▨",...o}=t,n=this.chart.addHistogramSeries(o);this._seriesList.push(n),this.seriesMap.set(e,n);const r=n.options().color||"rgba(255,0,0,1)",a={name:e,series:n,colors:[r.startsWith("rgba")?r.replace(/[^,]+(?=\))/,"1"):r],legendSymbol:[s],seriesType:"Histogram",group:i};return this.legend.addLegendItem(a),{name:e,series:n}}createAreaSeries(e,t){const{group:i,legendSymbol:s="▨",...o}=t,n=this.chart.addAreaSeries(o);this._seriesList.push(n),this.seriesMap.set(e,n);const r=n.options().lineColor||"rgba(255,0,0,1)",a={name:e,series:n,colors:[r.startsWith("rgba")?r.replace(/[^,]+(?=\))/,"1"):r],legendSymbol:[s],seriesType:"Area",group:i};return this.legend.addLegendItem(a),{name:e,series:n}}createBarSeries(e,t){const{group:i,legendSymbol:s=["▨","▨"],...o}=t,n=this.chart.addBarSeries(o);this._seriesList.push(n),this.seriesMap.set(e,n);const r=n.options().upColor||"rgba(0,255,0,1)",a=n.options().downColor||"rgba(255,0,0,1)",l={name:e,series:n,colors:[r,a],legendSymbol:s,seriesType:"Bar",group:i};return this.legend.addLegendItem(l),{name:e,series:n}}createCustomOHLCSeries(e,t={}){const i="ohlc",s={...K,...t,seriesType:i},{group:o,legendSymbol:n=["⑃","⑂"],chandelierSize:r=1,...a}=s,l=new X,h=this.chart.addCustomSeries(l,{...a,chandelierSize:r});this._seriesList.push(h),this.seriesMap.set(e,h);const d={name:e,series:h,colors:[s.borderUpColor||s.upColor,s.borderDownColor||s.downColor],legendSymbol:r>1?n.map((e=>`${e} (${r})`)):n,seriesType:i,group:o};return this.legend.addLegendItem(d),{name:e,series:h}}removeSeries(e){const t=this.seriesMap.get(e);t?(this.chart.removeSeries(t),this._seriesList=this._seriesList.filter((e=>e!==t)),this.seriesMap.delete(e),this.legend.deleteLegendEntry(e),console.log(`Series "${e}" removed.`)):console.warn(`Series "${e}" not found.`)}createToolBox(){this.toolBox=new H(this.id,this.chart,this.series,this.commandFunctions),this.div.appendChild(this.toolBox.div)}createTopBar(){return this._topBar=new U(this),this.wrapper.prepend(this._topBar._div),this._topBar}toJSON(){const{chart:e,...t}=this;return t}static syncCharts(e,t,i=!1){function s(e,t){t?(e.chart.setCrosshairPosition(t.value||t.close,t.time,e.series),e.legend.legendHandler(t,!0)):e.chart.clearCrosshairPosition()}function o(e,t){return t.time&&t.seriesData.get(e)||null}const n=e.chart.timeScale(),r=t.chart.timeScale(),a=e=>{e&&n.setVisibleLogicalRange(e)},l=e=>{e&&r.setVisibleLogicalRange(e)},h=i=>{s(t,o(e.series,i))},d=i=>{s(e,o(t.series,i))};let c=t;function p(e,t,s,o,n,r){e.wrapper.addEventListener("mouseover",(()=>{c!==e&&(c=e,t.chart.unsubscribeCrosshairMove(s),e.chart.subscribeCrosshairMove(o),i||(t.chart.timeScale().unsubscribeVisibleLogicalRangeChange(n),e.chart.timeScale().subscribeVisibleLogicalRangeChange(r)))}))}p(t,e,h,d,l,a),p(e,t,d,h,a,l),t.chart.subscribeCrosshairMove(d);const u=r.getVisibleLogicalRange();u&&n.setVisibleLogicalRange(u),i||t.chart.timeScale().subscribeVisibleLogicalRangeChange(a)}static makeSearchBox(e){const t=document.createElement("div");t.classList.add("searchbox"),t.style.display="none";const i=document.createElement("div");i.innerHTML='';const s=document.createElement("input");return s.type="text",t.appendChild(i),t.appendChild(s),e.div.appendChild(t),e.commandFunctions.push((i=>window.handlerInFocus===e.id&&!window.textBoxFocused&&("none"===t.style.display?!!/^[a-zA-Z0-9]$/.test(i.key)&&(t.style.display="flex",s.focus(),!0):("Enter"===i.key||"Escape"===i.key)&&("Enter"===i.key&&window.callbackFunction(`search${e.id}_~_${s.value}`),t.style.display="none",s.value="",!0)))),s.addEventListener("input",(()=>s.value=s.value.toUpperCase())),{window:t,box:s}}static makeSpinner(e){e.spinner=document.createElement("div"),e.spinner.classList.add("spinner"),e.wrapper.appendChild(e.spinner);let t=0;!function i(){e.spinner&&(t+=10,e.spinner.style.transform=`translate(-50%, -50%) rotate(${t}deg)`,requestAnimationFrame(i))}()}static _styleMap={"--bg-color":"backgroundColor","--hover-bg-color":"hoverBackgroundColor","--click-bg-color":"clickBackgroundColor","--active-bg-color":"activeBackgroundColor","--muted-bg-color":"mutedBackgroundColor","--border-color":"borderColor","--color":"color","--active-color":"activeColor"};static setRootStyles(e){const t=document.documentElement.style;for(const[i,s]of Object.entries(this._styleMap))t.setProperty(i,e[s])}},e.HorizontalLine=x,e.Legend=l,e.RayLine=$,e.Table=class{_div;callbackName;borderColor;borderWidth;table;rows={};headings;widths;alignments;footer;header;constructor(e,t,i,s,o,n,r=!1,a,l,h,d,c){this._div=document.createElement("div"),this.callbackName=null,this.borderColor=l,this.borderWidth=h,r?(this._div.style.position="absolute",this._div.style.cursor="move"):(this._div.style.position="relative",this._div.style.float=n),this._div.style.zIndex="2000",this.reSize(e,t),this._div.style.display="flex",this._div.style.flexDirection="column",this._div.style.borderRadius="5px",this._div.style.color="white",this._div.style.fontSize="12px",this._div.style.fontVariantNumeric="tabular-nums",this.table=document.createElement("table"),this.table.style.width="100%",this.table.style.borderCollapse="collapse",this._div.style.overflow="hidden",this.headings=i,this.widths=s.map((e=>100*e+"%")),this.alignments=o;let p=this.table.createTHead().insertRow();for(let e=0;e0?c[e]:a,t.style.color=d[e],p.appendChild(t)}let u,_,m=document.createElement("div");if(m.style.overflowY="auto",m.style.overflowX="hidden",m.style.backgroundColor=a,m.appendChild(this.table),this._div.appendChild(m),window.containerDiv.appendChild(this._div),!r)return;let g=e=>{this._div.style.left=e.clientX-u+"px",this._div.style.top=e.clientY-_+"px"},w=()=>{document.removeEventListener("mousemove",g),document.removeEventListener("mouseup",w)};this._div.addEventListener("mousedown",(e=>{u=e.clientX-this._div.offsetLeft,_=e.clientY-this._div.offsetTop,document.addEventListener("mousemove",g),document.addEventListener("mouseup",w)}))}divToButton(e,t){e.addEventListener("mouseover",(()=>e.style.backgroundColor="rgba(60, 60, 60, 0.6)")),e.addEventListener("mouseout",(()=>e.style.backgroundColor="transparent")),e.addEventListener("mousedown",(()=>e.style.backgroundColor="rgba(60, 60, 60)")),e.addEventListener("click",(()=>window.callbackFunction(t))),e.addEventListener("mouseup",(()=>e.style.backgroundColor="rgba(60, 60, 60, 0.6)"))}newRow(e,t=!1){let i=this.table.insertRow();i.style.cursor="default";for(let s=0;s{e&&(window.cursor=e),document.body.style.cursor=window.cursor},e}({},LightweightCharts); diff --git a/lightweight_charts/util.py b/lightweight_charts/util.py index a99df78..761ee10 100644 --- a/lightweight_charts/util.py +++ b/lightweight_charts/util.py @@ -80,6 +80,7 @@ def jbool(b: bool): return 'true' if b is True else 'false' if b is False else N FLOAT = Literal['left', 'right', 'top', 'bottom'] +CANDLE_SHAPE = Literal['Ellipse','Rectangle','Hourglass','Bowtie','X','3d','Polygon'] def as_enum(value, string_types): types = string_types.__args__ diff --git a/package-lock.json b/package-lock.json index 11ddc75..c57cf6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,17 +6,17 @@ "": { "name": "lwc-plugin-trend-line", "dependencies": { - "dts-bundle-generator": "^8.0.1", + "dts-bundle-generator": "^8.1.2", "fancy-canvas": "^2.1.0", - "lightweight-charts": "^4.1.0-rc2", - "tslib": "^2.6.2" + "lightweight-charts": "^4.2.1", + "tslib": "^2.8.1" }, "devDependencies": { "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", - "rollup": "^4.13.0", - "typescript": "^5.4.3", - "vite": "^4.3.1" + "rollup": "^4.27.2", + "typescript": "^5.6.3", + "vite": "^4.5.5" } }, "node_modules/@esbuild/darwin-x64": { @@ -88,6 +88,8 @@ }, "node_modules/@rollup/plugin-terser": { "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", "dev": true, "license": "MIT", "dependencies": { @@ -109,6 +111,8 @@ }, "node_modules/@rollup/plugin-typescript": { "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz", + "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==", "dev": true, "license": "MIT", "dependencies": { @@ -153,8 +157,52 @@ } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.2.tgz", + "integrity": "sha512-Tj+j7Pyzd15wAdSJswvs5CJzJNV+qqSUcr/aCD+jpQSBtXvGnV0pnrjoc8zFTe9fcKCatkpFpOO7yAzpO998HA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.2.tgz", + "integrity": "sha512-xsPeJgh2ThBpUqlLgRfiVYBEf/P1nWlWvReG+aBWfNv3XEBpa6ZCmxSVnxJgLgkNz4IbxpLy64h2gCmAAQLneQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.2.tgz", + "integrity": "sha512-KnXU4m9MywuZFedL35Z3PuwiTSn/yqRIhrEA9j+7OSkji39NzVkgxuxTYg5F8ryGysq4iFADaU5osSizMXhU2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.0", + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.2.tgz", + "integrity": "sha512-Hj77A3yTvUeCIx/Vi+4d4IbYhyTwtHj07lVzUgpUq9YpJSEiGJj4vXMKwzJ3w5zp5v3PFvpJNgc/J31smZey6g==", "cpu": [ "x64" ], @@ -165,8 +213,206 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.2.tgz", + "integrity": "sha512-RjgKf5C3xbn8gxvCm5VgKZ4nn0pRAIe90J0/fdHUsgztd3+Zesb2lm2+r6uX4prV2eUByuxJNdt647/1KPRq5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.2.tgz", + "integrity": "sha512-duq21FoXwQtuws+V9H6UZ+eCBc7fxSpMK1GQINKn3fAyd9DFYKPJNcUhdIKOrMFjLEJgQskoMoiuizMt+dl20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.2.tgz", + "integrity": "sha512-6npqOKEPRZkLrMcvyC/32OzJ2srdPzCylJjiTJT2c0bwwSGm7nz2F9mNQ1WrAqCBZROcQn91Fno+khFhVijmFA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.2.tgz", + "integrity": "sha512-V9Xg6eXtgBtHq2jnuQwM/jr2mwe2EycnopO8cbOvpzFuySCGtKlPCI3Hj9xup/pJK5Q0388qfZZy2DqV2J8ftw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.2.tgz", + "integrity": "sha512-uCFX9gtZJoQl2xDTpRdseYuNqyKkuMDtH6zSrBTA28yTfKyjN9hQ2B04N5ynR8ILCoSDOrG/Eg+J2TtJ1e/CSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.2.tgz", + "integrity": "sha512-/PU9P+7Rkz8JFYDHIi+xzHabOu9qEWR07L5nWLIUsvserrxegZExKCi2jhMZRd0ATdboKylu/K5yAXbp7fYFvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.2.tgz", + "integrity": "sha512-eCHmol/dT5odMYi/N0R0HC8V8QE40rEpkyje/ZAXJYNNoSfrObOvG/Mn+s1F/FJyB7co7UQZZf6FuWnN6a7f4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.2.tgz", + "integrity": "sha512-DEP3Njr9/ADDln3kNi76PXonLMSSMiCir0VHXxmGSHxCxDfQ70oWjHcJGfiBugzaqmYdTC7Y+8Int6qbnxPBIQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.2.tgz", + "integrity": "sha512-NHGo5i6IE/PtEPh5m0yw5OmPMpesFnzMIS/lzvN5vknnC1sXM5Z/id5VgcNPgpD+wHmIcuYYgW+Q53v+9s96lQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.2.tgz", + "integrity": "sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.2.tgz", + "integrity": "sha512-dOlWEMg2gI91Qx5I/HYqOD6iqlJspxLcS4Zlg3vjk1srE67z5T2Uz91yg/qA8sY0XcwQrFzWWiZhMNERylLrpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.2.tgz", + "integrity": "sha512-euMIv/4x5Y2/ImlbGl88mwKNXDsvzbWUlT7DFky76z2keajCtcbAsN9LUdmk31hAoVmJJYSThgdA0EsPeTr1+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.2.tgz", + "integrity": "sha512-RsnE6LQkUHlkC10RKngtHNLxb7scFykEbEwOFDjr3CeCMG+Rr+cKqlkKc2/wJ1u4u990urRHCbjz31x84PBrSQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.2.tgz", + "integrity": "sha512-foJM5vv+z2KQmn7emYdDLyTbkoO5bkHZE1oth2tWbQNGW7mX32d46Hz6T0MqXdWS2vBZhaEtHqdy9WYwGfiliA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/estree": { - "version": "1.0.5", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true, "license": "MIT" }, @@ -239,6 +485,8 @@ }, "node_modules/dts-bundle-generator": { "version": "8.1.2", + "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-8.1.2.tgz", + "integrity": "sha512-/yvy9Xw0cfFodA8n6jEq8/COZ/WXgJtPabnLBAzIfP/TfxWbD/0a0dvfqNHneNqswQrH0kUcaAfGJC9UNvH97w==", "license": "MIT", "dependencies": { "typescript": ">=5.0.2", @@ -305,6 +553,8 @@ }, "node_modules/fancy-canvas": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fancy-canvas/-/fancy-canvas-2.1.0.tgz", + "integrity": "sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==", "license": "MIT" }, "node_modules/fsevents": { @@ -364,7 +614,9 @@ } }, "node_modules/lightweight-charts": { - "version": "4.1.3", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lightweight-charts/-/lightweight-charts-4.2.1.tgz", + "integrity": "sha512-nE2zCZ5Gp7KZbVHUJi6QhQLkYRvYyxsQTnSLEXIFmc8iHOFBT4rk/Dbyecq+CLW59FNuoCPNOYjZnS63/uHDrA==", "license": "Apache-2.0", "dependencies": { "fancy-canvas": "2.1.0" @@ -467,11 +719,13 @@ } }, "node_modules/rollup": { - "version": "4.13.0", + "version": "4.27.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.2.tgz", + "integrity": "sha512-KreA+PzWmk2yaFmZVwe6GB2uBD86nXl86OsDkt1bJS9p3vqWuEQ6HnJJ+j/mZi/q0920P99/MVRlB4L3crpF5w==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -481,19 +735,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@rollup/rollup-android-arm-eabi": "4.27.2", + "@rollup/rollup-android-arm64": "4.27.2", + "@rollup/rollup-darwin-arm64": "4.27.2", + "@rollup/rollup-darwin-x64": "4.27.2", + "@rollup/rollup-freebsd-arm64": "4.27.2", + "@rollup/rollup-freebsd-x64": "4.27.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.27.2", + "@rollup/rollup-linux-arm-musleabihf": "4.27.2", + "@rollup/rollup-linux-arm64-gnu": "4.27.2", + "@rollup/rollup-linux-arm64-musl": "4.27.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.27.2", + "@rollup/rollup-linux-riscv64-gnu": "4.27.2", + "@rollup/rollup-linux-s390x-gnu": "4.27.2", + "@rollup/rollup-linux-x64-gnu": "4.27.2", + "@rollup/rollup-linux-x64-musl": "4.27.2", + "@rollup/rollup-win32-arm64-msvc": "4.27.2", + "@rollup/rollup-win32-ia32-msvc": "4.27.2", + "@rollup/rollup-win32-x64-msvc": "4.27.2", "fsevents": "~2.3.2" } }, @@ -605,11 +864,15 @@ } }, "node_modules/tslib": { - "version": "2.6.2", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/typescript": { - "version": "5.4.3", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -620,7 +883,9 @@ } }, "node_modules/vite": { - "version": "4.5.2", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", + "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", "dev": true, "license": "MIT", "dependencies": { @@ -674,7 +939,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "3.29.4", + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index 4a903d6..2d5bcca 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,14 @@ "devDependencies": { "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", - "rollup": "^4.13.0", - "typescript": "^5.4.3", - "vite": "^4.3.1" + "rollup": "^4.27.2", + "typescript": "^5.6.3", + "vite": "^4.5.5" }, "dependencies": { - "dts-bundle-generator": "^8.0.1", + "dts-bundle-generator": "^8.1.2", "fancy-canvas": "^2.1.0", - "lightweight-charts": "^4.1.0-rc2", - "tslib": "^2.6.2" + "lightweight-charts": "^4.2.1", + "tslib": "^2.8.1" } } diff --git a/src/general/handler.ts b/src/general/handler.ts index acaca40..092813e 100644 --- a/src/general/handler.ts +++ b/src/general/handler.ts @@ -1,6 +1,5 @@ import { AreaStyleOptions, - BarData, BarStyleOptions, ColorType, CrosshairMode, @@ -24,6 +23,7 @@ import { GlobalParams, globalParamInit, LegendItem } from "./global-params"; import { Legend } from "./legend"; import { ToolBox } from "./toolbox"; import { TopBar } from "./topbar"; +import { ohlcSeries, ohlcSeriesOptions, ohlcdefaultOptions } from "../ohlc-series/ohlc-series"; //import { ProbabilityConeOverlay, ProbabilityConeOptions } from "../probability-cone/probability-cone"; export interface Scale{ @@ -313,7 +313,60 @@ export class Handler { return { name, series: bar }; } + createCustomOHLCSeries( + name: string, + options: Partial = {} + ): { name: string; series: ISeriesApi } { + const seriesType = 'ohlc'; + + const mergedOptions: ohlcSeriesOptions & { + seriesType?: string; + group?: string; + legendSymbol?: string[]; + } = { + ...ohlcdefaultOptions, + ...options, + seriesType, + }; + + const { + group, + legendSymbol = ['⑃', '⑂'], + seriesType: _, + chandelierSize = 1, + ...filteredOptions + } = mergedOptions; + + const Instance = new ohlcSeries(); + const ohlcCustomSeries = this.chart.addCustomSeries(Instance, { + ...filteredOptions, + chandelierSize, + }); + this._seriesList.push(ohlcCustomSeries); + this.seriesMap.set(name, ohlcCustomSeries); + + const borderUpColor = mergedOptions.borderUpColor || mergedOptions.upColor; + const borderDownColor = mergedOptions.borderDownColor || mergedOptions.downColor; + + const colorsArray = [borderUpColor, borderDownColor]; + const legendSymbolsWithGrouping = chandelierSize > 1 + ? legendSymbol.map(symbol => `${symbol} (${chandelierSize})`) + : legendSymbol; + + const legendItem: LegendItem = { + name, + series: ohlcCustomSeries, + colors: colorsArray, + legendSymbol: legendSymbolsWithGrouping, + seriesType, + group, + }; + + this.legend.addLegendItem(legendItem); + + return { name, series: ohlcCustomSeries }; + } removeSeries(seriesName: string): void { const series = this.seriesMap.get(seriesName); if (series) { @@ -329,10 +382,7 @@ export class Handler { // Remove from legend this.legend.deleteLegendEntry(seriesName); - // Remove any associated primitives - ['Tooltip', 'DeltaTooltip', 'probabilityCone'].forEach(primitiveType => { - this.detachPrimitive(seriesName, primitiveType as any); - }); + console.log(`Series "${seriesName}" removed.`); } else { diff --git a/src/general/legend.ts b/src/general/legend.ts index fc23d1a..683c2ea 100644 --- a/src/general/legend.ts +++ b/src/general/legend.ts @@ -1,5 +1,5 @@ import {AreaData, BarData, HistogramData, ISeriesApi, LineData, Logical, MouseEventParams, PriceFormatBuiltIn, SeriesType } from "lightweight-charts"; -import { CustomCandleSeriesData } from "../custom-candle-series/data"; +import { ohlcSeriesData } from "../ohlc-series/ohlc-series"; import { Handler } from "./handler"; import { LegendItem } from "./global-params"; type LegendEntry = LegendSeries | LegendGroup; @@ -128,7 +128,7 @@ export class Legend { row.style.alignItems = 'center'; const div = document.createElement('div'); - const displayOCvalues = ['Bar', 'customCandle', 'htfCandle'].includes(line.seriesType || ''); + const displayOCvalues = ['Bar', 'ohlc'].includes(line.seriesType || ''); if (displayOCvalues) { const openPrice = '-'; @@ -193,6 +193,8 @@ export class Legend { if (group) { // Add the item to the existing group group.seriesList.push(item); + this._items.push(item as LegendSeries); + // Update the group's div content entry = group; return group.row; @@ -206,6 +208,8 @@ export class Legend { // Add as an individual series const seriesRow = this.makeSeriesRow(item); entry = this._lines[this._lines.length - 1]; // Get the newly added series + this._items.push(item as LegendSeries); + return seriesRow; } @@ -231,7 +235,7 @@ export class Legend { // Also remove from _items array this._items = this._items.filter(entry => entry !== legendGroup); - console.log(`Group "${groupName}" removed.`); + //console.log(`Group "${groupName}" removed.`); } else { console.warn(`Legend group with name "${groupName}" not found.`); } @@ -335,7 +339,7 @@ export class Legend { items.forEach(item => { - const displayOCvalues = item.seriesType === 'Bar' || item.seriesType === 'customCandle' || item.seriesType === 'htfCandle'; + const displayOCvalues = item.seriesType === 'Bar' || item.seriesType === 'ohlc'; if (displayOCvalues) { const [upSymbol, downSymbol] = item.legendSymbol; @@ -452,13 +456,13 @@ export class Legend { const priceFormat = seriesItem.series.options().priceFormat as PriceFormatBuiltIn; // Check if the series type supports OHLC values - const isOHLC = seriesType === 'Bar' || seriesType === 'customCandle' || seriesType === 'htfCandle'; + const isOHLC = seriesType === 'Bar' || seriesType === 'ohlc' || seriesType === 'htfCandle'; if (isOHLC) { // Ensure properties are available or skip const { open, close, high, low } = data; if (open == null || close == null || high == null || low == null) return; - //const { open, close } = data as CustomCandleSeriesData || {}; + //const { open, close } = data as ohlcSeriesData || {}; if (open == null || close == null) { legendText += `${name}: - `; return; @@ -522,10 +526,10 @@ export class Legend { const seriesType = e.seriesType || 'Line'; // Check if the series type supports OHLC values - const isOHLC = ['Bar', 'customCandle', 'htfCandle'].includes(seriesType); + const isOHLC = ['Bar', 'ohlc', 'htfCandle'].includes(seriesType); if (isOHLC) { - const { open, close } = data as CustomCandleSeriesData; + const { open, close } = data as ohlcSeriesData; if (open == null || close == null) { e.div.innerHTML = `${e.name}: -`; return; diff --git a/src/index.ts b/src/index.ts index 68fbe3f..57f1cf5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,4 +3,5 @@ export * from './horizontal-line/horizontal-line'; export * from './vertical-line/vertical-line'; export * from './box/box'; export * from './trend-line/trend-line'; -export * from './vertical-line/vertical-line'; \ No newline at end of file +export * from './vertical-line/vertical-line'; +export * from './ohlc-series/ohlc-series'; \ No newline at end of file diff --git a/src/ohlc-series/helpers.ts b/src/ohlc-series/helpers.ts new file mode 100644 index 0000000..edb051e --- /dev/null +++ b/src/ohlc-series/helpers.ts @@ -0,0 +1,52 @@ +// Converts a hex color to RGBA with specified opacity +export function hexToRGBA(hex: string, opacity: number): string { + hex = hex.replace(/^#/, ''); + if (!/^([0-9A-F]{3}){1,2}$/i.test(hex)) { + throw new Error("Invalid hex color format."); + } + + const getRGB = (hex: string) => { + return hex.length === 3 + ? [parseInt(hex[0] + hex[0], 16), parseInt(hex[1] + hex[1], 16), parseInt(hex[2] + hex[2], 16)] + : [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)]; + }; + + const [r, g, b] = getRGB(hex); + return `rgba(${r}, ${g}, ${b}, ${opacity})`; +} + +// Adjusts the opacity of a color (hex, rgb, or rgba) +export function setOpacity(color: string, newOpacity: number): string { + if (color.startsWith('#')) { + return hexToRGBA(color, newOpacity); + } else if (color.startsWith('rgba') || color.startsWith('rgb')) { + return color.replace(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/, + `rgba($1, $2, $3, ${newOpacity})`); + } else { + throw new Error("Unsupported color format. Use hex, rgb, or rgba."); + } +} + +// Darkens a color (hex or rgba) by a specified amount +export function darkenColor(color: string, amount: number = 0.2): string { + const hexToRgb = (hex: string) => { + hex = hex.replace(/^#/, ''); + return hex.length === 3 + ? [parseInt(hex[0] + hex[0], 16), parseInt(hex[1] + hex[1], 16), parseInt(hex[2] + hex[2], 16)] + : [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)]; + }; + + const rgbaToArray = (rgba: string) => rgba.match(/\d+(\.\d+)?/g)!.map(Number); + + let [r, g, b, a = 1] = color.startsWith('#') + ? [...hexToRgb(color), 1] + : rgbaToArray(color); + + r = Math.max(0, Math.min(255, r * (1 - amount))); + g = Math.max(0, Math.min(255, g * (1 - amount))); + b = Math.max(0, Math.min(255, b * (1 - amount))); + + return color.startsWith('#') + ? `#${((1 << 24) + (Math.round(r) << 16) + (Math.round(g) << 8) + Math.round(b)).toString(16).slice(1)}` + : `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${a})`; +} diff --git a/src/ohlc-series/ohlc-series.ts b/src/ohlc-series/ohlc-series.ts new file mode 100644 index 0000000..26c20eb --- /dev/null +++ b/src/ohlc-series/ohlc-series.ts @@ -0,0 +1,110 @@ +import { + CustomSeriesOptions, + CustomSeriesPricePlotValues, + ICustomSeriesPaneView, + PaneRendererCustomData, + customSeriesDefaultOptions, + CandlestickSeriesOptions, + WhitespaceData, + Time, + LineStyle, + LineWidth, + CandlestickData +} from 'lightweight-charts'; +import { ohlcSeriesRenderer} from './renderer'; +import { + +} from 'lightweight-charts'; + +export interface ohlcSeriesOptions + extends CustomSeriesOptions, + Exclude< + CandlestickSeriesOptions, + 'borderColor' + > { + radius: (barSpacing: number) => number; + shape:'Rectangle'|'Rounded'|'Ellipse'|'Arrow'|'Polygon'|'3d'; + chandelierSize: number + barSpacing: number + lineStyle: LineStyle + lineWidth: LineWidth + } + //upperUpColor: string|undefined + //upperDownColor: string|undefined + //lowerUpColor: string|undefined + //lowerDownColor: string|undefined +export const ohlcdefaultOptions: ohlcSeriesOptions = { + ...customSeriesDefaultOptions, + upColor: '#26a69a', + downColor: '#ef5350', + wickVisible: true, + borderVisible: true, + borderColor: '#378658', + borderUpColor: '#26a69a', + borderDownColor: '#ef5350', + wickColor: '#737375', + wickUpColor: '#26a69a', + wickDownColor: '#ef5350', + radius: function (bs: number) { + if (bs < 4) return 0; + return bs / 3; + }, + shape: 'Rectangle', // Default shape + chandelierSize: 1, + barSpacing: 0.8, + lineStyle: 0 as LineStyle, + lineWidth: 1 as LineWidth + +} as const; + //upperUpColor: undefined, + //upperDownColor: undefined, + //lowerUpColor: undefined, + //lowerDownColor: undefined, +export class ohlcSeries + implements ICustomSeriesPaneView +{ + _renderer: ohlcSeriesRenderer; + + constructor() { + this._renderer = new ohlcSeriesRenderer(); + } + + priceValueBuilder(plotRow: TData): CustomSeriesPricePlotValues { + return [plotRow.high, plotRow.low, plotRow.close]; + } + + renderer(): ohlcSeriesRenderer { + return this._renderer; + } + + isWhitespace(data: TData | WhitespaceData): data is WhitespaceData { + return (data as Partial).close === undefined; + } + + update( + data: PaneRendererCustomData, + options: ohlcSeriesOptions + ): void { + this._renderer.update(data, options); + } + + defaultOptions() { + return ohlcdefaultOptions; + } +} + +export interface ohlcSeriesData extends CandlestickData { + time: Time; // The time of the candle, typically required by the chart + open: number; // Opening price + high: number; // Highest price + low: number; // Lowest price + close: number; // Closing price + + // Optional customization properties + color?: string; // Optional fill color for the candle body + borderColor?: string; // Optional color for the candle border + wickColor?: string; // Optional color for the candle wicks + shape?: string; // Optional shape (e.g., 'Rectangle', 'Rounded', 'Ellipse', 'Arrow', '3d', 'Polygon') + lineStyle?: number; // Optional line style (e.g., solid, dashed) + lineWidth?: number; // Optional line width for the border or wick +} diff --git a/src/ohlc-series/renderer.ts b/src/ohlc-series/renderer.ts new file mode 100644 index 0000000..4d9e840 --- /dev/null +++ b/src/ohlc-series/renderer.ts @@ -0,0 +1,703 @@ +import { + CanvasRenderingTarget2D, + BitmapCoordinatesRenderingScope, +} from 'fancy-canvas'; +import { + ICustomSeriesPaneRenderer, + PaneRendererCustomData, + PriceToCoordinateConverter, + Range, + Time, + +} from 'lightweight-charts'; +import { ohlcSeriesOptions, ohlcSeriesData } from './ohlc-series'; +import { setOpacity, darkenColor } from './helpers'; +import { setLineStyle } from '../helpers/canvas-rendering'; +interface BarItem { + open: number; + high: number; + low: number; + close: number; + x: number; + isUp: boolean; + startIndex: number; + endIndex: number; + isInProgress?: boolean; + color: string; + borderColor: string; + wickColor: string; + originalData?: { + open: number; + high: number; + low: number; + close: number; + }; +} + +import { gridAndCrosshairMediaWidth } from '../helpers/dimensions/crosshair-width'; + +export class ohlcSeriesRenderer + implements ICustomSeriesPaneRenderer { + _data: PaneRendererCustomData | null = null; + _options: ohlcSeriesOptions | null = null; + + draw( + target: CanvasRenderingTarget2D, + priceConverter: PriceToCoordinateConverter + ): void { + target.useBitmapCoordinateSpace(scope => + this._drawImpl(scope, priceConverter) + ); + } + + update( + data: PaneRendererCustomData, + options: ohlcSeriesOptions + ): void { + this._data = data; + this._options = options; + } + + private _seggregate(data: BarItem[], priceToCoordinate: (price: number) => number | null): BarItem[] { + const groupSize = this._options?.chandelierSize || 1; + const seggregatedBars: BarItem[] = []; + + for (let i = 0; i < data.length; i += groupSize) { + const bucket = data.slice(i, i + groupSize); + const isInProgress = bucket.length < groupSize && i + bucket.length === data.length; + + const aggregatedBar = this._chandelier(bucket, i, i + bucket.length - 1, priceToCoordinate, isInProgress); + seggregatedBars.push(aggregatedBar); + } + + return seggregatedBars; + } + private _chandelier( + bucket: BarItem[], + startIndex: number, + endIndex: number, + priceToCoordinate: (price: number) => number | null, + isInProgress = false + ): BarItem { + // Calculate the open and close prices with coordinate conversion + const openPrice = bucket[0].originalData?.open ?? bucket[0].open ?? 0; + const closePrice = bucket[bucket.length - 1].originalData?.close ?? bucket[bucket.length - 1].close ?? 0; + + // Convert to coordinates, with fallbacks to 0 for safe rendering + const open = priceToCoordinate(openPrice) ?? 0; + const close = priceToCoordinate(closePrice) ?? 0; + const highPrice = Math.max(...bucket.map(bar => bar.originalData?.high ?? bar.high)); + const lowPrice = Math.min(...bucket.map(bar => bar.originalData?.low ?? bar.low)); + const high = priceToCoordinate(highPrice) ?? 0; + const low = priceToCoordinate(lowPrice) ?? 0; + + // Center x position for HTF + const x = bucket[0].x + + // Determine if the candle is up or down + const isUp = closePrice > openPrice; + + // Explicitly map colors based on `isUp` status + const color = isUp + ? (this._options?.upColor || 'rgba(0,255,0,.333)') + : (this._options?.downColor || 'rgba(255,0,0,.333)'); + + const borderColor = isUp + ? (this._options?.borderUpColor || setOpacity(color, 1)) + : (this._options?.borderDownColor || setOpacity(color, 1)); + + const wickColor = isUp + ? (this._options?.wickUpColor || borderColor) + : (this._options?.wickDownColor || borderColor); + + + + + return { + open, + high, + low, + close, + x, + isUp, + startIndex, + endIndex, + isInProgress, + color, + borderColor, + wickColor, + + }; + } + + + + _drawImpl( + renderingScope: BitmapCoordinatesRenderingScope, + priceToCoordinate: PriceToCoordinateConverter + ): void { + if ( + this._data === null || + this._data.bars.length === 0 || + this._data.visibleRange === null || + this._options === null + ) { + return; + } + + let lastClose = -Infinity; + const bars: BarItem[] = this._data.bars.map((bar, index) => { + const isUp = bar.originalData.close >= bar.originalData.open; + lastClose = bar.originalData.close ?? lastClose; + + // Convert price to coordinate2 + const open = (bar.originalData.open as number) ?? 0; + const high = (bar.originalData.high as number) ?? 0; + const low = (bar.originalData.low as number) ?? 0; + const close = (bar.originalData.close as number) ?? 0; + + // Determine colors based on isUp status + const color = !isUp ? this._options?.upColor || 'rgba(0,0,0,0)' : this._options?.downColor || 'rgba(0,0,0,0)'; + const borderColor = !isUp ? this._options?.borderUpColor || color : this._options?.borderDownColor || color; + const wickColor = !isUp ? this._options?.wickUpColor || color : this._options?.wickDownColor || color; + + return { + open, + high, + low, + close, + x: bar.x, + isUp, + startIndex: index, + endIndex: index, + color, // Add color + borderColor, // Add border color + wickColor, // Add wick color + }; + }); + + // Continue with rendering logic + // ... + + + const seggregatedBars = this._seggregate(bars, priceToCoordinate) + + + const radius = this._options.radius(this._data.barSpacing); + const { horizontalPixelRatio, verticalPixelRatio } = renderingScope; + const candleWidth = this._data!.barSpacing * horizontalPixelRatio ; // Adjusted width + + this._drawCandles(renderingScope, seggregatedBars, this._data.visibleRange, radius, candleWidth, horizontalPixelRatio, verticalPixelRatio); + this._drawWicks(renderingScope,seggregatedBars,this._data.visibleRange) + } + private _drawWicks( + renderingScope: BitmapCoordinatesRenderingScope, + bars: readonly BarItem[], + visibleRange: Range, + + ): void { + if (this._data === null || this._options === null || !this._options?.wickVisible) { + return; + } + + // Skip wick drawing if the shape is '3d' + if (this._options.shape === '3d') { + return; + } + + const { context: ctx, horizontalPixelRatio, verticalPixelRatio } = renderingScope; + const candleWidth = this._data.barSpacing * horizontalPixelRatio; + const wickWidth = gridAndCrosshairMediaWidth(horizontalPixelRatio); + + for (const bar of bars) { + // Check if the bar is within the visible range + if (bar.startIndex < visibleRange.from || bar.endIndex > visibleRange.to) { + continue; + } + + + // Set wick color from bar's wickColor property + ctx.fillStyle = bar.wickColor; + + // Calculate positions in pixels for high, low, open, and close + const low = bar.low * verticalPixelRatio; + const high = bar.high * verticalPixelRatio; + const openCloseTop = Math.min(bar.open, bar.close) * verticalPixelRatio; + const openCloseBottom = Math.max(bar.open, bar.close) * verticalPixelRatio; + const barSpace = this._options?.barSpacing ?? 0.8 + + // Set wick X position + let wickX = bar.x * horizontalPixelRatio; + const groupSize = bar.endIndex - bar.startIndex; + if (groupSize && groupSize > 1) { + wickX= wickX+((candleWidth) * Math.max(1, groupSize )/2) - ((1-barSpace) * candleWidth) + } + + // Adjust wick heights for the 'Polygon' shape + let upperWickTop = high; + let upperWickBottom = openCloseTop; + let lowerWickTop = openCloseBottom; + let lowerWickBottom = low; + ctx.save(); + ctx.lineWidth= this._options?.lineWidth??1 + if (this._options.shape === 'Polygon') { + // Set halfway points for 'Polygon' shape + upperWickBottom = (high + openCloseTop) / 2; + lowerWickTop = (low + openCloseBottom) / 2; + } + + // Draw the upper wick (from high to halfway point for 'Polygon') + const upperWickHeight = upperWickBottom - upperWickTop; + if (upperWickHeight > 0) { + ctx.strokeRect( + wickX - Math.floor(wickWidth / 2), + upperWickTop, + wickWidth, + upperWickHeight + ); + } + + // Draw the lower wick (from halfway point for 'Polygon' to low) + const lowerWickHeight = lowerWickBottom - lowerWickTop; + if (lowerWickHeight > 0) { + ctx.strokeRect( + wickX - Math.floor(wickWidth / 2), + lowerWickTop, + wickWidth, + lowerWickHeight + ); + } + ctx.restore(); + + } + } + + + private _drawCandles( + renderingScope: BitmapCoordinatesRenderingScope, + bars: readonly BarItem[], + visibleRange: Range, + radius: number, + candleWidth: number, + horizontalPixelRatio: number, + verticalPixelRatio: number + ): void { + const { context: ctx } = renderingScope; + const barSpace = this._options?.barSpacing ?? 0.8 + for (const bar of bars) { + const groupSize = bar.endIndex - bar.startIndex; + let barHorizontalSpan = this._options?.chandelierSize !== 1 + ? (candleWidth) * (Math.max(1, groupSize + 1)) - ((1-barSpace) * candleWidth) + : (candleWidth * barSpace); + const barHorizontalPos = bar.x * horizontalPixelRatio; + const candleBodyWidth = candleWidth * barSpace; + + if (bar.startIndex < visibleRange.from || bar.endIndex > visibleRange.to) { + continue; + } + // Calculate vertical positions + const barVerticalMax = Math.min(bar.open, bar.close) * verticalPixelRatio; + const barVerticalMin = Math.max(bar.open, bar.close) * verticalPixelRatio; + const barVerticalSpan = barVerticalMax - barVerticalMin; + const barY= (barVerticalMax+ barVerticalMin)/2 + ctx.save(); + + // Set fill and stroke styles from bar properties + ctx.fillStyle = bar.color; + ctx.strokeStyle = bar.borderColor; + ctx.lineWidth = 1.5; + setLineStyle(ctx,this._options?.lineStyle??1 ) + ctx.lineWidth= this._options?.lineWidth??1 + + // Draw based on shape type + switch (this._options?.shape) { + case 'Rectangle': + this._drawCandle(ctx, barHorizontalPos, barY, candleBodyWidth, barHorizontalSpan, barVerticalSpan); + break; + case 'Rounded': + this._drawRounded(ctx, barHorizontalPos, barVerticalMin, candleBodyWidth, barHorizontalSpan, barVerticalSpan, radius, horizontalPixelRatio); + break; + case 'Ellipse': + this._drawEllipse(ctx, barHorizontalPos, barY, candleBodyWidth, barHorizontalSpan, barVerticalSpan); + break; + case 'Arrow': + this._drawArrow(ctx, barHorizontalPos, barVerticalMax, barVerticalMin, candleBodyWidth, barHorizontalSpan, bar.high * verticalPixelRatio, bar.low * verticalPixelRatio, bar.isUp); + break; + case '3d': + this._draw3d(ctx, barHorizontalPos, bar.high * verticalPixelRatio, bar.low * verticalPixelRatio, bar.open * verticalPixelRatio, bar.close * verticalPixelRatio, candleBodyWidth, barHorizontalSpan, bar.color, bar.borderColor, bar.isUp, barSpace); + break; + case 'Polygon': + this._drawPolygon(ctx, barHorizontalPos, barVerticalMin + barVerticalSpan, barVerticalMin, candleBodyWidth, barHorizontalSpan, bar.high * verticalPixelRatio, bar.low * verticalPixelRatio, bar.isUp); + break; + + default: + // Optional: fallback for unknown shapes + this._drawCandle(ctx, barHorizontalPos, barY, candleBodyWidth, barHorizontalSpan, barVerticalSpan); + break; + } + + // Restore the state + ctx.restore(); + } + } + + private _drawCandle( + ctx: CanvasRenderingContext2D, + xCenter: number, + yCenter: number, + candleWidth: number, + combinedWidth: number, + candleHeight: number + ): void { + // Calculate the left and right edges of the candle based on xCenter and combined width + const leftEdge = xCenter - candleWidth / 2; + const rightEdge = xCenter - (candleWidth/2) + combinedWidth; + const topEdge = yCenter - candleHeight / 2; + const bottomEdge = yCenter + candleHeight / 2; + + + // Begin drawing the candle rectangle + ctx.beginPath(); + ctx.moveTo(leftEdge, topEdge); + ctx.lineTo(leftEdge, bottomEdge); + ctx.lineTo(rightEdge, bottomEdge); + ctx.lineTo(rightEdge, topEdge); + ctx.closePath(); + + // Fill and stroke the rectangle + ctx.fill(); + ctx.stroke(); + } + + //private _drawXShape(ctx: CanvasRenderingContext2D, xCenter: number, openCloseTop: number, openCloseBottom: number, candleWidth: number, combinedWidth: number, candleHeight: number): void { + // const controlOffsetX = candleWidth / 3; + // const controlOffsetY = candleHeight / 3; + // + // ctx.beginPath(); + // ctx.moveTo(xCenter - candleWidth / 2, openCloseTop); + // ctx.bezierCurveTo(xCenter - controlOffsetX, openCloseTop + controlOffsetY, xCenter + controlOffsetX, openCloseTop + controlOffsetY, xCenter + combinedWidth / 2, openCloseTop); + // ctx.bezierCurveTo(xCenter + combinedWidth / 2 - controlOffsetX, (openCloseTop + openCloseBottom) / 2, xCenter + combinedWidth / 2 - controlOffsetX, (openCloseTop + openCloseBottom) / 2, xCenter + combinedWidth / 2, openCloseBottom); + // ctx.bezierCurveTo(xCenter + controlOffsetX, openCloseBottom - controlOffsetY, xCenter - controlOffsetX, openCloseBottom - controlOffsetY, xCenter - combinedWidth / 2, openCloseBottom); + // ctx.bezierCurveTo(xCenter - candleWidth / 2 + controlOffsetX, (openCloseTop + openCloseBottom) / 2, xCenter - combinedWidth / 2 + controlOffsetX, (openCloseTop + openCloseBottom) / 2, xCenter - combinedWidth / 2, openCloseTop); + // ctx.closePath(); + // ctx.stroke(); + // ctx.fill(); + //} + private _drawEllipse( + ctx: CanvasRenderingContext2D, + xCenter: number, + yCenter: number, + candleWidth: number, + combinedWidth: number, + candleHeight: number, + ): void { + // Calculate x and y radii based on the group size and bar spacing + const xRadius = combinedWidth/2 + const yRadius = candleHeight / 2; + + // Shift xCenter to the right by half the total group width + one candleWidth for HTF candles + const adjustedXCenter = xCenter-(candleWidth/2) + (combinedWidth/2) + + ctx.beginPath(); + ctx.ellipse( + adjustedXCenter, // Shifted center only for HTF candles + yCenter, + Math.abs(xRadius), + Math.abs(yRadius), + 0, + 0, + Math.PI * 2 + ); + ctx.fill(); + ctx.stroke(); + } + + + + + private _drawRounded(ctx: CanvasRenderingContext2D, xCenter: number, openCloseTop: number, candleWidth: number, combinedWidth: number, candleHeight: number, radius: number,horizontalPixelRatio:number): void { + if (ctx.roundRect) { + const effectiveRadius = Math.abs(Math.min(radius, 0.1 * Math.min(candleWidth, candleHeight), 5))*horizontalPixelRatio; + ctx.beginPath(); + ctx.roundRect(xCenter - candleWidth / 2, openCloseTop, combinedWidth, candleHeight, effectiveRadius); + ctx.stroke(); + ctx.fill(); + } else { + ctx.strokeRect(xCenter - candleWidth / 2, openCloseTop, combinedWidth, candleHeight); + ctx.fillRect(xCenter - candleWidth / 2, openCloseTop, combinedWidth, candleHeight); + } + } + + private _draw3d( + ctx: CanvasRenderingContext2D, + xCenter: number, + high: number, + low: number, + open: number, + close: number, + candleWidth: number, + combinedWidth: number, + fillColor: string, + borderColor: string, + isUp: boolean, + barSpacing:number + ): void { + const xOffset = -Math.max(combinedWidth,1) * (1-barSpacing) ; + const insideColor = darkenColor(fillColor, 0.666); // Darker side color + const sideColor = darkenColor(fillColor,0.333) + const topColor = darkenColor(fillColor, 0.2); // Slightly lighter top face + + // Calculate front face X coordinates using candleWidth + const frontLeftX = xCenter - candleWidth/2 ; + const frontRightX = (xCenter-candleWidth/2) + (combinedWidth)+xOffset; + + // Calculate back face X coordinates with combined width for depth effect + const backLeftX = frontLeftX - xOffset; + const backRightX = frontRightX - xOffset; + + // Set Y coordinates for front and back faces based on candle direction + let frontTop, frontBottom, backTop, backBottom; + + if (!isUp) { + // Up candle: front face uses open/high, back face uses low/close + frontTop = open; + frontBottom = high; + backTop = low; + backBottom = close; + } else { + // Down candle: front face uses open/low, back face uses high/close + frontTop = open; + frontBottom = low; + backTop = high; + backBottom = close; + } + + // Draw back (shadow) rectangle + ctx.fillStyle = sideColor; + ctx.strokeStyle = borderColor; + //ctx.beginPath(); + //ctx.rect(backLeftX, backTop, (combinedWidth)+xOffset-(candleWidth/2), backBottom - backTop); + //ctx.fill(); + //ctx.stroke(); + + // Draw top face between front and back + ctx.fillStyle = topColor; + + + + if (isUp) { + // Draw bottom face first for up candles + ctx.fillStyle = insideColor; + ctx.beginPath(); + ctx.moveTo(frontLeftX, frontBottom); // Bottom-left corner at the front + ctx.lineTo(backLeftX, backBottom); // Bottom-left corner at the back + ctx.lineTo(backRightX, backBottom); // Bottom-right corner at the back + ctx.lineTo(frontRightX, frontBottom); // Bottom-right corner at the front + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Draw left side face for up candles + ctx.fillStyle = insideColor; + ctx.beginPath(); + ctx.moveTo(frontLeftX, frontTop); // Top-left corner at the front + ctx.lineTo(backLeftX, backTop); // Top-left corner at the back + ctx.lineTo(backLeftX, backBottom); // Bottom-left corner at the back + ctx.lineTo(frontLeftX, frontBottom); // Bottom-left corner at the front + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Draw right side face for up candles + ctx.fillStyle = insideColor; + ctx.beginPath(); + ctx.moveTo(frontRightX, frontTop); // Top-right corner at the front + ctx.lineTo(backRightX, backTop); // Top-right corner at the back + ctx.lineTo(backRightX, backBottom); // Bottom-right corner at the back + ctx.lineTo(frontRightX, frontBottom); // Bottom-right corner at the front + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Draw top face last for up candles + ctx.fillStyle = topColor; + ctx.beginPath(); + ctx.moveTo(frontLeftX, frontTop); // Top-left corner at the front + ctx.lineTo(backLeftX, backTop); // Top-left corner at the back + ctx.lineTo(backRightX, backTop); // Top-right corner at the back + ctx.lineTo(frontRightX, frontTop); // Top-right corner at the front + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } else { + // Draw top face first for down candles + ctx.fillStyle = topColor; + ctx.beginPath(); + ctx.moveTo(frontLeftX, frontTop); // Top-left corner at the front + ctx.lineTo(backLeftX, backTop); // Top-left corner at the back + ctx.lineTo(backRightX, backTop); // Top-right corner at the back + ctx.lineTo(frontRightX, frontTop); // Top-right corner at the front + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Draw right side face for down candles + ctx.fillStyle = sideColor; + ctx.beginPath(); + ctx.moveTo(frontRightX, frontTop); // Top-right corner at the front + ctx.lineTo(backRightX, backTop); // Top-right corner at the back + ctx.lineTo(backRightX, backBottom); // Bottom-right corner at the back + ctx.lineTo(frontRightX, frontBottom); // Bottom-right corner at the front + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Draw left side face for down candles + ctx.fillStyle = sideColor; + ctx.beginPath(); + ctx.moveTo(frontLeftX, frontTop); // Top-left corner at the front + ctx.lineTo(backLeftX, backTop); // Top-left corner at the back + ctx.lineTo(backLeftX, backBottom); // Bottom-left corner at the back + ctx.lineTo(frontLeftX, frontBottom); // Bottom-left corner at the front + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Draw bottom face last for down candles + ctx.fillStyle = sideColor; + ctx.beginPath(); + ctx.moveTo(frontLeftX, frontBottom); // Bottom-left corner at the front + ctx.lineTo(backLeftX, backBottom); // Bottom-left corner at the back + ctx.lineTo(backRightX, backBottom); // Bottom-right corner at the back + ctx.lineTo(frontRightX, frontBottom); // Bottom-right corner at the front + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } + } + + + + + + private _drawPolygon( + ctx: CanvasRenderingContext2D, + xCenter: number, + openCloseTop: number, + openCloseBottom: number, + candleWidth: number, + combinedWidth: number, + high: number, + low: number, + isUp: boolean, + //topColor?: string, + //bottomColor?: string, + ): void + { + + ctx.beginPath(); + if (isUp) { + ctx.moveTo(xCenter - candleWidth / 2, openCloseTop); + ctx.lineTo(xCenter + combinedWidth - candleWidth/2, high); + ctx.lineTo(xCenter + combinedWidth - candleWidth/2, openCloseBottom); + ctx.lineTo(xCenter - candleWidth / 2, low); + } else { + ctx.moveTo(xCenter - candleWidth / 2, high); + ctx.lineTo(xCenter + combinedWidth - candleWidth/2, openCloseTop); + ctx.lineTo(xCenter + combinedWidth - candleWidth/2, low); + ctx.lineTo(xCenter - candleWidth / 2, openCloseBottom); + } + + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + //// Draw the top overlay if topColor is provided + //if (topColor) { + // ctx.lineWidth = ctx.lineWidth*1.1 + // ctx.strokeStyle = setOpacity(topColor, 1); // Fully opaque border + // ctx.fillStyle = topColor; // Semi-transparent fill + // ctx.beginPath(); + // if (isUp) { + // // For up candles, bottom is between openCloseBottom and low + // ctx.moveTo(leftSide, openCloseBottom); + // ctx.lineTo(rightSide, openCloseBottom); + // ctx.lineTo(leftSide, low); + // ctx.lineTo(leftSide, openCloseBottom); +// + // } else { + // // For down candles, bottom is between openCloseBottom and low + // ctx.moveTo(leftSide, openCloseBottom); + // ctx.lineTo(rightSide, openCloseBottom); + // ctx.lineTo(rightSide, low); + // ctx.lineTo(leftSide, openCloseBottom); +// + // } + // + // ctx.closePath(); + // ctx.fill(); + // ctx.stroke(); + //} +// + //// Draw the bottom overlay if bottomColor is provided + //if (bottomColor) { + // ctx.lineWidth = ctx.lineWidth*1.1 +// + // ctx.strokeStyle = setOpacity(bottomColor, 1); // Fully opaque border + // ctx.fillStyle = bottomColor; // Semi-transparent fill + // ctx.beginPath(); + // if (isUp) { + // // For up candles, top is between openCloseTop and high + // ctx.moveTo(leftSide, openCloseTop); + // ctx.lineTo(rightSide, high); + // ctx.lineTo(rightSide, openCloseTop); + // ctx.lineTo(leftSide, openCloseTop); +// + // } else { + // // For down candles, top is between high and openCloseTop + // ctx.moveTo(leftSide, high); + // ctx.lineTo(rightSide, openCloseTop); + // ctx.lineTo(leftSide, openCloseTop); + // ctx.lineTo(leftSide, high); +// + // } + // ctx.closePath(); + // ctx.fill(); + // ctx.stroke(); + // + } + + + + private _drawArrow(ctx: CanvasRenderingContext2D, xCenter: number, + openCloseTopY: number, openCloseBottomY: number, candleBodyWidth: number,combinedBodyWidth:number, + highY: number, lowY: number, isUp: boolean): void { + ctx.beginPath(); + + const left = xCenter - candleBodyWidth / 2 + const right = left + combinedBodyWidth + const middle = left + combinedBodyWidth/2 + if (isUp) { + ctx.moveTo(left, lowY); + ctx.lineTo(left, openCloseTopY); + ctx.lineTo(middle, highY); + ctx.lineTo(right, openCloseTopY); + ctx.lineTo(right, lowY); + ctx.lineTo(middle, openCloseBottomY); + ctx.lineTo(left, lowY); + + } else { + ctx.moveTo(left, highY); + ctx.lineTo(left, openCloseBottomY); + ctx.lineTo(middle, lowY); + ctx.lineTo(right, openCloseBottomY); + ctx.lineTo(right, highY); + ctx.lineTo(middle, openCloseTopY); + ctx.lineTo(left, highY); + + } + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + } + } + From 148ce6444fa1ba4fd2ac6aabd7aa83603e1b3f07 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Sun, 17 Nov 2024 01:00:00 -0800 Subject: [PATCH 20/89] Update renderer.ts Fix wick position for combined candles --- src/ohlc-series/renderer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ohlc-series/renderer.ts b/src/ohlc-series/renderer.ts index 4d9e840..0e469fe 100644 --- a/src/ohlc-series/renderer.ts +++ b/src/ohlc-series/renderer.ts @@ -230,8 +230,9 @@ export class ohlcSeriesRenderer let wickX = bar.x * horizontalPixelRatio; const groupSize = bar.endIndex - bar.startIndex; if (groupSize && groupSize > 1) { - wickX= wickX+((candleWidth) * Math.max(1, groupSize )/2) - ((1-barSpace) * candleWidth) - } + wickX= wickX+((candleWidth) * Math.max(1, groupSize )/2) + } + // Adjust wick heights for the 'Polygon' shape let upperWickTop = high; From af3647f9385dafa591c60a0a7584f56066e79547 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Sun, 17 Nov 2024 01:19:00 -0800 Subject: [PATCH 21/89] add bar_width option For those who think the default candles are too thick. --- lightweight_charts/abstract.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 28fddbf..29556a2 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -678,7 +678,8 @@ def __init__( wick_down_color: str , wick_visible: bool = True, border_visible: bool= True, - radius: Optional[str] = 30, + bar_width: float = 0.8, + radius: Optional[str] = 100, shape: str = 'Rectangle', combineCandles: int = 1, line_width: int = 1, @@ -714,6 +715,7 @@ def __init__( wickDownColor: '{wick_down_color or border_down_color}', wickVisible: {jbool(wick_visible)}, borderVisible: {jbool(border_visible)}, + barSpacing: {bar_width}, radius: {radius_func}, shape: '{shape}', lastValueVisible: {jbool(price_label)}, @@ -1061,6 +1063,7 @@ def create_custom_candle( wick_down_color='rgba(255,0,0,1)', wick_visible: bool = True, border_visible: bool = True, + bar_width: float = 0.8, rounded_radius: Union[float, int] = 100, shape: Literal[CANDLE_SHAPE] = "Rectangle", combineCandles: int = 1, @@ -1092,6 +1095,7 @@ def create_custom_candle( wick_down_color=wick_down_color or border_down_color or border_down_color, wick_visible=wick_visible, border_visible=border_visible, + bar_width=bar_width, radius=rounded_radius, shape=shape, combineCandles=combineCandles, From ec4fa2dc6519ea095a53fe07bdf73127c9a0da24 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:54:17 -0800 Subject: [PATCH 22/89] Enhance bar specific rendering logic Enhance flexibility by enabling the configuration of lineStyle, lineWidth, and the shape of individual bars through corresponding columns in the DataFrame, similar to the existing options for color, borderColor, and wickColor. Also improve performance by separating data aggregation from rendering. --- src/ohlc-series/renderer.ts | 1108 ++++++++++++++++++++++------------- 1 file changed, 706 insertions(+), 402 deletions(-) diff --git a/src/ohlc-series/renderer.ts b/src/ohlc-series/renderer.ts index 0e469fe..47fbd00 100644 --- a/src/ohlc-series/renderer.ts +++ b/src/ohlc-series/renderer.ts @@ -1,118 +1,215 @@ +// ------------------------------------- +// Imports +// ------------------------------------- + import { CanvasRenderingTarget2D, BitmapCoordinatesRenderingScope, } from 'fancy-canvas'; + import { ICustomSeriesPaneRenderer, PaneRendererCustomData, - PriceToCoordinateConverter, Range, Time, - + PriceToCoordinateConverter, } from 'lightweight-charts'; -import { ohlcSeriesOptions, ohlcSeriesData } from './ohlc-series'; + +import { + ohlcSeriesOptions, + +} from './ohlc-series'; + +import { ohlcSeriesData, BarItem, AggregatorOptions, CandleShape, parseCandleShape } from './data'; + import { setOpacity, darkenColor } from './helpers'; import { setLineStyle } from '../helpers/canvas-rendering'; -interface BarItem { - open: number; - high: number; - low: number; - close: number; - x: number; - isUp: boolean; - startIndex: number; - endIndex: number; - isInProgress?: boolean; - color: string; - borderColor: string; - wickColor: string; - originalData?: { - open: number; - high: number; - low: number; - close: number; - }; -} - import { gridAndCrosshairMediaWidth } from '../helpers/dimensions/crosshair-width'; -export class ohlcSeriesRenderer - implements ICustomSeriesPaneRenderer { - _data: PaneRendererCustomData | null = null; - _options: ohlcSeriesOptions | null = null; +// ------------------------------------- +// Constants +// ------------------------------------- - draw( - target: CanvasRenderingTarget2D, - priceConverter: PriceToCoordinateConverter - ): void { - target.useBitmapCoordinateSpace(scope => - this._drawImpl(scope, priceConverter) - ); - } +/** + * Default color for upward-moving candles. + * Format: RGBA with 33.3% opacity. + */ - update( - data: PaneRendererCustomData, - options: ohlcSeriesOptions - ): void { - this._data = data; +/** + * Default color for downward-moving candles. + * Format: RGBA with 33.3% opacity. + */ + +/** + * Default line style for candle borders. + * 1 represents a solid line. + */ +const DEFAULT_LINE_STYLE = 1; + +/** + * Default line width for candle borders. + * 1 pixel. + */ +const DEFAULT_LINE_WIDTH = 1; + +// ------------------------------------- +// BarDataAggregator Class +// ------------------------------------- + +/** + * Aggregates raw bar data into grouped bar items based on specified options. + * Handles the styling and property consolidation for candle rendering. + */ +export class BarDataAggregator { + /** + * Configuration options for data aggregation and candle styling. + */ + private _options: AggregatorOptions | null; + + /** + * Constructs a new BarDataAggregator instance. + * @param options - Aggregation and styling options. Can be null to use defaults. + */ + constructor(options: AggregatorOptions | null) { this._options = options; } - private _seggregate(data: BarItem[], priceToCoordinate: (price: number) => number | null): BarItem[] { - const groupSize = this._options?.chandelierSize || 1; - const seggregatedBars: BarItem[] = []; - + /** + * Aggregates an array of BarItem objects into grouped BarItem objects. + * @param data - The raw bar data to aggregate. + * @param priceToCoordinate - Function to convert price values to canvas coordinates. + * @returns An array of aggregated BarItem objects. + */ + public aggregate( + data: BarItem[], + priceToCoordinate: PriceToCoordinateConverter + ): BarItem[] { + // Determine the number of bars to group based on chandelierSize. + const groupSize = this._options?.chandelierSize ?? 1; + const aggregatedBars: BarItem[] = []; + + // Iterate over the data in increments of groupSize to create buckets. for (let i = 0; i < data.length; i += groupSize) { const bucket = data.slice(i, i + groupSize); - const isInProgress = bucket.length < groupSize && i + bucket.length === data.length; - - const aggregatedBar = this._chandelier(bucket, i, i + bucket.length - 1, priceToCoordinate, isInProgress); - seggregatedBars.push(aggregatedBar); + const isInProgress = + bucket.length < groupSize && i + bucket.length === data.length; + + // Warn and skip if an empty bucket is encountered. + if (bucket.length === 0) { + console.warn('Empty bucket encountered during aggregation.'); + continue; + } + + // Aggregate the current bucket into a single BarItem. + const aggregatedBar = this._chandelier( + bucket, + i, + i + bucket.length - 1, + priceToCoordinate, + isInProgress + ); + aggregatedBars.push(aggregatedBar); } - - return seggregatedBars; + + return aggregatedBars; } + + /** + * Aggregates a single bucket of BarItem objects into one consolidated BarItem. + * @param bucket - The group of BarItem objects to aggregate. + * @param startIndex - The starting index of the bucket in the original data array. + * @param endIndex - The ending index of the bucket in the original data array. + * @param priceToCoordinate - Function to convert price values to canvas coordinates. + * @param isInProgress - Indicates if the aggregation is currently in progress. + * @returns A single aggregated BarItem. + * @throws Will throw an error if the bucket is empty. + */ private _chandelier( bucket: BarItem[], startIndex: number, endIndex: number, - priceToCoordinate: (price: number) => number | null, + priceToCoordinate: PriceToCoordinateConverter, isInProgress = false ): BarItem { - // Calculate the open and close prices with coordinate conversion + if (bucket.length === 0) { + throw new Error('Bucket cannot be empty in _chandelier method.'); + } + + // Extract open and close prices from the first and last bars in the bucket. const openPrice = bucket[0].originalData?.open ?? bucket[0].open ?? 0; - const closePrice = bucket[bucket.length - 1].originalData?.close ?? bucket[bucket.length - 1].close ?? 0; - - // Convert to coordinates, with fallbacks to 0 for safe rendering + const closePrice = + bucket[bucket.length - 1].originalData?.close ?? + bucket[bucket.length - 1].close ?? + 0; + + // Convert open and close prices to canvas coordinates. const open = priceToCoordinate(openPrice) ?? 0; const close = priceToCoordinate(closePrice) ?? 0; - const highPrice = Math.max(...bucket.map(bar => bar.originalData?.high ?? bar.high)); - const lowPrice = Math.min(...bucket.map(bar => bar.originalData?.low ?? bar.low)); + + // Extract high and low prices from all bars in the bucket. + const highPrices = bucket.map( + (bar) => bar.originalData?.high ?? bar.high + ); + const lowPrices = bucket.map((bar) => bar.originalData?.low ?? bar.low); + + // Determine the highest and lowest prices in the bucket. + const highPrice = highPrices.length > 0 ? Math.max(...highPrices) : 0; + const lowPrice = lowPrices.length > 0 ? Math.min(...lowPrices) : 0; + + // Convert high and low prices to canvas coordinates. const high = priceToCoordinate(highPrice) ?? 0; const low = priceToCoordinate(lowPrice) ?? 0; - - // Center x position for HTF - const x = bucket[0].x - - // Determine if the candle is up or down + + // Position of the aggregated bar on the x-axis. + const x = bucket[0].x; + + // Determine if the aggregated bar represents an upward movement. const isUp = closePrice > openPrice; - - // Explicitly map colors based on `isUp` status + + // Explicitly map colors based on `isUp` status. const color = isUp - ? (this._options?.upColor || 'rgba(0,255,0,.333)') - : (this._options?.downColor || 'rgba(255,0,0,.333)'); - + ? (this._options?.upColor || 'rgba(0,255,0,0.333)') + : (this._options?.downColor || 'rgba(255,0,0,0.333)'); + const borderColor = isUp ? (this._options?.borderUpColor || setOpacity(color, 1)) : (this._options?.borderDownColor || setOpacity(color, 1)); - + const wickColor = isUp ? (this._options?.wickUpColor || borderColor) : (this._options?.wickDownColor || borderColor); - - - + + // Aggregate lineStyle similarly to other properties. + const lineStyle = bucket.reduce( + (style, bar) => bar.lineStyle ?? bar.originalData?.lineStyle ?? style, + this._options?.lineStyle ?? DEFAULT_LINE_STYLE + ); + + // Aggregate lineWidth similarly to other properties. + const lineWidth = bucket.reduce( + (currentWidth, bar) => + bar.lineWidth ?? bar.originalData?.lineWidth ?? currentWidth, + this._options?.lineWidth ?? DEFAULT_LINE_WIDTH + ); + // Aggregate shape similarly to other properties. + const shape = bucket.reduce( + (currentShape, bar) => { + const parsedShape = bar.shape + ? parseCandleShape(bar.shape) + : bar.originalData?.shape + ? parseCandleShape(bar.originalData.shape) + : undefined; + + // If parsing fails, retain the current shape. + return parsedShape ?? currentShape; + }, + this._options?.shape ?? CandleShape.Rectangle + ); + + // Ensure that `shape` is never undefined. If it is, default to Rectangle. + const finalShape = shape || CandleShape.Rectangle; + // Return the aggregated BarItem with all consolidated properties. return { open, high, @@ -126,154 +223,275 @@ export class ohlcSeriesRenderer color, borderColor, wickColor, - + shape: finalShape, + lineStyle, + lineWidth, }; } - - - - _drawImpl( +} + +// ------------------------------------- +// ohlcSeriesRenderer Class +// ------------------------------------- + +/** + * Custom renderer for candle series, implementing various candle shapes and styles. + * Utilizes BarDataAggregator for data aggregation and rendering logic for different candle shapes. + * @template TData - The type of custom candle series data. + */ +export class ohlcSeriesRenderer< + TData extends ohlcSeriesData +> implements ICustomSeriesPaneRenderer { + /** + * The current data to be rendered. + */ + private _data: PaneRendererCustomData | null = null; + + /** + * The current rendering options. + */ + private _options: ohlcSeriesOptions | null = null; + + /** + * The data aggregator instance. + */ + private _aggregator: BarDataAggregator | null = null; + + /** + * Draws the candle series onto the provided canvas target. + * @param target - The canvas rendering target. + * @param priceConverter - Function to convert price values to canvas coordinates. + */ + draw( + target: CanvasRenderingTarget2D, + priceConverter: PriceToCoordinateConverter + ): void { + target.useBitmapCoordinateSpace((scope) => + this._drawImpl(scope, priceConverter) + ); + } + + /** + * Updates the renderer with new data and options. + * @param data - The custom series data to render. + * @param options - The custom series options for styling and behavior. + */ + update( + data: PaneRendererCustomData, + options: ohlcSeriesOptions + ): void { + this._data = data; + this._options = options; + this._aggregator = new BarDataAggregator(options); + } + + /** + * Internal implementation of the drawing logic. + * Processes data, aggregates bars, and delegates drawing to specific methods. + * @param renderingScope - The rendering scope containing canvas context and scaling information. + * @param priceToCoordinate - Function to convert price values to canvas coordinates. + */ + private _drawImpl( renderingScope: BitmapCoordinatesRenderingScope, priceToCoordinate: PriceToCoordinateConverter ): void { + // Exit early if there's no data or options to render. if ( - this._data === null || + !this._data || this._data.bars.length === 0 || - this._data.visibleRange === null || - this._options === null + !this._data.visibleRange || + !this._options ) { return; } - - let lastClose = -Infinity; - const bars: BarItem[] = this._data.bars.map((bar, index) => { - const isUp = bar.originalData.close >= bar.originalData.open; - lastClose = bar.originalData.close ?? lastClose; - - // Convert price to coordinate2 - const open = (bar.originalData.open as number) ?? 0; - const high = (bar.originalData.high as number) ?? 0; - const low = (bar.originalData.low as number) ?? 0; - const close = (bar.originalData.close as number) ?? 0; - - // Determine colors based on isUp status - const color = !isUp ? this._options?.upColor || 'rgba(0,0,0,0)' : this._options?.downColor || 'rgba(0,0,0,0)'; - const borderColor = !isUp ? this._options?.borderUpColor || color : this._options?.borderDownColor || color; - const wickColor = !isUp ? this._options?.wickUpColor || color : this._options?.wickDownColor || color; - - return { - open, - high, - low, - close, - x: bar.x, - isUp, - startIndex: index, - endIndex: index, - color, // Add color - borderColor, // Add border color - wickColor, // Add wick color - }; - }); - - // Continue with rendering logic - // ... - - const seggregatedBars = this._seggregate(bars, priceToCoordinate) - + // Transform raw data into BarItem objects with initial styling. + const bars: BarItem[] = this._data.bars.map((bar, index) => ({ + open: bar.originalData?.open ?? 0, + high: bar.originalData?.high ?? 0, + low: bar.originalData?.low ?? 0, + close: bar.originalData?.close ?? 0, + x: bar.x, + shape: + (bar.originalData?.shape ?? + this._options?.shape ?? + 'Rectangle') as CandleShape, + lineStyle: + bar.originalData?.lineStyle ?? + this._options?.lineStyle ?? + 1, + lineWidth: + bar.originalData?.lineWidth ?? + this._options?.lineWidth ?? + 1, + isUp: + (bar.originalData?.close ?? 0) >= + (bar.originalData?.open ?? 0), + color: this._options?.color ?? 'rgba(0,0,0,0)', + borderColor: this._options?.borderColor ?? 'rgba(0,0,0,0)', + wickColor: this._options?.wickColor ?? 'rgba(0,0,0,0)', + startIndex: index, + endIndex: index, + })); + // Aggregate the bars using the BarDataAggregator. + const aggregatedBars = + this._aggregator?.aggregate(bars, priceToCoordinate) ?? []; + + // Determine the radius for rounded shapes and candle width based on scaling. const radius = this._options.radius(this._data.barSpacing); const { horizontalPixelRatio, verticalPixelRatio } = renderingScope; - const candleWidth = this._data!.barSpacing * horizontalPixelRatio ; // Adjusted width + const candleWidth = this._data.barSpacing * horizontalPixelRatio; - this._drawCandles(renderingScope, seggregatedBars, this._data.visibleRange, radius, candleWidth, horizontalPixelRatio, verticalPixelRatio); - this._drawWicks(renderingScope,seggregatedBars,this._data.visibleRange) + // Delegate drawing of candle bodies and wicks. + this._drawCandles( + renderingScope, + aggregatedBars, + this._data.visibleRange, + radius, + candleWidth, + horizontalPixelRatio, + verticalPixelRatio + ); + this._drawWicks( + renderingScope, + aggregatedBars, + this._data.visibleRange + ); } + + /** + * Draws the wicks (high-low lines) for each aggregated candle. + * Skips rendering if the candle shape is '3d'. + * @param renderingScope - The rendering scope containing canvas context and scaling information. + * @param bars - Array of aggregated BarItem objects to draw wicks for. + * @param visibleRange - The range of visible bars to render. + */ private _drawWicks( renderingScope: BitmapCoordinatesRenderingScope, bars: readonly BarItem[], - visibleRange: Range, - + visibleRange: Range ): void { - if (this._data === null || this._options === null || !this._options?.wickVisible) { + // Exit early if there's no data or options. + if (this._data === null || this._options === null) { return; } - - // Skip wick drawing if the shape is '3d' + + // Skip wick drawing if the candle shape is '3d'. if (this._options.shape === '3d') { return; } - - const { context: ctx, horizontalPixelRatio, verticalPixelRatio } = renderingScope; + + const { context: ctx, horizontalPixelRatio, verticalPixelRatio } = + renderingScope; const candleWidth = this._data.barSpacing * horizontalPixelRatio; const wickWidth = gridAndCrosshairMediaWidth(horizontalPixelRatio); - + + // Iterate over each aggregated bar to draw its wicks. for (const bar of bars) { - // Check if the bar is within the visible range - if (bar.startIndex < visibleRange.from || bar.endIndex > visibleRange.to) { + // Skip bars outside the visible range. + if ( + bar.startIndex < visibleRange.from || + bar.endIndex > visibleRange.to + ) { continue; } - - // Set wick color from bar's wickColor property - ctx.fillStyle = bar.wickColor; - - // Calculate positions in pixels for high, low, open, and close + // Calculate pixel positions for high, low, open, and close. const low = bar.low * verticalPixelRatio; const high = bar.high * verticalPixelRatio; const openCloseTop = Math.min(bar.open, bar.close) * verticalPixelRatio; - const openCloseBottom = Math.max(bar.open, bar.close) * verticalPixelRatio; - const barSpace = this._options?.barSpacing ?? 0.8 + const openCloseBottom = + Math.max(bar.open, bar.close) * verticalPixelRatio; - // Set wick X position + // Determine the X position for the wick. let wickX = bar.x * horizontalPixelRatio; const groupSize = bar.endIndex - bar.startIndex; if (groupSize && groupSize > 1) { - wickX= wickX+((candleWidth) * Math.max(1, groupSize )/2) + wickX += candleWidth * Math.max(1, groupSize) / 2; } - - - // Adjust wick heights for the 'Polygon' shape + + // Adjust wick heights for 'Polygon' shape candles. let upperWickTop = high; let upperWickBottom = openCloseTop; let lowerWickTop = openCloseBottom; let lowerWickBottom = low; - ctx.save(); - ctx.lineWidth= this._options?.lineWidth??1 + if (this._options.shape === 'Polygon') { - // Set halfway points for 'Polygon' shape + // For 'Polygon' candles, set halfway points. upperWickBottom = (high + openCloseTop) / 2; lowerWickTop = (low + openCloseBottom) / 2; } - - // Draw the upper wick (from high to halfway point for 'Polygon') + + // Set fill and stroke styles for the wick. + ctx.fillStyle = bar.color; + ctx.strokeStyle = bar.wickColor ?? bar.color; + + /** + * Draws a rounded rectangle or a standard rectangle as a wick. + * @param x - The X-coordinate of the top-left corner. + * @param y - The Y-coordinate of the top-left corner. + * @param width - The width of the rectangle. + * @param height - The height of the rectangle. + * @param radius - The corner radius for rounded rectangles. + */ + const drawRoundedRect = ( + x: number, + y: number, + width: number, + height: number, + radius: number + ) => { + if (ctx.roundRect) { + ctx.roundRect(x, y, width, height, radius); + } else { + ctx.rect(x, y, width, height); + } + }; + + // Draw the upper wick. const upperWickHeight = upperWickBottom - upperWickTop; if (upperWickHeight > 0) { - ctx.strokeRect( + ctx.beginPath(); + drawRoundedRect( wickX - Math.floor(wickWidth / 2), upperWickTop, wickWidth, - upperWickHeight + upperWickHeight, + wickWidth / 2 // Radius for rounded corners. ); + ctx.fill(); + ctx.stroke(); } - - // Draw the lower wick (from halfway point for 'Polygon' to low) + + // Draw the lower wick. const lowerWickHeight = lowerWickBottom - lowerWickTop; if (lowerWickHeight > 0) { - ctx.strokeRect( + ctx.beginPath(); + drawRoundedRect( wickX - Math.floor(wickWidth / 2), lowerWickTop, wickWidth, - lowerWickHeight + lowerWickHeight, + wickWidth / 2 // Radius for rounded corners. ); - } - ctx.restore(); - + ctx.fill(); + ctx.stroke(); + } } } - - + + /** + * Draws the candle bodies based on their specified shapes. + * Supports multiple shapes like Rectangle, Rounded, Ellipse, Arrow, 3D, and Polygon. + * @param renderingScope - The rendering scope containing canvas context and scaling information. + * @param bars - Array of aggregated BarItem objects to draw candles for. + * @param visibleRange - The range of visible bars to render. + * @param radius - The radius for rounded candle shapes. + * @param candleWidth - The width of the candle in pixels. + * @param horizontalPixelRatio - Scaling factor for horizontal dimensions. + * @param verticalPixelRatio - Scaling factor for vertical dimensions. + */ private _drawCandles( renderingScope: BitmapCoordinatesRenderingScope, bars: readonly BarItem[], @@ -284,151 +502,250 @@ export class ohlcSeriesRenderer verticalPixelRatio: number ): void { const { context: ctx } = renderingScope; - const barSpace = this._options?.barSpacing ?? 0.8 + const barSpace = this._options?.barSpacing ?? 0.8; + + // Save the current canvas state before drawing. + ctx.save(); + + // Iterate over each aggregated bar to draw its body. for (const bar of bars) { const groupSize = bar.endIndex - bar.startIndex; - let barHorizontalSpan = this._options?.chandelierSize !== 1 - ? (candleWidth) * (Math.max(1, groupSize + 1)) - ((1-barSpace) * candleWidth) - : (candleWidth * barSpace); - const barHorizontalPos = bar.x * horizontalPixelRatio; - const candleBodyWidth = candleWidth * barSpace; - - if (bar.startIndex < visibleRange.from || bar.endIndex > visibleRange.to) { - continue; - } - // Calculate vertical positions - const barVerticalMax = Math.min(bar.open, bar.close) * verticalPixelRatio; - const barVerticalMin = Math.max(bar.open, bar.close) * verticalPixelRatio; - const barVerticalSpan = barVerticalMax - barVerticalMin; - const barY= (barVerticalMax+ barVerticalMin)/2 - ctx.save(); - - // Set fill and stroke styles from bar properties - ctx.fillStyle = bar.color; - ctx.strokeStyle = bar.borderColor; - ctx.lineWidth = 1.5; - setLineStyle(ctx,this._options?.lineStyle??1 ) - ctx.lineWidth= this._options?.lineWidth??1 - - // Draw based on shape type - switch (this._options?.shape) { - case 'Rectangle': - this._drawCandle(ctx, barHorizontalPos, barY, candleBodyWidth, barHorizontalSpan, barVerticalSpan); - break; - case 'Rounded': - this._drawRounded(ctx, barHorizontalPos, barVerticalMin, candleBodyWidth, barHorizontalSpan, barVerticalSpan, radius, horizontalPixelRatio); - break; - case 'Ellipse': - this._drawEllipse(ctx, barHorizontalPos, barY, candleBodyWidth, barHorizontalSpan, barVerticalSpan); - break; - case 'Arrow': - this._drawArrow(ctx, barHorizontalPos, barVerticalMax, barVerticalMin, candleBodyWidth, barHorizontalSpan, bar.high * verticalPixelRatio, bar.low * verticalPixelRatio, bar.isUp); - break; - case '3d': + + // Calculate the horizontal span of the candle based on grouping. + const barHorizontalSpan = + this._options?.chandelierSize !== 1 + ? candleWidth * Math.max(1, groupSize + 1) - + (1 - barSpace) * candleWidth + : candleWidth * barSpace; + + // Determine the X position for the candle. + const barHorizontalPos = bar.x * horizontalPixelRatio; + + // Calculate the actual width of the candle body. + const candleBodyWidth = candleWidth * barSpace; + + // Skip rendering if the bar is outside the visible range. + if ( + bar.startIndex < visibleRange.from || + bar.endIndex > visibleRange.to + ) { + continue; + } + + // Calculate vertical positions for the candle body. + const barVerticalMax = Math.min(bar.open, bar.close) * verticalPixelRatio; + const barVerticalMin = Math.max(bar.open, bar.close) * verticalPixelRatio; + const barVerticalSpan = barVerticalMax - barVerticalMin; + const barY = (barVerticalMax + barVerticalMin) / 2; + + // Precompute common X coordinates for drawing. + const leftSide = barHorizontalPos - candleBodyWidth / 2; + const rightSide = leftSide + barHorizontalSpan; + const middle = leftSide + barHorizontalSpan / 2; + + // Set fill and stroke styles from bar properties. + ctx.fillStyle = + bar.color ?? this._options?.color ?? 'rgba(255,255,255,1)'; + ctx.strokeStyle = + bar.borderColor ?? + this._options?.borderColor ?? + bar.color ?? + 'rgba(255,255,255,1)'; + setLineStyle(ctx, bar.lineStyle); + ctx.lineWidth = bar.lineWidth ?? DEFAULT_LINE_WIDTH; + + // Draw the candle based on its specified shape. + switch (bar.shape) { + case 'Rectangle': + this._drawCandle(ctx, leftSide, rightSide, barY, barVerticalSpan); + break; + + case 'Rounded': + this._drawRounded( + ctx, + leftSide, + rightSide, + barY, + barVerticalSpan, + radius + ); + break; + + case 'Ellipse': + this._drawEllipse( + ctx, + leftSide, + rightSide, + middle, + barY, + barVerticalSpan, + ); + break; + + case 'Arrow': + this._drawArrow( + ctx, + leftSide, + rightSide, + middle, + barY, + barVerticalSpan, + bar.high * verticalPixelRatio, + bar.low * verticalPixelRatio, + bar.isUp + ); + break; + + case '3d': this._draw3d(ctx, barHorizontalPos, bar.high * verticalPixelRatio, bar.low * verticalPixelRatio, bar.open * verticalPixelRatio, bar.close * verticalPixelRatio, candleBodyWidth, barHorizontalSpan, bar.color, bar.borderColor, bar.isUp, barSpace); - break; - case 'Polygon': - this._drawPolygon(ctx, barHorizontalPos, barVerticalMin + barVerticalSpan, barVerticalMin, candleBodyWidth, barHorizontalSpan, bar.high * verticalPixelRatio, bar.low * verticalPixelRatio, bar.isUp); - break; - - default: - // Optional: fallback for unknown shapes - this._drawCandle(ctx, barHorizontalPos, barY, candleBodyWidth, barHorizontalSpan, barVerticalSpan); - break; - } - - // Restore the state - ctx.restore(); - } + break; + + case 'Polygon': + this._drawPolygon( + ctx, + leftSide, + rightSide, + barY, + barVerticalSpan, + bar.high * verticalPixelRatio, + bar.low * verticalPixelRatio, + bar.isUp + ); + break; + + default: + // Fallback to rectangle shape if unknown shape is specified. + this._drawCandle(ctx, leftSide, rightSide, barY, barVerticalSpan); + break; } - - private _drawCandle( - ctx: CanvasRenderingContext2D, - xCenter: number, - yCenter: number, - candleWidth: number, - combinedWidth: number, - candleHeight: number - ): void { - // Calculate the left and right edges of the candle based on xCenter and combined width - const leftEdge = xCenter - candleWidth / 2; - const rightEdge = xCenter - (candleWidth/2) + combinedWidth; - const topEdge = yCenter - candleHeight / 2; - const bottomEdge = yCenter + candleHeight / 2; - + } - // Begin drawing the candle rectangle - ctx.beginPath(); - ctx.moveTo(leftEdge, topEdge); - ctx.lineTo(leftEdge, bottomEdge); - ctx.lineTo(rightEdge, bottomEdge); - ctx.lineTo(rightEdge, topEdge); - ctx.closePath(); - - // Fill and stroke the rectangle - ctx.fill(); - ctx.stroke(); + // Restore the canvas state after drawing. + ctx.restore(); + } + + /** + * Draws a rectangle-shaped candle. + * @param ctx - The canvas rendering context. + * @param leftSide - The X-coordinate of the left edge of the candle. + * @param rightSide - The X-coordinate of the right edge of the candle. + * @param yCenter - The Y-coordinate of the center of the candle. + * @param candleHeight - The height of the candle in pixels. + */ + private _drawCandle( + ctx: CanvasRenderingContext2D, + leftSide: number, + rightSide: number, + yCenter: number, + candleHeight: number + ): void { + const topEdge = yCenter - candleHeight / 2; + const bottomEdge = yCenter + candleHeight / 2; + + // Begin drawing the candle rectangle. + ctx.beginPath(); + ctx.moveTo(leftSide, topEdge); + ctx.lineTo(leftSide, bottomEdge); + ctx.lineTo(rightSide, bottomEdge); + ctx.lineTo(rightSide, topEdge); + ctx.closePath(); + + // Fill and stroke the rectangle. + ctx.fill(); + ctx.stroke(); + } + + /** + * Draws a rounded rectangle-shaped candle. + * @param ctx - The canvas rendering context. + * @param leftSide - The X-coordinate of the left edge of the candle. + * @param rightSide - The X-coordinate of the right edge of the candle. + * @param yCenter - The Y-coordinate of the center of the candle. + * @param candleHeight - The height of the candle in pixels. + * @param radius - The corner radius for the rounded rectangle. + */ + private _drawRounded( + ctx: CanvasRenderingContext2D, + leftSide: number, + rightSide: number, + yCenter: number, + candleHeight: number, + radius: number + ): void { + const topEdge = yCenter - candleHeight / 2; + const width = rightSide - leftSide; + const effectiveRadius = Math.abs( + Math.min(radius, 0.1 * Math.min(width, candleHeight), 5) + ); + + // Begin drawing the rounded rectangle. + ctx.beginPath(); + if (ctx.roundRect) { + ctx.roundRect(leftSide, topEdge, width, candleHeight, effectiveRadius); + } else { + // Fallback to standard rectangle if roundRect is not supported. + ctx.rect(leftSide, topEdge, width, candleHeight); } - - //private _drawXShape(ctx: CanvasRenderingContext2D, xCenter: number, openCloseTop: number, openCloseBottom: number, candleWidth: number, combinedWidth: number, candleHeight: number): void { - // const controlOffsetX = candleWidth / 3; - // const controlOffsetY = candleHeight / 3; - // - // ctx.beginPath(); - // ctx.moveTo(xCenter - candleWidth / 2, openCloseTop); - // ctx.bezierCurveTo(xCenter - controlOffsetX, openCloseTop + controlOffsetY, xCenter + controlOffsetX, openCloseTop + controlOffsetY, xCenter + combinedWidth / 2, openCloseTop); - // ctx.bezierCurveTo(xCenter + combinedWidth / 2 - controlOffsetX, (openCloseTop + openCloseBottom) / 2, xCenter + combinedWidth / 2 - controlOffsetX, (openCloseTop + openCloseBottom) / 2, xCenter + combinedWidth / 2, openCloseBottom); - // ctx.bezierCurveTo(xCenter + controlOffsetX, openCloseBottom - controlOffsetY, xCenter - controlOffsetX, openCloseBottom - controlOffsetY, xCenter - combinedWidth / 2, openCloseBottom); - // ctx.bezierCurveTo(xCenter - candleWidth / 2 + controlOffsetX, (openCloseTop + openCloseBottom) / 2, xCenter - combinedWidth / 2 + controlOffsetX, (openCloseTop + openCloseBottom) / 2, xCenter - combinedWidth / 2, openCloseTop); - // ctx.closePath(); - // ctx.stroke(); - // ctx.fill(); - //} + ctx.fill(); + ctx.stroke(); + } + + /** + * Draws an ellipse-shaped candle. + * @param ctx - The canvas rendering context. + * @param leftSide - The X-coordinate of the left edge of the ellipse. + * @param rightSide - The X-coordinate of the right edge of the ellipse. + * @param middle - The X-coordinate of the center of the ellipse. + * @param yCenter - The Y-coordinate of the center of the ellipse. + * @param candleHeight - The height of the ellipse in pixels. + * @param barSpacing - The spacing factor between bars. + */ private _drawEllipse( ctx: CanvasRenderingContext2D, - xCenter: number, + leftSide: number, + rightSide: number, + middle: number, yCenter: number, - candleWidth: number, - combinedWidth: number, candleHeight: number, ): void { - // Calculate x and y radii based on the group size and bar spacing - const xRadius = combinedWidth/2 + // Calculate radii based on candle dimensions and spacing. + const xRadius = (rightSide - leftSide) / 2; const yRadius = candleHeight / 2; - - // Shift xCenter to the right by half the total group width + one candleWidth for HTF candles - const adjustedXCenter = xCenter-(candleWidth/2) + (combinedWidth/2) - + const adjustedXCenter = middle; + + // Begin drawing the ellipse. ctx.beginPath(); ctx.ellipse( - adjustedXCenter, // Shifted center only for HTF candles - yCenter, - Math.abs(xRadius), - Math.abs(yRadius), - 0, - 0, - Math.PI * 2 + adjustedXCenter, // X-coordinate of the center. + yCenter, // Y-coordinate of the center. + Math.abs(xRadius), // Horizontal radius. + Math.abs(yRadius), // Vertical radius. + 0, // Rotation angle. + 0, // Start angle. + Math.PI * 2 // End angle. ); ctx.fill(); ctx.stroke(); } - - - - - private _drawRounded(ctx: CanvasRenderingContext2D, xCenter: number, openCloseTop: number, candleWidth: number, combinedWidth: number, candleHeight: number, radius: number,horizontalPixelRatio:number): void { - if (ctx.roundRect) { - const effectiveRadius = Math.abs(Math.min(radius, 0.1 * Math.min(candleWidth, candleHeight), 5))*horizontalPixelRatio; - ctx.beginPath(); - ctx.roundRect(xCenter - candleWidth / 2, openCloseTop, combinedWidth, candleHeight, effectiveRadius); - ctx.stroke(); - ctx.fill(); - } else { - ctx.strokeRect(xCenter - candleWidth / 2, openCloseTop, combinedWidth, candleHeight); - ctx.fillRect(xCenter - candleWidth / 2, openCloseTop, combinedWidth, candleHeight); - } - } - + + + /** + * Draws a 3D-shaped candle, providing a depth effect. + * @param ctx - The canvas rendering context. + * @param leftSide - The X-coordinate of the front left edge of the candle. + * @param rightSide - The X-coordinate of the front right edge of the candle. + * @param middle - The X-coordinate of the center depth. + * @param yCenter - The Y-coordinate of the center of the candle. + * @param candleHeight - The height of the candle in pixels. + * @param highY - The Y-coordinate of the highest point of the candle. + * @param lowY - The Y-coordinate of the lowest point of the candle. + * @param openY - The Y-coordinate of the opening price. + * @param closeY - The Y-coordinate of the closing price. + * @param fillColor - The fill color of the candle. + * @param borderColor - The border color of the candle. + * @param isUp - Indicates if the candle is upward-moving. + * @param barSpacing - The spacing factor between bars. + */ private _draw3d( ctx: CanvasRenderingContext2D, xCenter: number, @@ -581,124 +898,111 @@ export class ohlcSeriesRenderer - private _drawPolygon( - ctx: CanvasRenderingContext2D, - xCenter: number, - openCloseTop: number, - openCloseBottom: number, - candleWidth: number, - combinedWidth: number, - high: number, - low: number, - isUp: boolean, - //topColor?: string, - //bottomColor?: string, - ): void - { - - ctx.beginPath(); - if (isUp) { - ctx.moveTo(xCenter - candleWidth / 2, openCloseTop); - ctx.lineTo(xCenter + combinedWidth - candleWidth/2, high); - ctx.lineTo(xCenter + combinedWidth - candleWidth/2, openCloseBottom); - ctx.lineTo(xCenter - candleWidth / 2, low); - } else { - ctx.moveTo(xCenter - candleWidth / 2, high); - ctx.lineTo(xCenter + combinedWidth - candleWidth/2, openCloseTop); - ctx.lineTo(xCenter + combinedWidth - candleWidth/2, low); - ctx.lineTo(xCenter - candleWidth / 2, openCloseBottom); - } - - ctx.closePath(); - ctx.stroke(); - ctx.fill(); - //// Draw the top overlay if topColor is provided - //if (topColor) { - // ctx.lineWidth = ctx.lineWidth*1.1 - // ctx.strokeStyle = setOpacity(topColor, 1); // Fully opaque border - // ctx.fillStyle = topColor; // Semi-transparent fill - // ctx.beginPath(); - // if (isUp) { - // // For up candles, bottom is between openCloseBottom and low - // ctx.moveTo(leftSide, openCloseBottom); - // ctx.lineTo(rightSide, openCloseBottom); - // ctx.lineTo(leftSide, low); - // ctx.lineTo(leftSide, openCloseBottom); -// - // } else { - // // For down candles, bottom is between openCloseBottom and low - // ctx.moveTo(leftSide, openCloseBottom); - // ctx.lineTo(rightSide, openCloseBottom); - // ctx.lineTo(rightSide, low); - // ctx.lineTo(leftSide, openCloseBottom); -// - // } - // - // ctx.closePath(); - // ctx.fill(); - // ctx.stroke(); - //} -// - //// Draw the bottom overlay if bottomColor is provided - //if (bottomColor) { - // ctx.lineWidth = ctx.lineWidth*1.1 -// - // ctx.strokeStyle = setOpacity(bottomColor, 1); // Fully opaque border - // ctx.fillStyle = bottomColor; // Semi-transparent fill - // ctx.beginPath(); - // if (isUp) { - // // For up candles, top is between openCloseTop and high - // ctx.moveTo(leftSide, openCloseTop); - // ctx.lineTo(rightSide, high); - // ctx.lineTo(rightSide, openCloseTop); - // ctx.lineTo(leftSide, openCloseTop); -// - // } else { - // // For down candles, top is between high and openCloseTop - // ctx.moveTo(leftSide, high); - // ctx.lineTo(rightSide, openCloseTop); - // ctx.lineTo(leftSide, openCloseTop); - // ctx.lineTo(leftSide, high); -// - // } - // ctx.closePath(); - // ctx.fill(); - // ctx.stroke(); - // - } - - - - private _drawArrow(ctx: CanvasRenderingContext2D, xCenter: number, - openCloseTopY: number, openCloseBottomY: number, candleBodyWidth: number,combinedBodyWidth:number, - highY: number, lowY: number, isUp: boolean): void { - ctx.beginPath(); - - const left = xCenter - candleBodyWidth / 2 - const right = left + combinedBodyWidth - const middle = left + combinedBodyWidth/2 - if (isUp) { - ctx.moveTo(left, lowY); - ctx.lineTo(left, openCloseTopY); - ctx.lineTo(middle, highY); - ctx.lineTo(right, openCloseTopY); - ctx.lineTo(right, lowY); - ctx.lineTo(middle, openCloseBottomY); - ctx.lineTo(left, lowY); - } else { - ctx.moveTo(left, highY); - ctx.lineTo(left, openCloseBottomY); - ctx.lineTo(middle, lowY); - ctx.lineTo(right, openCloseBottomY); - ctx.lineTo(right, highY); - ctx.lineTo(middle, openCloseTopY); - ctx.lineTo(left, highY); + /** + * Draws a polygon-shaped candle. + * @param ctx - The canvas rendering context. + * @param leftSide - The X-coordinate of the left edge of the polygon. + * @param rightSide - The X-coordinate of the right edge of the polygon. + * @param middle - The X-coordinate of the center depth. + * @param yCenter - The Y-coordinate of the center of the polygon. + * @param candleHeight - The height of the polygon in pixels. + * @param highY - The Y-coordinate of the highest point of the polygon. + * @param lowY - The Y-coordinate of the lowest point of the polygon. + * @param isUp - Indicates if the polygon points upwards. + */ + private _drawPolygon( + ctx: CanvasRenderingContext2D, + leftSide: number, + rightSide: number, + yCenter: number, + candleHeight: number, + highY: number, + lowY: number, + isUp: boolean + ): void { + const openCloseTop = yCenter + candleHeight / 2; + const openCloseBottom = yCenter - candleHeight / 2; - } - ctx.closePath(); - ctx.fill(); - ctx.stroke(); + // Save the current canvas state before drawing. + ctx.save(); + ctx.beginPath(); + + if (isUp) { + // Define the path for an upward-pointing polygon. + ctx.moveTo(leftSide, openCloseTop); + ctx.lineTo(rightSide, highY); + ctx.lineTo(rightSide, openCloseBottom); + ctx.lineTo(leftSide, lowY); + } else { + // Define the path for a downward-pointing polygon. + ctx.moveTo(leftSide, highY); + ctx.lineTo(rightSide, openCloseTop); + ctx.lineTo(rightSide, lowY); + ctx.lineTo(leftSide, openCloseBottom); } + + // Complete the path and apply styles. + ctx.closePath(); + ctx.stroke(); + ctx.fill(); + // Restore the canvas state after drawing. + ctx.restore(); } - + + /** + * Draws an arrow-shaped candle. + * @param ctx - The canvas rendering context. + * @param leftSide - The X-coordinate of the left edge of the arrow. + * @param rightSide - The X-coordinate of the right edge of the arrow. + * @param middle - The X-coordinate of the tip of the arrow. + * @param yCenter - The Y-coordinate of the center of the arrow. + * @param candleHeight - The height of the arrow in pixels. + * @param highY - The Y-coordinate of the highest point of the arrow. + * @param lowY - The Y-coordinate of the lowest point of the arrow. + * @param isUp - Indicates if the arrow points upwards. + */ + private _drawArrow( + ctx: CanvasRenderingContext2D, + leftSide: number, + rightSide: number, + middle: number, + yCenter: number, + candleHeight: number, + highY: number, + lowY: number, + isUp: boolean + ): void { + // Save the current canvas state before drawing. + ctx.save(); + ctx.beginPath(); + + if (isUp) { + // Define the path for an upward-pointing arrow. + ctx.moveTo(leftSide, lowY); + ctx.lineTo(leftSide, yCenter + candleHeight / 2); + ctx.lineTo(middle, highY); + ctx.lineTo(rightSide, yCenter + candleHeight / 2); + ctx.lineTo(rightSide, lowY); + ctx.lineTo(middle, yCenter - candleHeight / 2); + ctx.lineTo(leftSide, lowY); + } else { + // Define the path for a downward-pointing arrow. + ctx.moveTo(leftSide, highY); + ctx.lineTo(leftSide, yCenter - candleHeight / 2); + ctx.lineTo(middle, lowY); + ctx.lineTo(rightSide, yCenter - candleHeight / 2); + ctx.lineTo(rightSide, highY); + ctx.lineTo(middle, yCenter + candleHeight / 2); + ctx.lineTo(leftSide, highY); + } + + // Complete the path and apply styles. + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Restore the canvas state after drawing. + ctx.restore(); + } +} From a8c4d113314cef5cf37e9e455af4cf8f1cfbaa0c Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:55:30 -0800 Subject: [PATCH 23/89] Update ohlc-series.ts --- src/ohlc-series/ohlc-series.ts | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/src/ohlc-series/ohlc-series.ts b/src/ohlc-series/ohlc-series.ts index 26c20eb..6a451ed 100644 --- a/src/ohlc-series/ohlc-series.ts +++ b/src/ohlc-series/ohlc-series.ts @@ -8,13 +8,10 @@ import { WhitespaceData, Time, LineStyle, - LineWidth, - CandlestickData + LineWidth } from 'lightweight-charts'; +import { ohlcSeriesData, CandleShape } from './data'; import { ohlcSeriesRenderer} from './renderer'; -import { - -} from 'lightweight-charts'; export interface ohlcSeriesOptions extends CustomSeriesOptions, @@ -23,7 +20,7 @@ export interface ohlcSeriesOptions 'borderColor' > { radius: (barSpacing: number) => number; - shape:'Rectangle'|'Rounded'|'Ellipse'|'Arrow'|'Polygon'|'3d'; + shape:CandleShape; chandelierSize: number barSpacing: number lineStyle: LineStyle @@ -49,7 +46,7 @@ export const ohlcdefaultOptions: ohlcSeriesOptions = { if (bs < 4) return 0; return bs / 3; }, - shape: 'Rectangle', // Default shape + shape: 'Rectangle' as CandleShape, // Default shape chandelierSize: 1, barSpacing: 0.8, lineStyle: 0 as LineStyle, @@ -92,19 +89,5 @@ export class ohlcSeries return ohlcdefaultOptions; } } +// ./types.ts -export interface ohlcSeriesData extends CandlestickData { - time: Time; // The time of the candle, typically required by the chart - open: number; // Opening price - high: number; // Highest price - low: number; // Lowest price - close: number; // Closing price - - // Optional customization properties - color?: string; // Optional fill color for the candle body - borderColor?: string; // Optional color for the candle border - wickColor?: string; // Optional color for the candle wicks - shape?: string; // Optional shape (e.g., 'Rectangle', 'Rounded', 'Ellipse', 'Arrow', '3d', 'Polygon') - lineStyle?: number; // Optional line style (e.g., solid, dashed) - lineWidth?: number; // Optional line width for the border or wick -} From f2515fc24234f0130901592000f2525aef0fc164 Mon Sep 17 00:00:00 2001 From: EsIstJosh <81405820+EsIstJosh@users.noreply.github.com> Date: Wed, 20 Nov 2024 01:56:36 -0800 Subject: [PATCH 24/89] Add files via upload --- src/ohlc-series/data.ts | 162 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/ohlc-series/data.ts diff --git a/src/ohlc-series/data.ts b/src/ohlc-series/data.ts new file mode 100644 index 0000000..bf07b0a --- /dev/null +++ b/src/ohlc-series/data.ts @@ -0,0 +1,162 @@ +import { + CandlestickData, + Time, + LineStyle, + LineWidth +} from 'lightweight-charts'; + +export interface ohlcSeriesData extends CandlestickData { + time: Time; // The time of the candle, typically required by the chart + open: number; // Opening price + high: number; // Highest price + low: number; // Lowest price + close: number; // Closing price + + // Optional customization properties + color?: string; // Optional fill color for the candle body + borderColor?: string; // Optional color for the candle border + wickColor?: string; // Optional color for the candle wicks + shape?: string; // Optional shape (e.g., 'Rectangle', 'Rounded', 'Ellipse', 'Arrow', '3d', 'Polygon') + lineStyle?: number; // Optional line style (e.g., solid, dashed) + lineWidth?: number; // Optional line width for the border or wick +} +/** + * Enumeration for different candle shapes. + */ +export enum CandleShape { + Rectangle = 'Rectangle', + Rounded = 'Rounded', + Ellipse = 'Ellipse', + Arrow = 'Arrow', + Cube = '3d', + Polygon = 'Polygon', + } + + /** + * Enumeration for different line styles. + */ + + + /** + * Interface representing the original data of a bar. + */ + export interface BarOriginalData { + open: number; + high: number; + low: number; + close: number; + lineStyle: LineStyle; + lineWidth: number; + shape: CandleShape; + color: string; + borderColor: string; + wickColor: string; + } + + /** + * Interface representing a bar item used in rendering. + */ + export interface BarItem { + open: number; + high: number; + low: number; + close: number; + x: number; + isUp: boolean; + startIndex: number; + endIndex: number; + isInProgress?: boolean; + color: string; + borderColor: string; + wickColor: string; + originalData?: BarOriginalData; + lineStyle: LineStyle; + lineWidth: number; + shape: CandleShape; + } + + /** + * Interface for aggregator configuration options. + */ + export interface AggregatorOptions { + /** + * Size of the chandelier aggregation. Determines how many bars are grouped together. + * Default is 1 (no aggregation). + */ + chandelierSize?: number; + + /** + * Color of upward-moving candles. + * Default: 'rgba(0,255,0,0.333)' + */ + upColor?: string; + + /** + * Color of downward-moving candles. + * Default: 'rgba(255,0,0,0.333)' + */ + downColor?: string; + + /** + * Border color for upward-moving candles. + * If not specified, defaults to full opacity of `upColor`. + */ + borderUpColor?: string; + + /** + * Border color for downward-moving candles. + * If not specified, defaults to full opacity of `downColor`. + */ + borderDownColor?: string; + + /** + * Wick color for upward-moving candles. + * If not specified, defaults to `borderUpColor` or `upColor`. + */ + wickUpColor?: string; + + /** + * Wick color for downward-moving candles. + * If not specified, defaults to `borderDownColor` or `downColor`. + */ + wickDownColor?: string; + + /** + * Line style for candle borders. + * Uses the `LineStyle` enum. + * Default: `LineStyle.Solid` + */ + lineStyle?: LineStyle; + + /** + * Line width for candle borders. + * Default: 1 + */ + lineWidth?: LineWidth; + + /** + * Shape of the candles. + * Uses the `CandleShape` enum. + * Default: `CandleShape.Rectangle` + */ + shape?: CandleShape; + } + export function parseCandleShape(input: string): CandleShape | undefined { + switch (input.trim().toLowerCase()) { + case 'rectangle': + return CandleShape.Rectangle; + case 'rounded': + return CandleShape.Rounded; + case 'ellipse': + return CandleShape.Ellipse; + case 'arrow': + return CandleShape.Arrow; + case '3d': + return CandleShape.Cube; + case 'polygon': + return CandleShape.Polygon; + default: + console.warn(`Unknown CandleShape: ${input}`); + return CandleShape.Rectangle; + } +} \ No newline at end of file From e3af7d5fa6722862e26442f5d91b080da09b4713 Mon Sep 17 00:00:00 2001 From: EsIstTurnt Date: Fri, 13 Dec 2024 11:03:13 -0800 Subject: [PATCH 25/89] Enhance: Context Menu --- abstract.py | 2124 ++++++++++++++++++++++++++++ lightweight_charts_/js/bundle.js | 1 + src/context-menu/color-picker_.ts | 291 ++++ src/context-menu/context-menu.ts | 2202 +++++++++++++++++++++++++++-- src/fill-area/fill-area.ts | 363 +++++ src/general/global-params.ts | 93 +- src/general/handler.ts | 469 ++++-- src/general/index.ts | 2 +- src/general/legend.ts | 994 ++++++++----- src/general/toolbox.ts | 12 +- src/helpers/closest-index.ts | 44 + src/helpers/colors.ts | 52 + src/helpers/general.ts | 183 +++ src/helpers/typeguards.ts | 50 + src/index.ts | 4 +- src/tooltip/tooltip-element.ts | 218 +++ src/tooltip/tooltip.ts | 320 +++++ 17 files changed, 6829 insertions(+), 593 deletions(-) create mode 100644 abstract.py create mode 100644 lightweight_charts_/js/bundle.js create mode 100644 src/context-menu/color-picker_.ts create mode 100644 src/fill-area/fill-area.ts create mode 100644 src/helpers/closest-index.ts create mode 100644 src/helpers/colors.ts create mode 100644 src/helpers/general.ts create mode 100644 src/helpers/typeguards.ts create mode 100644 src/tooltip/tooltip-element.ts create mode 100644 src/tooltip/tooltip.ts diff --git a/abstract.py b/abstract.py new file mode 100644 index 0000000..793055f --- /dev/null +++ b/abstract.py @@ -0,0 +1,2124 @@ +import asyncio +import json +import os +from base64 import b64decode +from datetime import datetime +from typing import Callable, Union, Literal, List, Optional, Dict +import pandas as pd + +from .table import Table +from .toolbox import ToolBox +from .drawings import Box, HorizontalLine, RayLine, TrendLine, TwoPointDrawing, VerticalLine, VerticalSpan, Candle, ChandelierSeries +from .topbar import TopBar +from .util import ( + BulkRunScript, Pane, Events, IDGen, as_enum, jbool, js_json, TIME, NUM, FLOAT, + LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, + PRICE_SCALE_MODE,CANDLE_SHAPE, marker_position, marker_shape, js_data, +) + +current_dir = os.path.dirname(os.path.abspath(__file__)) +INDEX = os.path.join(current_dir, 'js', 'index.html') + + +class Window: + _id_gen = IDGen() + handlers = {} + + def __init__( + self, + script_func: Optional[Callable] = None, + js_api_code: Optional[str] = None, + run_script: Optional[Callable] = None + ): + self.loaded = False + self.script_func = script_func + self.scripts = [] + self.final_scripts = [] + self.bulk_run = BulkRunScript(script_func) + + if run_script: + self.run_script = run_script + + if js_api_code: + self.run_script(f'window.callbackFunction = {js_api_code}') + + def on_js_load(self): + if self.loaded: + return + self.loaded = True + + if hasattr(self, '_return_q'): + while not self.run_script_and_get('document.readyState == "complete"'): + continue # scary, but works + + initial_script = '' + self.scripts.extend(self.final_scripts) + for script in self.scripts: + initial_script += f'\n{script}' + self.script_func(initial_script) + + def run_script(self, script: str, run_last: bool = False): + """ + For advanced users; evaluates JavaScript within the Webview. + """ + if self.script_func is None: + raise AttributeError("script_func has not been set") + if self.loaded: + if self.bulk_run.enabled: + self.bulk_run.add_script(script) + else: + self.script_func(script) + elif run_last: + self.final_scripts.append(script) + else: + self.scripts.append(script) + + def run_script_and_get(self, script: str): + self.run_script(f'_~_~RETURN~_~_{script}') + return self._return_q.get() + + def create_table( + self, + width: NUM, + height: NUM, + headings: tuple, + widths: Optional[tuple] = None, + alignments: Optional[tuple] = None, + position: FLOAT = 'left', + draggable: bool = False, + background_color: str = '#121417', + border_color: str = 'rgb(70, 70, 70)', + border_width: int = 1, + heading_text_colors: Optional[tuple] = None, + heading_background_colors: Optional[tuple] = None, + return_clicked_cells: bool = False, + func: Optional[Callable] = None + ) -> 'Table': + return Table(*locals().values()) + + def create_subchart( + self, + position: FLOAT = 'left', + width: float = 0.5, + height: float = 0.5, + sync_id: Optional[str] = None, + scale_candles_only: bool = False, + sync_crosshairs_only: bool = False, + toolbox: bool = False + ) -> 'AbstractChart': + subchart = AbstractChart( + self, + width, + height, + scale_candles_only, + toolbox, + position=position + ) + if not sync_id: + return subchart + self.run_script(f''' + Lib.Handler.syncCharts( + {subchart.id}, + {sync_id}, + {jbool(sync_crosshairs_only)} + ) + ''', run_last=True) + return subchart + + def style( + self, + background_color: str = '#0c0d0f', + hover_background_color: str = '#3c434c', + click_background_color: str = '#50565E', + active_background_color: str = 'rgba(0, 122, 255, 0.7)', + muted_background_color: str = 'rgba(0, 122, 255, 0.3)', + border_color: str = '#3C434C', + color: str = '#d8d9db', + active_color: str = '#ececed' + ): + self.run_script(f'Lib.Handler.setRootStyles({js_json(locals())});') + + +class SeriesCommon(Pane): + def __init__(self, chart: 'AbstractChart', name: str = ''): + super().__init__(chart.win) + self._chart = chart + if hasattr(chart, '_interval'): + self._interval = chart._interval + else: + self._interval = 1 + self._last_bar = None + self.name = name + self.num_decimals = 2 + self.offset = 0 + self.data = pd.DataFrame() + self.markers = {} + self.primitives = { + 'ToolTip': False, + 'deltaToolTip': False + } + self.interval_str = self._format_interval_string() # Initialize with a formatted string + self.group = '' # Default to empty string; subclasses can set this + + # ... other methods ... + + def _set_interval(self, df: pd.DataFrame): + if not pd.api.types.is_datetime64_any_dtype(df['time']): + df['time'] = pd.to_datetime(df['time']) + common_interval = df['time'].diff().value_counts() + if common_interval.empty: + return + self._interval = common_interval.index[0].total_seconds() + + # Set interval string after calculating interval + self.interval_str = self._format_interval_string() + + units = [ + pd.Timedelta(microseconds=df['time'].dt.microsecond.value_counts().index[0]), + pd.Timedelta(seconds=df['time'].dt.second.value_counts().index[0]), + pd.Timedelta(minutes=df['time'].dt.minute.value_counts().index[0]), + pd.Timedelta(hours=df['time'].dt.hour.value_counts().index[0]), + pd.Timedelta(days=df['time'].dt.day.value_counts().index[0]), + ] + self.offset = 0 + for value in units: + value = value.total_seconds() + if value == 0: + continue + elif value >= self._interval: + break + self.offset = value + break + + def _format_interval_string(self) -> str: + """Convert the interval in seconds to a human-readable string format.""" + seconds = self._interval + + if seconds < 60: + return f"{int(seconds)}s" + elif seconds < 3600: + minutes = seconds // 60 + return f"{int(minutes)}m" + elif seconds < 86400: + hours = seconds // 3600 + return f"{int(hours)}h" + elif seconds < 2592000: # About 30 days + days = seconds // 86400 + return f"{int(days)}d" + elif seconds < 31536000: # About 365 days + months = seconds // 2592000 + return f"{int(months)}mo" + else: + years = seconds // 31536000 + return f"{int(years)}y" + + # Other methods as before... + @staticmethod + def _format_labels(data, labels, index, exclude_lowercase): + def rename(la, mapper): + return [mapper[key] if key in mapper else key for key in la] + if 'date' not in labels and 'time' not in labels: + labels = labels.str.lower() + if exclude_lowercase: + labels = rename(labels, {exclude_lowercase.lower(): exclude_lowercase}) + if 'date' in labels: + labels = rename(labels, {'date': 'time'}) + elif 'time' not in labels: + data['time'] = index + labels = [*labels, 'time'] + return labels + @staticmethod + def _legend_list_format(value: Union[str, List[str]]) -> List[str]: + """ + Ensures that the input `value` has exactly two elements. + - If `value` is a string, it duplicates it into a list of two elements. + - If `value` is a list with one item, it duplicates that item. + - If `value` is a list with more than two items, it truncates to the first two. + """ + if isinstance(value, str): + return [value, value] + elif len(value) == 1: + return [value[0], value[0]] + else: + return value[:2] + def _df_datetime_format(self, df: pd.DataFrame, exclude_lowercase=None): + df = df.copy() + df.columns = self._format_labels(df, df.columns, df.index, exclude_lowercase) + self._set_interval(df) + if not pd.api.types.is_datetime64_any_dtype(df['time']): + df['time'] = pd.to_datetime(df['time']) + df['time'] = df['time'].astype('int64') // 10 ** 9 + return df + + def _series_datetime_format(self, series: pd.Series, exclude_lowercase=None): + series = series.copy() + series.index = self._format_labels(series, series.index, series.name, exclude_lowercase) + series['time'] = self._single_datetime_format(series['time']) + return series + + def _single_datetime_format(self, arg) -> float: + if isinstance(arg, (str, int, float)) or not pd.api.types.is_datetime64_any_dtype(arg): + try: + arg = pd.to_datetime(arg, unit='ms') + except ValueError: + arg = pd.to_datetime(arg) + arg = self._interval * (arg.timestamp() // self._interval)+self.offset + return arg + + def set(self, df: Optional[pd.DataFrame] = None, format_cols: bool = True): + if df is None or df.empty: + self.run_script(f'{self.id}.series.setData([])') + self.data = pd.DataFrame() + return + if format_cols: + df = self._df_datetime_format(df, exclude_lowercase=self.name) + if self.name: + if self.name not in df: + raise NameError(f'No column named "{self.name}".') + df = df.rename(columns={self.name: 'value'}) + self.data = df.copy() + self._last_bar = df.iloc[-1] + self.run_script(f'{self.id}.series.setData({js_data(df)}); ') + def set_indicator( + self, + function: callable, + data: Optional[Union[pd.DataFrame, pd.Series]] = None, + parameters: Optional[List[Dict[str, Union[str, int, float, bool]]]] = None, + format_cols: bool = True + ): + + processed_data = align_length(function(data,parameters),data) + + + if df is None or df.empty: + self.run_script(f'{self.id}.series.setData([])') + self.data = pd.DataFrame() + return + if format_cols: + df = self._df_datetime_format(df, exclude_lowercase=self.name) + if self.name: + if self.name not in df: + raise NameError(f'No column named "{self.name}".') + df = df.rename(columns={self.name: 'value'}) + self.data = df.copy() + self._last_bar = df.iloc[-1] + self.run_script(f'{self.id}.series.setData({js_data(df)}); ') + + def update(self, series: pd.Series): + series = self._series_datetime_format(series, exclude_lowercase=self.name) + if self.name in series.index: + series.rename({self.name: 'value'}, inplace=True) + if self._last_bar is not None and series['time'] != self._last_bar['time']: + self.data.loc[self.data.index[-1]] = self._last_bar + self.data = pd.concat([self.data, series.to_frame().T], ignore_index=True) + self._last_bar = series + self.run_script(f'{self.id}.series.update({js_data(series)})') + + def _update_markers(self): + self.run_script(f'{self.id}.series.setMarkers({json.dumps(list(self.markers.values()))})') + + def marker_list(self, markers: list): + """ + Creates multiple markers.\n + :param markers: The list of markers to set. These should be in the format:\n + [ + {"time": "2021-01-21", "position": "below", "shape": "circle", "color": "#2196F3", "text": ""}, + {"time": "2021-01-22", "position": "below", "shape": "circle", "color": "#2196F3", "text": ""}, + ... + ] + :return: a list of marker ids. + """ + markers = markers.copy() + marker_ids = [] + for marker in markers: + marker_id = self.win._id_gen.generate() + self.markers[marker_id] = { + "time": self._single_datetime_format(marker['time']), + "position": marker_position(marker['position']), + "color": marker['color'], + "shape": marker_shape(marker['shape']), + "text": marker['text'], + } + marker_ids.append(marker_id) + self._update_markers() + return marker_ids + + def marker(self, time: Optional[datetime] = None, position: MARKER_POSITION = 'below', + shape: MARKER_SHAPE = 'arrow_up', color: str = '#2196F3', text: str = '' + ) -> str: + """ + Creates a new marker.\n + :param time: Time location of the marker. If no time is given, it will be placed at the last bar. + :param position: The position of the marker. + :param color: The color of the marker (rgb, rgba or hex). + :param shape: The shape of the marker. + :param text: The text to be placed with the marker. + :return: The id of the marker placed. + """ + try: + formatted_time = self._last_bar['time'] if not time else self._single_datetime_format(time) + except TypeError: + raise TypeError('Chart marker created before data was set.') + marker_id = self.win._id_gen.generate() + + self.markers[marker_id] = { + "time": formatted_time, + "position": marker_position(position), + "color": color, + "shape": marker_shape(shape), + "text": text, + } + self._update_markers() + return marker_id + + def remove_marker(self, marker_id: str): + """ + Removes the marker with the given id.\n + """ + self.markers.pop(marker_id) + self._update_markers() + + def horizontal_line(self, price: NUM, color: str = 'rgb(122, 146, 202)', width: int = 2, + style: LINE_STYLE = 'solid', text: str = '', axis_label_visible: bool = True, + func: Optional[Callable] = None + ) -> 'HorizontalLine': + """ + Creates a horizontal line at the given price. + """ + return HorizontalLine(self, price, color, width, style, text, axis_label_visible, func) + + def trend_line( + self, + start_time: TIME, + start_value: NUM, + end_time: TIME, + end_value: NUM, + round: bool = False, + line_color: str = '#1E80F0', + width: int = 2, + style: LINE_STYLE = 'solid', + ) -> TwoPointDrawing: + return TrendLine(*locals().values()) + + def box( + self, + start_time: TIME, + start_value: NUM, + end_time: TIME, + end_value: NUM, + round: bool = False, + color: str = '#1E80F0', + fill_color: str = 'rgba(255, 255, 255, 0.2)', + width: int = 2, + style: LINE_STYLE = 'solid', + ) -> TwoPointDrawing: + return Box(*locals().values()) + + def ray_line( + self, + start_time: TIME, + value: NUM, + round: bool = False, + color: str = '#1E80F0', + width: int = 2, + style: LINE_STYLE = 'solid', + text: str = '' + ) -> RayLine: + # TODO + return RayLine(*locals().values()) + + def vertical_line( + self, + time: TIME, + color: str = '#1E80F0', + width: int = 2, + style: LINE_STYLE ='solid', + text: str = '' + ) -> VerticalLine: + return VerticalLine(*locals().values()) + + def clear_markers(self): + """ + Clears the markers displayed on the data.\n + """ + self.markers.clear() + self._update_markers() + + def price_line(self, label_visible: bool = True, line_visible: bool = True, title: str = ''): + self.run_script(f''' + {self.id}.series.applyOptions({{ + lastValueVisible: {jbool(label_visible)}, + priceLineVisible: {jbool(line_visible)}, + title: '{title}', + }})''') + + def precision(self, precision: int): + """ + Sets the precision and minMove.\n + :param precision: The number of decimal places. + """ + min_move = 1 / (10**precision) + self.run_script(f''' + {self.id}.series.applyOptions({{ + priceFormat: {{precision: {precision}, minMove: {min_move}}} + }})''') + self.num_decimals = precision + + def hide_data(self): + self._toggle_data(False) + + def show_data(self): + self._toggle_data(True) + + def _toggle_data(self, arg): + self.run_script(f''' + {self.id}.series.applyOptions({{visible: {jbool(arg)}}}) + if ('volumeSeries' in {self.id}) {self.id}.volumeSeries.applyOptions({{visible: {jbool(arg)}}}) + ''') + + def vertical_span( + self, + start_time: Union[TIME, tuple, list], + end_time: Optional[TIME] = None, + color: str = 'rgba(252, 219, 3, 0.2)', + round: bool = False + ): + """ + Creates a vertical line or span across the chart.\n + Start time and end time can be used together, or end_time can be + omitted and a single time or a list of times can be passed to start_time. + """ + if round: + start_time = self._single_datetime_format(start_time) + end_time = self._single_datetime_format(end_time) if end_time else None + return VerticalSpan(self, start_time, end_time, color) + + def tooltip(self, line_color: str = 'rgba(0, 0, 0, 0.2)', follow_mode: str = 'top'): + """ + Attach a tooltip primitive to the series. + """ + if not self._chart.primitives.get('ToolTip'): + js_code = f""" + {self._chart.id}.attachTooltip('{self.name}', '{line_color}'); + """ + self._chart.run_script(js_code) + self._chart.primitives['ToolTip'] = True # Mark tooltip as attached + else: + self._update_tooltip_follow_mode(follow_mode) + + def detach_tooltip(self): + """ + Detach the tooltip primitive from the series. + """ + if self._chart.primitives.get('ToolTip'): + js_code = f""" + {self._chart.id}.detachTooltip('{self.name}'); + """ + self._chart.run_script(js_code) + self._chart.primitives['ToolTip'] = False # Mark tooltip as detached + + def delta_tooltip(self, line_color: str = 'rgba(0, 0, 0, 0.2)'): + """ + Attach a delta tooltip primitive to the series. + """ + if not self._chart.primitives.get('deltaToolTip'): + js_code = f""" + {self._chart.id}.attachDeltaTooltip('{self.name}', '{line_color}'); + """ + self._chart.run_script(js_code) + self._chart.primitives['deltaToolTip'] = True # Mark delta tooltip as attached + + def detach_delta_tooltip(self): + """ + Detach the delta tooltip primitive from the series. + """ + if self._chart.primitives.get('deltaToolTip'): + js_code = f""" + {self._chart.id}.detachDeltaTooltip('{self.name}'); + """ + self._chart.run_script(js_code) + self._chart.primitives['deltaToolTip'] = False # Mark delta tooltip as detached + + self.primitives['deltaToolTip'] = True + + + def attach_probability_cone(self): + """ + Attach a probability cone primitive to the series. + """ + if not self._chart.primitives.get('probabilityCone'): + js_code = f""" + {self._chart.id}.attachprobabilityCone('{self.name}'); + """ + self._chart.run_script(js_code) + self._chart.primitives['probabilityCone'] = True # Mark probability cone as attached + + def detach_probability_cone(self): + """ + Detach the probability cone primitive from the series. + """ + if self._chart.primitives.get('probabilityCone'): + js_code = f""" + {self._chart.id}.detachprobabilityCone('{self.name}'); + """ + self._chart.run_script(js_code) + self._chart.primitives['probabilityCone'] = False + + def delete(self): + """ + Irreversibly deletes the series and removes it from the legend and chart. + """ + # Remove the series from the chart's internal list if it exists + if hasattr(self._chart, '_lines') and self in self._chart._lines: + self._chart._lines.remove(self) + + # Prepare the group name for JavaScript (handle None) + group = self.group if self.group else '' + + self.run_script(f''' + // Remove the series from the legend + {self._chart.id}.legend.deleteLegendEntry('{self.name}', '{group}'); + + // Remove the series from the chart + {self._chart.id}.chart.removeSeries({self.id}.series); + + // Clean up references + delete {self.id}; + ''') + + + def fill_area( + self, + destination_series: str, + name: str = "FillArea", + origin_color: Optional[str] = None, + destination_color: Optional[str] = None, + ) -> 'FillArea': + """ + Creates a colored region between this series and the destination series. + + Args: + destination_series (SeriesCommon): The target series for the indicator. + origin_color (str): Color for the band area where this series is above the destination. + destination_color (str): Color for the band area where this series is below the destination. + line_width (Optional[int]): Line width for the bands. + name (str): Optional name for the FillArea. + + Returns: + FillArea: The created FillArea instance. + """ + + # Default name if none is provided + + # Create the FillArea + bands = FillArea( + chart=self._chart, + origin_series=self.name, + destination_series=destination_series, + origin_color=origin_color, + destination_color=destination_color, + name=name, + ) + + # Track the indicator for potential management or cleanup + + return bands +class Line(SeriesCommon): + def __init__( + self, chart, name, color, style, width, price_line, price_label, + group, legend_symbol, price_scale_id, crosshair_marker=True): + super().__init__(chart, name) + self.color = color + self.group = group # Store group for legend grouping + self.legend_symbol = legend_symbol # Store the legend symbol + + # Initialize series with configuration options + self.run_script(f''' + {self.id} = {self._chart.id}.createLineSeries( + "{name}", + {{ + group: '{group}', + title: '{name}', + color: '{color}', + lineStyle: {as_enum(style, LINE_STYLE)}, + lineWidth: {width}, + lastValueVisible: {jbool(price_label)}, + priceLineVisible: {jbool(price_line)}, + crosshairMarkerVisible: {jbool(crosshair_marker)}, + legendSymbol: '{legend_symbol}', + priceScaleId: {f'"{price_scale_id}"' if price_scale_id else 'undefined'} + {"""autoscaleInfoProvider: () => ({ + priceRange: { + minValue: 1_000_000_000, + maxValue: 0, + }, + }), + """ if chart._scale_candles_only else ''} + }} + ) + null''') + # if round: + # start_time = self._single_datetime_format(start_time) + # end_time = self._single_datetime_format(end_time) + # else: + # start_time, end_time = pd.to_datetime((start_time, end_time)).astype('int64') // 10 ** 9 + + # self.run_script(f''' + # {self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: false}}) + # {self.id}.series.setData( + # calculateTrendLine({start_time}, {start_value}, {end_time}, {end_value}, + # {self._chart.id}, {jbool(ray)})) + # {self._chart.id}.chart.timeScale().applyOptions({{shiftVisibleRangeOnNewBar: true}}) + # ''') + + def delete(self): + """ + Irreversibly deletes the line, as well as the object that contains the line. + """ + self._chart._lines.remove(self) if self in self._chart._lines else None + self.run_script(f''' + // Check if the item is part of a named group + if ('{self.group}' !== '' && '{self.group}' !== 'None') {{ + // Find the specific group by matching the group name + let targetGroup = {self._chart.id}.legend._groups.find(group => group.name === '{self.group}'); + if (targetGroup) {{ + // Locate the index of the item with the matching name in `names` array + let targetIndex = targetGroup.names.findIndex(name => name === '{self.name}'); + if (targetIndex !== -1) {{ + // Remove items at `targetIndex` from all arrays in the group + targetGroup.names.splice(targetIndex, 1); + targetGroup.seriesList.splice(targetIndex, 1); + targetGroup.solidColors.splice(targetIndex, 1); + targetGroup.legendSymbols.splice(targetIndex, 1); + + // Remove from `seriesTypes` only if it exists + if (targetGroup.seriesTypes) {{ + targetGroup.seriesTypes.splice(targetIndex, 1); + }} + + // If the group is now empty (e.g., `names` is empty), remove it from `_groups` + if (targetGroup.names.length === 0) {{ + {self._chart.id}.legend._groups = {self._chart.id}.legend._groups.filter(group => group !== targetGroup); + }} + }} + }} + }} else {{ + // Otherwise, treat it as a standalone item in `_lines` + {self.id}legendItem = {self._chart.id}.legend._lines.find((line) => line.series == {self.id}.series); + {self._chart.id}.legend._lines = {self._chart.id}.legend._lines.filter((item) => item !== {self.id}legendItem); + + // Remove from the legend div if it's a standalone row + if ({self.id}legendItem && {self.id}legendItem.row) {{ + {self._chart.id}.legend.div.removeChild({self.id}legendItem.row); + }} + }} + + // Remove the series from the chart and clean up the references + {self._chart.id}.chart.removeSeries({self.id}.series); + delete {self.id}legendItem; + delete {self.id}; + ''') + + def set(self,data): + super().set(data) +class Histogram(SeriesCommon): + def __init__( + self, chart, name, color, price_line, price_label, group, legend_symbol, scale_margin_top, scale_margin_bottom): + super().__init__(chart, name) + self.color = color + self.group = group # Store group for legend grouping + self.legend_symbol = legend_symbol # Store legend symbol + + self.run_script(f''' + {self.id} = {chart.id}.createHistogramSeries( + "{name}", + {{ + group: '{group}', + title: '{name}', + color: '{color}', + lastValueVisible: {jbool(price_label)}, + priceLineVisible: {jbool(price_line)}, + legendSymbol: '{legend_symbol}', + priceScaleId: '{self.id}', + priceFormat: {{type: "volume"}} + }}, + // precision: 2, + ) + {self.id}.series.priceScale().applyOptions({{ + scaleMargins: {{top:{scale_margin_top}, bottom: {scale_margin_bottom}}} + }})''') + + def delete(self): + """ + Irreversibly deletes the histogram. + """ + self.run_script(f''' + {self.id}legendItem = {self._chart.id}.legend._lines.find((line) => line.series == {self.id}.series) + {self._chart.id}.legend._lines = {self._chart.id}.legend._lines.filter((item) => item != {self.id}legendItem) + + if ({self.id}legendItem) {{ + {self._chart.id}.legend.div.removeChild({self.id}legendItem.row) + }} + + {self._chart.id}.chart.removeSeries({self.id}.series) + delete {self.id}legendItem + delete {self.id} + ''') + + def scale(self, scale_margin_top: float = 0.0, scale_margin_bottom: float = 0.0): + self.run_script(f''' + {self.id}.series.priceScale().applyOptions({{ + scaleMargins: {{top: {scale_margin_top}, bottom: {scale_margin_bottom}}} + }})''') + + + +class Area(SeriesCommon): + def __init__( + self, chart, name, top_color, bottom_color, invert, line_color, + style, width, price_line, price_label, group, legend_symbol, price_scale_id, crosshair_marker=True): + super().__init__(chart, name) + self.color = line_color + self.topColor = top_color + self.bottomColor = bottom_color + self.group = group # Store group for legend grouping + self.legend_symbol = legend_symbol # Store legend symbol + + self.run_script(f''' + {self.id} = {self._chart.id}.createAreaSeries( + "{name}", + {{ + group: '{group}', + title: '{name}', + topColor: '{top_color}', + bottomColor: '{bottom_color}', + invertFilledArea: {jbool(invert)}, + color: '{line_color}', + lineColor: '{line_color}', + lineStyle: {as_enum(style, LINE_STYLE)}, + lineWidth: {width}, + lastValueVisible: {jbool(price_label)}, + priceLineVisible: {jbool(price_line)}, + crosshairMarkerVisible: {jbool(crosshair_marker)}, + legendSymbol: '{legend_symbol}', + priceScaleId: {f'"{price_scale_id}"' if price_scale_id else 'undefined'} + {"""autoscaleInfoProvider: () => ({ + priceRange: { + minValue: 1_000_000_000, + maxValue: 0, + }, + }), + """ if chart._scale_candles_only else ''} + }} + ) + null''') + def delete(self): + """ + Irreversibly deletes the line, as well as the object that contains the line. + """ + self._chart._lines.remove(self) if self in self._chart._lines else None + self.run_script(f''' + {self.id}legendItem = {self._chart.id}.legend._lines.find((line) => line.series == {self.id}.series) + {self._chart.id}.legend._lines = {self._chart.id}.legend._lines.filter((item) => item != {self.id}legendItem) + + if ({self.id}legendItem) {{ + {self._chart.id}.legend.div.removeChild({self.id}legendItem.row) + }} + + {self._chart.id}.chart.removeSeries({self.id}.series) + delete {self.id}legendItem + delete {self.id} + ''') + + +class Bar(SeriesCommon): + def __init__( + self, chart, name, up_color, down_color, open_visible, thin_bars, + price_line, price_label, group, legend_symbol, price_scale_id): + super().__init__(chart, name) + self.up_color = up_color + self.down_color = down_color + self.group = group # Store group for legend grouping + self.legend_symbol = legend_symbol if isinstance(legend_symbol, list) else [legend_symbol, legend_symbol] # Store legend symbols + + self.run_script(f''' + {self.id} = {chart.id}.createBarSeries( + "{name}", + {{ + group: '{group}', + title: '{name}', + color: '{up_color}', + upColor: '{up_color}', + downColor: '{down_color}', + openVisible: {jbool(open_visible)}, + thinBars: {jbool(thin_bars)}, + lastValueVisible: {jbool(price_label)}, + priceLineVisible: {jbool(price_line)}, + legendSymbol: {json.dumps(self.legend_symbol)}, + priceScaleId: {f'"{price_scale_id}"' if price_scale_id else 'undefined'} + }} + + )''') + def set(self, df: Optional[pd.DataFrame] = None): + if df is None or df.empty: + self.run_script(f'{self.id}.series.setData([])') + self.candle_data = pd.DataFrame() + return + df = self._df_datetime_format(df) + self.data = df.copy() + self._last_bar = df.iloc[-1] + self.run_script(f'{self.id}.series.setData({js_data(df)})') + + def update(self, series: pd.Series, _from_tick=False): + """ + Updates the data from a bar; + if series['time'] is the same time as the last bar, the last bar will be overwritten.\n + :param series: labels: date/time, open, high, low, close, volume (if using volume). + """ + series = self._series_datetime_format(series) if not _from_tick else series + if series['time'] != self._last_bar['time']: + self.data.loc[self.data.index[-1]] = self._last_bar + self.data = pd.concat([self.data, series.to_frame().T], ignore_index=True) + self._chart.events.new_bar._emit(self) + + self._last_bar = series + self.run_script(f'{self.id}.series.update({js_data(series)})') + def delete(self): + """ + Irreversibly deletes the bar series. + """ + self.run_script(f''' + {self.id}legendItem = {self._chart.id}.legend._lines.find((line) => line.series == {self.id}.series) + {self._chart.id}.legend._lines = {self._chart.id}.legend._lines.filter((item) => item != {self.id}legendItem) + + if ({self.id}legendItem) {{ + {self._chart.id}.legend.div.removeChild({self.id}legendItem.row) + }} + + {self._chart.id}.chart.removeSeries({self.id}.series) + delete {self.id}legendItem + delete {self.id} + ''') +class CustomCandle(SeriesCommon): + def __init__( + self, + chart, + name: str, + up_color: str , + down_color: str , + border_up_color: str, + border_down_color: str , + wick_up_color: str , + wick_down_color: str , + wick_visible: bool = True, + border_visible: bool= True, + bar_width: float = 0.8, + radius: Optional[str] = 30, + shape: str = 'Rectangle', + combineCandles: int = 1, + vp_sections: int = 4, + line_width: int = 1, + line_style: LINE_STYLE = 'solid', + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: Union[str, List[str]] = ['⬤', '⬤'], + price_scale_id: Optional[str] = None ): + super().__init__(chart, name) + self.up_color = up_color + self.down_color = down_color + self.group = group # Store group for legend grouping + self.legend_symbol = legend_symbol if isinstance(legend_symbol, list) else [legend_symbol, legend_symbol] + radius_value = radius if radius is not None else 3 + + # Define the radius function as a JavaScript function string if none provided + radius_func = f"function(barSpacing) {{ return barSpacing < {radius_value} ? 0 : barSpacing / {radius_value}; }}" + + # Run the JavaScript to initialize the series with the provided options + self.run_script(f''' + {self.id} = {chart.id}.createCustomCandleSeries( + "{name}", + {{ + group: '{group}', + title: '{name}', + upColor: '{up_color}', + downColor: '{down_color}', + borderUpColor: '{border_up_color}', + borderDownColor: '{border_down_color}', + wickUpColor: '{wick_up_color or border_up_color}', + wickDownColor: '{wick_down_color or border_down_color}', + wickVisible: {jbool(wick_visible)}, + borderVisible: {jbool(border_visible)}, + barSpacing: {bar_width}, + radius: {radius_func}, + shape: '{shape}', + lastValueVisible: {jbool(price_label)}, + priceLineVisible: {jbool(price_line)}, + legendSymbol: {json.dumps(self.legend_symbol)}, + priceScaleId: {f'"{price_scale_id}"' if price_scale_id else 'undefined'}, + seriesType: "customCandle", + chandelierSize: {combineCandles}, + lineStyle: {as_enum(line_style, LINE_STYLE)}, + lineWidth: {line_width}, + vpSections: {vp_sections} + + }} + ) + null''') + + def set(self, df: Optional[pd.DataFrame] = None): + if df is None or df.empty: + self.run_script(f'{self.id}.series.setData([])') + self.data = pd.DataFrame() + return + df = self._df_datetime_format(df) + self.data = df.copy() + self._last_bar = df.iloc[-1] + self.run_script(f'{self.id}.series.setData({js_data(df)})') + + def update(self, series: pd.Series): + series = self._series_datetime_format(series) + if series['time'] != self._last_bar['time']: + self.data.loc[self.data.index[-1]] = self._last_bar + self.data = pd.concat([self.data, series.to_frame().T], ignore_index=True) + self._chart.events.new_bar._emit(self) + + self._last_bar = series + self.run_script(f'{self.id}.series.update({js_data(series)})') + + def delete(self): + """ + Irreversibly deletes the custom candle series. + """ + self.run_script(f''' + {self.id}legendItem = {self._chart.id}.legend._lines.find((line) => line.series == {self.id}.series) + {self._chart.id}.legend._lines = {self._chart.id}.legend._lines.filter((item) => item != {self.id}legendItem) + + if ({self.id}legendItem) {{ + {self._chart.id}.legend.div.removeChild({self.id}legendItem.row) + }} + + {self._chart.id}.chart.removeSeries({self.id}.series) + delete {self.id}legendItem + delete {self.id} + ''') +class HTFCandle(SeriesCommon): + def __init__( + self, + chart, + name: str, + up_color: str = '#26a69a', + down_color: str = '#ef5350', + border_up_color: str = '#26a69a', + border_down_color: str = '#ef5350', + wick_up_color: str = '#26a69a', + wick_down_color: str = '#ef5350', + wick_visible: bool = True, + border_visible: bool = True, + radius: Optional[str] = None, + multiple: int = 5, + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: Union[str, List[str]] = ['⬤', '⬤'], + price_scale_id: Optional[str] = None + ): + super().__init__(chart, name) + self.up_color = up_color + self.down_color = down_color + self.group = group # Store group for legend grouping + self.legend_symbol = legend_symbol if isinstance(legend_symbol, list) else [legend_symbol, legend_symbol] + radius_value = radius if radius is not None else 3 + + # Define the radius function as a JavaScript function string if none provided + radius_func = f"function(barSpacing) {{ return barSpacing < {radius_value} ? 0 : barSpacing / {radius_value}; }}" + + # Run the JavaScript to initialize the series with the provided options + + self.run_script(f''' + {self.id} = {chart.id}.createHigherTFCandleSeries( + "{name}", + {{ + group: '{group}', + title: '{name}', upColor: '{up_color}', + downColor: '{down_color}', + borderUpColor:'{border_up_color}', + borderDownColor:'{border_down_color}', + wickUpColor:'{border_up_color}', + wickDownColor:'{border_down_color}', + wickVisible: {jbool(wick_visible)}, + borderVisible: {jbool(border_visible)}, + radius: {radius_func}, + multiple: {multiple}, + lastValueVisible: {jbool(price_label)}, + priceLineVisible: {jbool(price_line)}, + legendSymbol: {json.dumps(self.legend_symbol)}, + priceScaleId: {f'"{price_scale_id}"' if price_scale_id else 'undefined'}, + seriesType: "htfCandle" + }} + ) + null''') + + def set(self, df: Optional[pd.DataFrame] = None): + if df is None or df.empty: + self.run_script(f'{self.id}.series.setData([])') + self.data = pd.DataFrame() + return + df = self._df_datetime_format(df) + self.data = df.copy() + self._last_bar = df.iloc[-1] + self.run_script(f'{self.id}.series.setData({js_data(df)})') + + def update(self, series: pd.Series): + series = self._series_datetime_format(series) + if series['time'] != self._last_bar['time']: + self.data.loc[self.data.index[-1]] = self._last_bar + self.data = pd.concat([self.data, series.to_frame().T], ignore_index=True) + self._chart.events.new_bar._emit(self) + + self._last_bar = series + self.run_script(f'{self.id}.series.update({js_data(series)})') + + def delete(self): + """ + Irreversibly deletes the custom candle series. + """ + self.run_script(f''' + {self.id}legendItem = {self._chart.id}.legend._lines.find((line) => line.series == {self.id}.series) + {self._chart.id}.legend._lines = {self._chart.id}.legend._lines.filter((item) => item != {self.id}legendItem) + + if ({self.id}legendItem) {{ + {self._chart.id}.legend.div.removeChild({self.id}legendItem.row) + }} + + {self._chart.id}.chart.removeSeries({self.id}.series) + delete {self.id}legendItem + delete {self.id} + ''') + +class CandleBar: + def __init__(self, chart: 'AbstractChart', name: str, up_color='#26a69a', down_color='#ef5350', open_visible=True, thin_bars=True, **kwargs): + # Use the existing chart (inherits from Candlestick) and data + self._chart = chart + self._name = name + self.up_color = up_color + self.down_color = down_color + self.args_dict = kwargs + + # Create an instance of Bar using composition + self._bar_chart = Bar( + chart=chart, + name=f"{name}_Bar", + up_color=up_color, + down_color=down_color, + open_visible=open_visible, + thin_bars=thin_bars, + price_line = True, + price_label = True, + group = '', + legend_symbol = ['┌', '└'], + price_scale_id = None + ) + + + # Apply the candle_style if relevant parameters are provided in kwargs + self._apply_candle_style_if_exists(kwargs) + + + def _apply_candle_style_if_exists(self, kwargs: dict): + """ + Checks for any candle style parameters in kwargs and applies the style if present. + """ + relevant_keys = [ + 'up_color', 'down_color', 'wick_visible', 'border_visible', + 'border_up_color', 'border_down_color', 'wick_up_color', 'wick_down_color' + ] + self._chart.candle_style( + up_color=kwargs.get('up_color', self.up_color), + down_color=kwargs.get('down_color', self.down_color), + wick_visible=kwargs.get('wick_visible', True), + border_visible=kwargs.get('border_visible', False), + border_up_color=kwargs.get('border_up_color', self.up_color), + border_down_color=kwargs.get('border_down_color', self.down_color), + wick_up_color=kwargs.get('wick_up_color', self.up_color), + wick_down_color=kwargs.get('wick_down_color', self.down_color) + ) + + self._chart.volume_config( + up_color = self.up_color, + down_color = self.down_color) + + def set(self, df: pd.DataFrame, condition_func: Callable[[pd.Series], bool]): + """ + Sets the initial data for the chart. + :param df: DataFrame with columns: date/time, open, high, low, close, volume. + :param condition_func: A callable that defines the visibility of bars. + """ + # Store a copy of the data + self._condition_func = condition_func + self._data = df.copy() + self._candles = df.copy() + self._bars = df.copy() + + # Determine visibility based on the condition function + visibility_condition = condition_func(self._data) + + # Make the entire candle completely invisible if condition_func is False + self._candles['color'] = self._candles.apply( + lambda row: ( + self.up_color if row['close'] > row['open'] else self.down_color + ) if visibility_condition.loc[row.name] else 'rgba(0,0,0,0)', + axis=1 + ) + self._candles['border_color'] = self._candles.apply( + lambda row: ( + self.up_color if row['close'] > row['open'] else self.down_color + ) if visibility_condition.loc[row.name] else 'rgba(0,0,0,0)', + axis=1 + ) + self._candles['wick_color'] = self._candles.apply( + lambda row: ( + self.up_color if row['close'] > row['open'] else self.down_color + ) if visibility_condition.loc[row.name] else 'rgba(0,0,0,0)', + axis=1 + ) + + # Define the candles DataFrame explicitly to include full OHLCV and styling columns + candles = self._candles[['time', 'open', 'high', 'low', 'close', 'volume', 'color', 'border_color', 'wick_color']].copy() + + # Initialize the bars DataFrame, which should only be visible if condition_func is True + bars = self._bars.copy() + + # Hide OHLC values for bars if not visible + bars.loc[visibility_condition, ['open', 'high', 'low', 'close']] = None + + # Set color-related columns to fully transparent if bars should not be visible + bars.loc[visibility_condition, ['color', 'border_color', 'wick_color']] = 'rgba(0,0,0,0)' + + # Use Candlestick class to set candlestick data + self._chart.set(candles) + + # Use Bar class to set bar data + self._bar_chart.set(bars) + + def update(self, series: pd.Series): + """ + Updates the data from a bar; + if series['time'] is the same time as the last bar, the last bar will be overwritten. + :param series: A Pandas Series containing the new bar data. + """ + # Append the new series to the existing data + self._data = pd.concat([self._data, series.to_frame().T], ignore_index=True) + + # Check the condition for the new series + condition_met = self._condition_func(self._data).iloc[-1] + + # Set colors based on whether the condition is met and price movement + if condition_met: + # When the condition is met, use standard colors + candle_color = "rgba(0,255,0,1)" if series['close'] > series['open'] else "rgba(255,0,0,1)" + border_color = candle_color + wick_color = candle_color + + # OHLCV data for the candle is fully visible + candle_data = { + 'time': series['time'], + 'open': series['open'], + 'high': series['high'], + 'low': series['low'], + 'close': series['close'], + 'volume': series['volume'], + 'color': candle_color, + 'border_color': border_color, + 'wick_color': wick_color, + } + + # Hide bar data (set OHLC to None) + bar_data = { + 'time': series['time'], + 'open': series['open'], + 'high': series['high'], + 'low': series['low'], + 'close': series['close'], + 'volume': series['volume'], + 'color': 'rgba(0,0,0,0)', + 'border_color': 'rgba(0,0,0,0)', + 'wick_color': 'rgba(0,0,0,0)', + } + else: + # When the condition is not met, hide the candle + candle_data = { + 'time': series['time'], + 'open': series['open'], + 'high': series['high'], + 'low': series['low'], + 'close': series['close'], + 'volume': series['volume'], + 'color': 'rgba(0,0,0,0)', + 'border_color': 'rgba(0,0,0,0)', + 'wick_color': 'rgba(0,0,0,0)', + } + + # Set bar data using standard colors + bar_color = "rgba(0,255,0,1)" if series['close'] > series['open'] else "rgba(255,0,0,1)" + bar_data = { + 'time': series['time'], + 'open': series['open'], + 'high': series['high'], + 'low': series['low'], + 'close': series['close'], + 'volume': series['volume'], + 'color': bar_color, + 'border_color': bar_color, + 'wick_color': bar_color, + } + + # Update candlestick and bar charts + self._chart.update(pd.Series(candle_data)) + self._bar_chart.update(pd.Series(bar_data)) + + +class Candlestick(SeriesCommon): + def __init__(self, chart: 'AbstractChart'): + super().__init__(chart) + self._volume_up_color = 'rgba(83,141,131,0.8)' + self._volume_down_color = 'rgba(200,127,130,0.8)' + + self.candle_data = pd.DataFrame() + + # self.run_script(f'{self.id}.makeCandlestickSeries()') + + def set(self, df: Optional[pd.DataFrame] = None, keep_drawings=False): + """ + Sets the initial data for the chart.\n + :param df: columns: date/time, open, high, low, close, volume (if volume enabled). + :param keep_drawings: keeps any drawings made through the toolbox. Otherwise, they will be deleted. + """ + if df is None or df.empty: + self.run_script(f'{self.id}.series.setData([])') + self.run_script(f'{self.id}.volumeSeries.setData([])') + self.candle_data = pd.DataFrame() + return + df = self._df_datetime_format(df) + self.candle_data = df.copy() + self._last_bar = df.iloc[-1] + self.run_script(f'{self.id}.series.setData({js_data(df)})') + + if 'volume' not in df: + return + volume = df.drop(columns=['open', 'high', 'low', 'close']).rename(columns={'volume': 'value'}) + volume['color'] = self._volume_down_color + volume.loc[df['close'] > df['open'], 'color'] = self._volume_up_color + self.run_script(f'{self.id}.volumeSeries.setData({js_data(volume)})') + + for line in self._lines: + if line.name not in df.columns: + continue + line.set(df[['time', line.name]], format_cols=False) + # set autoScale to true in case the user has dragged the price scale + self.run_script(f''' + if (!{self.id}.chart.priceScale("right").options.autoScale) + {self.id}.chart.priceScale("right").applyOptions({{autoScale: true}}) + ''') + # TODO keep drawings doesn't work consistenly w + if keep_drawings: + self.run_script(f'{self._chart.id}.toolBox?._drawingTool.repositionOnTime()') + else: + self.run_script(f"{self._chart.id}.toolBox?.clearDrawings()") + + def update(self, series: pd.Series, _from_tick=False): + """ + Updates the data from a bar; + if series['time'] is the same time as the last bar, the last bar will be overwritten.\n + :param series: labels: date/time, open, high, low, close, volume (if using volume). + """ + series = self._series_datetime_format(series) if not _from_tick else series + if series['time'] != self._last_bar['time']: + self.candle_data.loc[self.candle_data.index[-1]] = self._last_bar + self.candle_data = pd.concat([self.candle_data, series.to_frame().T], ignore_index=True) + self._chart.events.new_bar._emit(self) + + self._last_bar = series + self.run_script(f'{self.id}.series.update({js_data(series)})') + if 'volume' not in series: + return + volume = series.drop(['open', 'high', 'low', 'close']).rename({'volume': 'value'}) + volume['color'] = self._volume_up_color if series['close'] > series['open'] else self._volume_down_color + self.run_script(f'{self.id}.volumeSeries.update({js_data(volume)})') + + def update_from_tick(self, series: pd.Series, cumulative_volume: bool = False): + """ + Updates the data from a tick.\n + :param series: labels: date/time, price, volume (if using volume). + :param cumulative_volume: Adds the given volume onto the latest bar. + """ + series = self._series_datetime_format(series) + if series['time'] < self._last_bar['time']: + raise ValueError(f'Trying to update tick of time "{pd.to_datetime(series["time"])}", which occurs before the last bar time of "{pd.to_datetime(self._last_bar["time"])}".') + bar = pd.Series(dtype='float64') + if series['time'] == self._last_bar['time']: + bar = self._last_bar + bar['high'] = max(self._last_bar['high'], series['price']) + bar['low'] = min(self._last_bar['low'], series['price']) + bar['close'] = series['price'] + if 'volume' in series: + if cumulative_volume: + bar['volume'] += series['volume'] + else: + bar['volume'] = series['volume'] + else: + for key in ('open', 'high', 'low', 'close'): + bar[key] = series['price'] + bar['time'] = series['time'] + if 'volume' in series: + bar['volume'] = series['volume'] + self.update(bar, _from_tick=True) + + def price_scale( + self, + auto_scale: bool = True, + mode: PRICE_SCALE_MODE = 'normal', + invert_scale: bool = False, + align_labels: bool = True, + scale_margin_top: float = 0.2, + scale_margin_bottom: float = 0.2, + border_visible: bool = False, + border_color: Optional[str] = None, + text_color: Optional[str] = None, + entire_text_only: bool = False, + visible: bool = True, + ticks_visible: bool = False, + minimum_width: int = 0 + ): + self.run_script(f''' + {self.id}.series.priceScale().applyOptions({{ + autoScale: {jbool(auto_scale)}, + mode: {as_enum(mode, PRICE_SCALE_MODE)}, + invertScale: {jbool(invert_scale)}, + alignLabels: {jbool(align_labels)}, + scaleMargins: {{top: {scale_margin_top}, bottom: {scale_margin_bottom}}}, + borderVisible: {jbool(border_visible)}, + {f'borderColor: "{border_color}",' if border_color else ''} + {f'textColor: "{text_color}",' if text_color else ''} + entireTextOnly: {jbool(entire_text_only)}, + visible: {jbool(visible)}, + ticksVisible: {jbool(ticks_visible)}, + minimumWidth: {minimum_width} + }})''') + + def candle_style( + self, up_color: str = 'rgba(39, 157, 130, 100)', down_color: str = 'rgba(200, 97, 100, 100)', + wick_visible: bool = True, border_visible: bool = True, border_up_color: str = '', + border_down_color: str = '', wick_up_color: str = '', wick_down_color: str = ''): + """ + Candle styling for each of its parts.\n + If only `up_color` and `down_color` are passed, they will color all parts of the candle. + """ + border_up_color = border_up_color if border_up_color else up_color + border_down_color = border_down_color if border_down_color else down_color + wick_up_color = wick_up_color if wick_up_color else up_color + wick_down_color = wick_down_color if wick_down_color else down_color + self.run_script(f"{self.id}.series.applyOptions({js_json(locals())})") + + def volume_config(self, scale_margin_top: float = 0.8, scale_margin_bottom: float = 0.0, + up_color='rgba(83,141,131,0.8)', down_color='rgba(200,127,130,0.8)'): + """ + Configure volume settings.\n + Numbers for scaling must be greater than 0 and less than 1.\n + Volume colors must be applied prior to setting/updating the bars.\n + """ + self._volume_up_color = up_color if up_color else self._volume_up_color + self._volume_down_color = down_color if down_color else self._volume_down_color + self.run_script(f''' + {self.id}.volumeSeries.priceScale().applyOptions({{ + scaleMargins: {{ + top: {scale_margin_top}, + bottom: {scale_margin_bottom}, + }} + }})''') +class VolumeProfile: + def __init__(self, chart, side: bool, sections: int, width: float, up_color: str, down_color: str, + border_up_color: str, border_down_color: str, + fibonacci_profile:bool, fibonacci_levels:bool, start_index: int, end_index: int): + """ + Initialize a VolumeProfile, which triggers the JavaScript `createVolumeProfile` + method to calculate and display the volume profile for the specified series. + + :param chart: The chart instance. + :param side: The side to render the volume profile on (True for left, False for right). + :param up_color: The color for upward volume. + :param down_color: The color for downward volume. + """ + self.chart = chart + self.side = False if side == 'left' else True # Now a boolean where True=left and False=right + + self.chart.run_script(f''' + + {self.chart.id}.createVolumeProfile( + {jbool(self.side)}, + {sections}, + {width}, + "{up_color}", + "{down_color}", + "{border_up_color}", + "{border_down_color}", + {jbool(fibonacci_profile)}, + {jbool(fibonacci_levels)}, + {start_index if start_index is not None else "undefined"}, + {end_index if end_index is not None else "undefined"} + ); + ''') + +class DeltaProfile: + def __init__(self, chart, side:str,up_color: str, down_color: str): + + """ + Initialize a DeltaProfile, which triggers the JavaScript `createDeltaProfile` + method to calculate and display the delta profile for the specified series. + + :param chart: The chart instance. + :param size: The number of sections (bins) in the delta profile. + :param side: The side ('right' or 'left') to display the delta profile. + """ + self.chart = chart + self.side = 'true' if side == 'left' else 'false' + + self.up_color = up_color + self.down_color = down_color + # Call JavaScript to create the delta profile + self.create_delta_profile_js() + + def create_delta_profile_js(self): + """ + Calls the JavaScript `createDeltaProfile` method to render the delta profile for the specified series. + """ + self.chart.run_script(f''' + {self.chart.id}.createDeltaProfile({{ + {f'borderColor: "{self.side}",' if border_color else ''}, + upColor: "{self.up_color}", + downColor: "{self.down_color}" + + }}); + ''') +class FillArea: + def __init__( + self, + name: str, + chart: 'AbstractChart', + origin_series:str, + destination_series: str, + origin_color: Optional[str] = None, + destination_color: Optional[str] = None, + ): + self.chart = chart + self.origin_series = origin_series + self.destination_series = destination_series + self.origin_color = origin_color + self.destination_color = destination_color + self.name = name + + # Run JavaScript to create the visual indicator + js_code = f""" + {self.name} = {self.chart.id}.createFillArea({{ + originSeries: '{self.origin_series}', + destinationSeries: '{self.destination_series}', + {f'originColor: "{self.origin_color}",' if self.origin_color else ''} + {f'destinationColor: "{self.destination_color}",' if self.destination_color else ''} + name: '{self.name}' + }}); + """ + # Debugging: Print the JavaScript code + print("Generated JavaScript Code for FillArea:", js_code) + + # Execute the JavaScript + self.chart.run_script(js_code) + + + + def applyOptions(self, **kwargs): + """ + Updates the FillArea options dynamically. + + Args: + kwargs: Dictionary of options to update. + - originColor (str): New color for the origin side of the fill. + - destinationColor (str): New color for the destination side of the fill. + """ + # Update options with new values + for key, value in kwargs.items(): + if key in self.options: + self.options[key] = value + + # Build the JavaScript options object dynamically + js_options = [] + if self.options.get("originColor"): + js_options.append(f'originColor: "{self.options["originColor"]}"') + if self.options.get("destinationColor"): + js_options.append(f'destinationColor: "{self.options["destinationColor"]}"') + js_options_string = ", ".join(js_options) + + # Apply the updates via JavaScript + # Apply the updates to the chart + self.chart.run_script(f''' + const originSeries = this.seriesMap.get(origin); + const destinationSeries = this.seriesMap.get(destination); + + originSeries.primitives['{self.name}'].applyOptions({{ + {js_options_string} + + }}) + ''') +class AbstractChart(Candlestick, Pane): + def __init__(self, window: Window, width: float = 1.0, height: float = 1.0, + scale_candles_only: bool = False, toolbox: bool = False, + autosize: bool = True, position: FLOAT = 'left'): + Pane.__init__(self, window) + + self._lines = [] + self._scale_candles_only = scale_candles_only + self._width = width + self._height = height + self.events: Events = Events(self) + self.primitives = { + 'ToolTip': False, + 'deltaToolTip': False + } + from .polygon import PolygonAPI + self.polygon: PolygonAPI = PolygonAPI(self) + + self.run_script( + f'{self.id} = new Lib.Handler("{self.id}", {width}, {height}, "{position}", {jbool(autosize)})') + + Candlestick.__init__(self, self) + + self.topbar: TopBar = TopBar(self) + if toolbox: + self.toolbox: ToolBox = ToolBox(self) + + def fit(self): + """ + Fits the maximum amount of the chart data within the viewport. + """ + self.run_script(f'{self.id}.chart.timeScale().fitContent()') + + from typing import Union, List, Optional + + + def create_line( + self, + name: str = '', + color: str = 'rgba(214, 237, 255, 0.6)', + style: LINE_STYLE = 'solid', + width: int = 2, + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: str = '', + price_scale_id: Optional[str] = None + ) -> Line: + """ + Creates and returns a Line object. + """ + + symbol_styles = { + 'solid':'―', + 'dotted':'··', + 'dashed':'--', + 'large_dashed':'- -', + 'sparse_dotted':"· ·", + } + if legend_symbol == '': + legend_symbol = symbol_styles.get(style, '━') # Default to 'solid' if style is unrecognized + + if not isinstance(legend_symbol, str): + raise TypeError("legend_symbol must be a string for Line series.") + + self._lines.append(Line( + self, name, color, style, width, price_line, price_label, + group, legend_symbol, price_scale_id + )) + return self._lines[-1] + + def create_histogram( + self, + name: str = '', + color: str = 'rgba(214, 237, 255, 0.6)', + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: str = '▥', + scale_margin_top: float = 0.0, + scale_margin_bottom: float = 0.0 + ) -> Histogram: + """ + Creates and returns a Histogram object. + """ + if not isinstance(legend_symbol, str): + raise TypeError("legend_symbol must be a string for Histogram series.") + + return Histogram( + self, name, color, price_line, price_label, + group, legend_symbol, scale_margin_top, scale_margin_bottom + ) + + def create_area( + self, + name: str = '', + top_color: str = 'rgba(0, 100, 0, 0.5)', + bottom_color: str = 'rgba(138, 3, 3, 0.5)', + invert: bool = False, + color: str = 'rgba(0,0,255,1)', + style: LINE_STYLE = 'solid', + width: int = 2, + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: str = '◪', + price_scale_id: Optional[str] = None + ) -> Area: + """ + Creates and returns an Area object. + """ + if not isinstance(legend_symbol, str): + raise TypeError("legend_symbol must be a string for Area series.") + + self._lines.append(Area( + self, name, top_color, bottom_color, invert, color, style, + width, price_line, price_label, group, legend_symbol, price_scale_id + )) + return self._lines[-1] + + def create_bar( + self, + name: str = '', + up_color: str = '#26a69a', + down_color: str = '#ef5350', + open_visible: bool = True, + thin_bars: bool = True, + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: Union[str, List[str]] = ['┌', '└'], + price_scale_id: Optional[str] = None + ) -> Bar: + """ + Creates and returns a Bar object. + """ + if not isinstance(legend_symbol, (str, list)): + raise TypeError("legend_symbol must be a string or list of strings for Bar series.") + if isinstance(legend_symbol, list) and not all(isinstance(symbol, str) for symbol in legend_symbol): + raise TypeError("Each item in legend_symbol list must be a string for Bar series.") + + return Bar( + self, name, up_color, down_color, open_visible, thin_bars, + price_line, price_label, group, legend_symbol, price_scale_id + ) + + def create_custom_candle( + self, + name: str = '', + up_color: str = None, + down_color: str = None, + border_up_color='rgba(0,255,0,1)', + border_down_color='rgba(255,0,0,1)', + wick_up_color='rgba(0,255,0,1)', + wick_down_color='rgba(255,0,0,1)', + wick_visible: bool = True, + border_visible: bool = True, + bar_width: float = 0.8, + rounded_radius: Union[float, int] = 100, + shape: Literal[CANDLE_SHAPE] = "Rectangle", + combineCandles: int = 1, + vp_sections: int = 4, + line_width: int = 1, + line_style: LINE_STYLE = 'solid', + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: Union[str, List[str]] = ['⑃', '⑂'], + price_scale_id: Optional[str] = None, + ) -> CustomCandle: + """ + Creates and returns a CustomCandle object. + """ + # Validate that legend_symbol is either a string or a list of two strings + if not isinstance(legend_symbol, (str, list)): + raise TypeError("legend_symbol must be a string or list of strings for CustomCandle series.") + if isinstance(legend_symbol, list) and len(legend_symbol) != 2: + raise ValueError("legend_symbol list must contain exactly two symbols for CustomCandle series.") + + return CustomCandle( + self, + name=name, + up_color=up_color or border_up_color, + down_color=down_color or border_down_color, + border_up_color=border_up_color or up_color, + border_down_color=border_down_color or down_color, + wick_up_color=wick_up_color or border_up_color or border_up_color, + wick_down_color=wick_down_color or border_down_color or border_down_color, + wick_visible=wick_visible, + border_visible=border_visible, + bar_width=bar_width, + radius=rounded_radius, + shape=shape, + combineCandles=combineCandles, + vp_sections = vp_sections, + line_style= line_style, + line_width= line_width, + price_line=price_line, + price_label=price_label, + group=group, + legend_symbol=legend_symbol, + price_scale_id=price_scale_id, + ) + def create_htf_candle( + self, + name: str = '', + up_color: str = None, + down_color: str = None, + border_up_color = 'rgba(0,255,0,1)', + border_down_color = 'rgba(255,0,0,1)', + wick_up_color = None, + wick_down_color = None, + wick_visible: bool = True, + border_visible: bool = True, + radius: Union[float,int] = 3, + multiple: int = 5, + price_line: bool = True, + price_label: bool = True, + group: str = '', + legend_symbol: Union[str, List[str]] =['⑃', '⑂'], + price_scale_id: Optional[str] = None + ) -> CustomCandle: + """ + Creates and returns a CustomCandle object. + """ + # Validate that legend_symbol is either a string or a list of two strings + if not isinstance(legend_symbol, (str, list)): + raise TypeError("legend_symbol must be a string or list of strings for CustomCandle series.") + if isinstance(legend_symbol, list) and len(legend_symbol) != 2: + raise ValueError("legend_symbol list must contain exactly two symbols for CustomCandle series.") + + return HTFCandle( + self, + name=name, + up_color=up_color or border_up_color, + down_color=down_color or border_down_color, + border_up_color =border_up_color or up_color, + border_down_color =border_down_color or down_color, + wick_up_color =border_up_color or border_up_color, + wick_down_color =border_down_color or border_down_color, + wick_visible=wick_visible, + border_visible=border_visible, + radius=radius, + multiple=multiple, + price_line=price_line, + price_label=price_label, + group=group, + legend_symbol=legend_symbol, + price_scale_id=price_scale_id + ) + def create_chandelier( + self, + interval: int = 7, + wick_width: int = 2, + wick_style: LINE_STYLE = 'solid', + alpha: float = 1.0, + colors: Optional[List[str]] = None, + df: pd.DataFrame = None + ) -> 'ChandelierSeries': + """ + Creates and returns a ChandelierSeries object with dynamic color calculation. + + Args: + - interval: Number of bars after which a new Chandelier candle is created. + - wick_width: Width of the chandelier's wicks. + - wick_style: Style of the chandelier's wicks. + - alpha: Transparency level of the candle colors. + - colors: Optional list of colors for dynamic gradient. + - df: Optional DataFrame to initialize ChandelierSeries with historical data. + + Returns: + - ChandelierSeries: A newly created ChandelierSeries object. + """ + if df.empty: + if not self.candle_data.empty: + df = self.candle_data + # Initialize ChandelierSeries with provided parameters + + if len(df) > 1: + + chandelier_series = ChandelierSeries( + chart=self, # Assuming `self` is the chart or manager handling ChandelierSeries + interval=interval, + wick_width=wick_width, + wick_style=wick_style, + alpha=alpha, + colors=colors, + df=df # Pass initial DataFrame if needed + ) + return chandelier_series + + def create_candle_bar( + self,chart, name: str = '', up_color: str = '#26a69a', down_color: str = '#ef5350', + open_visible: bool = True, thin_bars: bool = True, price_line: bool = True, + price_label: bool = True, price_scale_id: Optional[str] = None + ) -> CandleBar: + """ + Creates and returns a CandleBar object. + """ + return CandleBar( + chart=self, name=name, up_color=up_color, down_color=down_color, + open_visible=open_visible, thin_bars=thin_bars + ) + def create_volume_profile(self, side: str = 'left', sections: int = 10, width: float = 0.1, + up_color: str = 'rgba(0,255,0,0.333)', down_color: str = 'rgba(255,0,0,0.333)', + border_up_color: str = 'rgba(0,255,0,1)', border_down_color: str = 'rgba(255,0,0,1)', + fibonacci_profile: bool = False, fibonacci_levels: bool = False,start_index: Optional[int]= None, + end_index: Optional[int]= None): + """ + Creates a VolumeProfile for the specified series on this chart. + + :param side: Boolean indicating the side to render the profile on (True for left, False for right). + :param sections: Number of sections in the volume profile. + :param width: Width of each section. + :param up_color: The color for upward volume. + :param down_color: The color for downward volume. + :param border_up_color: The border color for upward volume bars. + :param border_down_color: The border color for downward volume bars. + :return: VolumeProfile instance associated with the specified series. + """ + print(f"[create_volume_profile] side: {side}, sections: {sections}, width: {width}, up_color: {up_color},\ + down_color: {down_color}, border_up_color: {border_up_color}, border_down_color: {border_down_color}") + # Create and return a new VolumeProfile instance with all parameters + return VolumeProfile(self, side, sections, width, up_color, down_color, border_up_color, + border_down_color, fibonacci_profile, fibonacci_levels, start_index, end_index) + + def create_delta_profile(self, side: str ='right', up_color: str = 'rgba(0,255,0,1)', down_color: str = 'rgba(255,0,0,1)'): + """ + Creates a DeltaProfile for the specified series on this chart. + + :param section_size: The number of sections (bins) in the delta profile. + :param side: The side ('right' or 'left') to display the delta profile. + :return: DeltaProfile instance associated with the specified series. + """ + # Create and return a new DeltaProfile instance for the specified series + return DeltaProfile(self, side, up_color, down_color) + + def lines(self) -> List[Line]: + """ + Returns all lines for the chart. + """ + return self._lines.copy() + + def set_visible_range(self, start_time: TIME, end_time: TIME): + self.run_script(f''' + {self.id}.chart.timeScale().setVisibleRange({{ + from: {pd.to_datetime(start_time).timestamp()}, + to: {pd.to_datetime(end_time).timestamp()} + }}) + ''') + + def resize(self, width: Optional[float] = None, height: Optional[float] = None): + """ + Resizes the chart within the window. + Dimensions should be given as a float between 0 and 1. + """ + self._width = width if width is not None else self._width + self._height = height if height is not None else self._height + self.run_script(f''' + {self.id}.scale.width = {self._width} + {self.id}.scale.height = {self._height} + {self.id}.reSize() + ''') + + def time_scale(self, right_offset: int = 0, min_bar_spacing: float = 0.5, + visible: bool = True, time_visible: bool = True, seconds_visible: bool = False, + border_visible: bool = True, border_color: Optional[str] = None): + """ + Options for the timescale of the chart. + """ + self.run_script(f'''{self.id}.chart.applyOptions({{timeScale: {js_json(locals())}}})''') + + def layout(self, background_color: str = '#000000', text_color: Optional[str] = None, + font_size: Optional[int] = None, font_family: Optional[str] = None): + """ + Global layout options for the chart. + """ + self.run_script(f""" + document.getElementById('container').style.backgroundColor = '{background_color}' + {self.id}.chart.applyOptions({{ + layout: {{ + background: {{color: "{background_color}"}}, + {f'textColor: "{text_color}",' if text_color else ''} + {f'fontSize: {font_size},' if font_size else ''} + {f'fontFamily: "{font_family}",' if font_family else ''} + }}}})""") + + def grid(self, vert_enabled: bool = True, horz_enabled: bool = True, + color: str = 'rgba(29, 30, 38, 5)', style: LINE_STYLE = 'solid'): + """ + Grid styling for the chart. + """ + self.run_script(f""" + {self.id}.chart.applyOptions({{ + grid: {{ + vertLines: {{ + visible: {jbool(vert_enabled)}, + color: "{color}", + style: {as_enum(style, LINE_STYLE)}, + }}, + horzLines: {{ + visible: {jbool(horz_enabled)}, + color: "{color}", + style: {as_enum(style, LINE_STYLE)}, + }}, + }} + }})""") + + def crosshair( + self, + mode: CROSSHAIR_MODE = 'normal', + vert_visible: bool = True, + vert_width: int = 1, + vert_color: Optional[str] = None, + vert_style: LINE_STYLE = 'large_dashed', + vert_label_background_color: str = 'rgb(46, 46, 46)', + horz_visible: bool = True, + horz_width: int = 1, + horz_color: Optional[str] = None, + horz_style: LINE_STYLE = 'large_dashed', + horz_label_background_color: str = 'rgb(55, 55, 55)' + ): + """ + Crosshair formatting for its vertical and horizontal axes. + """ + self.run_script(f''' + {self.id}.chart.applyOptions({{ + crosshair: {{ + mode: {as_enum(mode, CROSSHAIR_MODE)}, + vertLine: {{ + visible: {jbool(vert_visible)}, + width: {vert_width}, + {f'color: "{vert_color}",' if vert_color else ''} + style: {as_enum(vert_style, LINE_STYLE)}, + labelBackgroundColor: "{vert_label_background_color}" + }}, + horzLine: {{ + visible: {jbool(horz_visible)}, + width: {horz_width}, + {f'color: "{horz_color}",' if horz_color else ''} + style: {as_enum(horz_style, LINE_STYLE)}, + labelBackgroundColor: "{horz_label_background_color}" + }} + }} + }})''') + + def watermark(self, text: str, font_size: int = 44, color: str = 'rgba(180, 180, 200, 0.5)'): + """ + Adds a watermark to the chart. + """ + self.run_script(f''' + {self.id}.chart.applyOptions({{ + watermark: {{ + visible: true, + horzAlign: 'center', + vertAlign: 'center', + ...{js_json(locals())} + }} + }})''') + + def legend(self, visible: bool = False, ohlc: bool = True, percent: bool = True, lines: bool = True, + color: str = 'rgb(191, 195, 203)', font_size: int = 11, font_family: str = 'Monaco', + text: str = '', color_based_on_candle: bool = False): + """ + Configures the legend of the chart. + """ + l_id = f'{self.id}.legend' + if not visible: + self.run_script(f''' + {l_id}.div.style.display = "none" + {l_id}.ohlcEnabled = false + {l_id}.percentEnabled = false + {l_id}.linesEnabled = false + ''') + return + self.run_script(f''' + {l_id}.div.style.display = 'flex' + {l_id}.ohlcEnabled = {jbool(ohlc)} + {l_id}.percentEnabled = {jbool(percent)} + {l_id}.linesEnabled = {jbool(lines)} + {l_id}.colorBasedOnCandle = {jbool(color_based_on_candle)} + {l_id}.div.style.color = '{color}' + {l_id}.color = '{color}' + {l_id}.div.style.fontSize = '{font_size}px' + {l_id}.div.style.fontFamily = '{font_family}' + {l_id}.text.innerText = '{text}' + ''') + + def spinner(self, visible): + self.run_script(f"{self.id}.spinner.style.display = '{'block' if visible else 'none'}'") + + def hotkey(self, modifier_key: Literal['ctrl', 'alt', 'shift', 'meta', None], + keys: Union[str, tuple, int], func: Callable): + if not isinstance(keys, tuple): + keys = (keys,) + for key in keys: + key = str(key) + if key.isalnum() and len(key) == 1: + key_code = f'Digit{key}' if key.isdigit() else f'Key{key.upper()}' + key_condition = f'event.code === "{key_code}"' + else: + key_condition = f'event.key === "{key}"' + if modifier_key is not None: + key_condition += f'&& event.{modifier_key}Key' + + self.run_script(f''' + {self.id}.commandFunctions.unshift((event) => {{ + if ({key_condition}) {{ + event.preventDefault() + window.callbackFunction(`{modifier_key, keys}_~_{key}`) + return true + }} + else return false + }})''') + self.win.handlers[f'{modifier_key, keys}'] = func + + def create_table( + self, + width: NUM, + height: NUM, + headings: tuple, + widths: Optional[tuple] = None, + alignments: Optional[tuple] = None, + position: FLOAT = 'left', + draggable: bool = False, + background_color: str = '#121417', + border_color: str = 'rgb(70, 70, 70)', + border_width: int = 1, + heading_text_colors: Optional[tuple] = None, + heading_background_colors: Optional[tuple] = None, + return_clicked_cells: bool = False, + func: Optional[Callable] = None + ) -> Table: + args = locals() + del args['self'] + return self.win.create_table(*args.values()) + + def screenshot(self) -> bytes: + """ + Takes a screenshot. This method can only be used after the chart window is visible. + :return: a bytes object containing a screenshot of the chart. + """ + serial_data = self.win.run_script_and_get(f'{self.id}.chart.takeScreenshot().toDataURL()') + return b64decode(serial_data.split(',')[1]) + + def create_subchart(self, position: FLOAT = 'left', width: float = 0.5, height: float = 0.5, + sync: Optional[Union[str, bool]] = None, scale_candles_only: bool = False, + sync_crosshairs_only: bool = False, + toolbox: bool = False) -> 'AbstractChart': + if sync is True: + sync = self.id + args = locals() + del args['self'] + return self.win.create_subchart(*args.values()) diff --git a/lightweight_charts_/js/bundle.js b/lightweight_charts_/js/bundle.js new file mode 100644 index 0000000..93fc203 --- /dev/null +++ b/lightweight_charts_/js/bundle.js @@ -0,0 +1 @@ +var Lib=function(e,t){"use strict";function i(e){if(void 0===e)throw new Error("Value is undefined");return e}class o{_chart=void 0;_series=void 0;requestUpdate(){this._requestUpdate&&this._requestUpdate()}_requestUpdate;attached({chart:e,series:t,requestUpdate:i}){this._chart=e,this._series=t,this._series.subscribeDataChanged(this._fireDataUpdated),this._requestUpdate=i,this.requestUpdate()}detached(){this._chart=void 0,this._series=void 0,this._requestUpdate=void 0}get chart(){return i(this._chart)}get series(){return i(this._series)}_fireDataUpdated(e){this.dataUpdated&&this.dataUpdated(e)}}function s(e,t){if(e.startsWith("#"))return function(e,t){if(e=e.replace(/^#/,""),!/^([0-9A-F]{3}){1,2}$/i.test(e))throw new Error("Invalid hex color format.");const[i,o,s]=(e=>3===e.length?[parseInt(e[0]+e[0],16),parseInt(e[1]+e[1],16),parseInt(e[2]+e[2],16)]:[parseInt(e.slice(0,2),16),parseInt(e.slice(2,4),16),parseInt(e.slice(4,6),16)])(e);return`rgba(${i}, ${o}, ${s}, ${t})`}(e,t);if(e.startsWith("rgba")||e.startsWith("rgb"))return e.replace(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/,`rgba($1, $2, $3, ${t})`);throw new Error("Unsupported color format. Use hex, rgb, or rgba.")}class n{numbers;cache;constructor(e){this.numbers=e,this.cache=new Map}findClosestIndex(e,t){const i=`${e}:${t}`;if(this.cache.has(i))return this.cache.get(i);const o=this._performSearch(e,t);return this.cache.set(i,o),o}_performSearch(e,t){let i=0,o=this.numbers.length-1;if(e<=this.numbers[0].time)return 0;if(e>=this.numbers[o].time)return o;for(;i<=o;){const t=Math.floor((i+o)/2),s=this.numbers[t].time;if(s===e)return t;s>e?o=t-1:i=t+1}return"left"===t?i:o}}function r(e,t){if(e._isDecorated)return console.warn("Series is already decorated. Skipping decoration."),e;e._isDecorated=!0;const i=[],o=e.setData.bind(e),s=[];let n=null;const r=e.detachPrimitive?.bind(e),a=e.attachPrimitive?.bind(e);function l(i){const o=s.indexOf(i);if(-1!==o&&(s.splice(o,1),n===i&&(n=null),r&&r(i),t)){t.findLegendPrimitive(e,i)&&(t.removeLegendPrimitive(i),console.log(`Removed primitive of type "${i.constructor.name}" from legend.`))}}function c(){for(console.log("Detaching all primitives.");s.length>0;){l(s.pop())}console.log("All primitives detached.")}return Object.assign(e,{setData:function(e){o(e);for(const t of i)t.setData(e);console.log("Data updated on series and peers.")},addPeer:function(e){i.push(e)},removePeer:function(e){const t=i.indexOf(e);-1!==t&&i.splice(t,1)},peers:i,primitives:s,attachPrimitive:function(i,o,r=!0,d=!1){const h=i.constructor.type||i.constructor.name;if(r)c();else{const e=s.findIndex((e=>e.constructor.type===h));-1!==e&&l(s[e])}a&&a(i),s.push(i),n=i,console.log(`Primitive of type "${h}" attached.`),t&&d&&t.addLegendPrimitive(e,i,o)},detachPrimitive:l,detachPrimitives:c,decorated:!0,get primitive(){return n}})}function a(e){const t=e.options();return"lineColor"in t||"color"in t}function l(e,t){return(e=>void 0!==e.primitives)(e)?e:(console.log("Decorating the series dynamically."),r(e,t))}class c extends o{static type="Fill Area";_paneViews;_originSeries;_destinationSeries;_bandsData=[];options;_timeIndices;constructor(e,t,i){super();const o="#0000FF",r="#FF0000",l=a(e)?s(e.options().lineColor||e.options().color||o,.3):s(o,.3),c=a(t)?s(t.options().lineColor||t.options().color||r,.3):s(r,.3);this.options={...p,...i,originColor:i.originColor??l,destinationColor:i.destinationColor??c},this._paneViews=[new h(this)],this._timeIndices=new n([]),this._originSeries=e,this._destinationSeries=t}updateAllViews(){this._paneViews.forEach((e=>e.update()))}applyOptions(e){const t="#0000FF",i="#FF0000",o=a(this._originSeries)?s(this._originSeries.options().lineColor||this._originSeries.options().color||t,.3):s(t,.3),n=a(this._destinationSeries)?s(this._destinationSeries.options().lineColor||this._destinationSeries.options().color||i,.3):s(i,.3);this.options={...this.options,...e,originColor:e.originColor||o,destinationColor:e.destinationColor||n},this.calculateBands(),this.updateAllViews(),super.requestUpdate(),console.log("FillArea options updated:",this.options)}paneViews(){return this._paneViews}attached(e){super.attached(e),this.dataUpdated("full")}dataUpdated(e){if(this.calculateBands(),"full"===e){const e=this._originSeries.data();this._timeIndices=new n([...e])}}calculateBands(){const e=this._originSeries.data(),t=this._destinationSeries.data(),i=this._alignDataLengths([...e],[...t]),o=[];for(let e=0;eo){const e=t[o-1];for(;t.lengthi){const t=e[i-1];for(;e.lengthe.lower)).slice(n,r+1)),maxValue:Math.max(...this._bandsData.map((e=>e.upper)).slice(n,r+1))};return{priceRange:{minValue:a.minValue,maxValue:a.maxValue}}}}class d{_viewData;_options;constructor(e){this._viewData=e,this._options=e.options}draw(){}drawBackground(e){const t=this._viewData.data,i=this._options;t.length<2||e.useBitmapCoordinateSpace((e=>{const o=e.context;o.scale(e.horizontalPixelRatio,e.verticalPixelRatio);let s=!1,n=0;for(let e=0;e=n;i--)o.lineTo(t[i].x,t[i].destination);o.closePath(),o.fill()}o.beginPath(),o.moveTo(r.x,r.origin),o.fillStyle=r.isOriginAbove?i.originColor||"rgba(0, 0, 0, 0)":i.destinationColor||"rgba(0, 0, 0, 0)",n=e,s=!0}if(o.lineTo(a.x,a.origin),e===t.length-2||a.isOriginAbove!==r.isOriginAbove){for(let i=e+1;i>=n;i--)o.lineTo(t[i].x,t[i].destination);o.closePath(),o.fill(),s=!1}}i.lineWidth&&(o.lineWidth=i.lineWidth,o.strokeStyle=i.originColor||"rgba(0, 0, 0, 0)",o.stroke())}))}}class h{_source;_data;constructor(e){this._source=e,this._data={data:[],options:this._source.options}}update(){const e=this._source.chart.timeScale();this._data.data=this._source._bandsData.map((t=>({x:e.timeToCoordinate(t.time),origin:this._source._originSeries.priceToCoordinate(t.origin),destination:this._source._destinationSeries.priceToCoordinate(t.destination),isOriginAbove:t.origin>t.destination}))),this._data.options=this._source.options}renderer(){return new d(this._data)}}const p={originColor:null,destinationColor:null,lineWidth:null};function u(e,t){let i,o;if(void 0!==e.close){i=e.close}else void 0!==e.value&&(i=e.value);if(void 0!==t.close){o=t.close}else void 0!==t.value&&(o=t.value);if(void 0!==i&&void 0!==o){if(i{e&&(window.cursor=e),document.body.style.cursor=window.cursor},window.cursor="default",window.textBoxFocused=!1}const _='\n\n \n \n\n',y='\n\n \n \n \n\n',v=new Map;function b(e){return v.get(e)||null}class f{handler;div;seriesContainer;ohlcEnabled=!1;percentEnabled=!1;linesEnabled=!1;colorBasedOnCandle=!1;text;candle;_items=[];_lines=[];_groups=[];constructor(e){this.handler=e,this.div=document.createElement("div"),this.div.classList.add("legend"),this.seriesContainer=document.createElement("div"),this.text=document.createElement("span"),this.candle=document.createElement("div"),this.setupLegend(),this.legendHandler=this.legendHandler.bind(this),e.chart.subscribeCrosshairMove(this.legendHandler)}setupLegend(){this.div.style.maxWidth=100*this.handler.scale.width-8+"vw",this.div.style.display="none";const e=document.createElement("div");e.style.display="flex",e.style.flexDirection="row",this.seriesContainer.classList.add("series-container"),this.text.style.lineHeight="1.8",e.appendChild(this.seriesContainer),this.div.appendChild(this.text),this.div.appendChild(this.candle),this.div.appendChild(e),this.handler.div.appendChild(this.div)}legendItemFormat(e,t){return"number"!=typeof e||isNaN(e)?"-":e.toFixed(t).toString().padStart(8," ")}shorthandFormat(e){const t=Math.abs(e);return t>=1e6?(e/1e6).toFixed(1)+"M":t>=1e3?(e/1e3).toFixed(1)+"K":e.toString().padStart(8," ")}createSvgIcon(e){const t=document.createElement("div");t.innerHTML=e.trim();return t.querySelector("svg")}addLegendItem(e){const t=this.mapToSeries(e);if(t.group)return this.addItemToGroup(t,t.group);{const e=this.makeSeriesRow(t,this.seriesContainer);return this._lines.push(t),this._items.push(t),e}}addLegendPrimitive(e,t,i){const o=i||t.constructor.name,s=this._lines.find((t=>t.series===e));if(!s)return void console.warn(`Parent series not found in legend for primitive: ${o}`);let n=this.seriesContainer.querySelector(`[data-series-id="${s.name}"] .primitives-container`);n||(n=document.createElement("div"),n.classList.add("primitives-container"),n.style.display="none",n.style.marginLeft="20px",n.style.flexDirection="column",s.row.insertAdjacentElement("afterend",n));const r=Array.from(n.children).find((e=>e.getAttribute("data-primitive-type")===o));if(r)return console.warn(`Primitive "${o}" already exists under the parent series.`),r;const a=document.createElement("div");a.classList.add("legend-primitive-row"),a.setAttribute("data-primitive-type",o),a.style.display="flex",a.style.justifyContent="space-between",a.style.marginTop="4px";const l=document.createElement("span");l.innerText=o;const c=document.createElement("div");c.style.cursor="pointer",c.style.display="flex",c.style.alignItems="center";const d=this.createSvgIcon(_),h=this.createSvgIcon(y);c.appendChild(d.cloneNode(!0));let p=!0;return c.addEventListener("click",(()=>{p=!p,c.innerHTML="",c.appendChild(p?d.cloneNode(!0):h.cloneNode(!0)),this.togglePrimitive(t,p)})),a.appendChild(l),a.appendChild(c),n.appendChild(a),n.children.length>0&&(n.style.display="block"),a}togglePrimitive(e,t){const i=e.options;if(!i)return void console.warn("Primitive has no options to update.");const o="_originalColors";e[o]||(e[o]={});const s=e[o],n={};for(const e of Object.keys(i))e.toLowerCase().includes("color")&&(t?n[e]=s[e]||i[e]:(s[e]||(s[e]=i[e]),n[e]="rgba(0,0,0,0)"));Object.keys(n).length>0&&(console.log(`Updating visibility for primitive: ${e.constructor.name}`),e.applyOptions(n),t&&delete e[o])}findLegendPrimitive(e,t){const i=this._lines.find((t=>t.series===e))?.row;if(!i)return null;const o=i.querySelector(".primitives-container");if(!o)return null;const s=t.constructor.type||t.constructor.name;return Array.from(o.children).find((e=>e.getAttribute("data-primitive-type")===s))}removeLegendPrimitive(e){const t=e.constructor.type||e.constructor.name;console.log(`Removing legend entry for primitive: ${t}`);const i=Array.from(this.seriesContainer.children);for(const e of i)if(e.textContent?.includes(`Primitive: ${t}`)){this.seriesContainer.removeChild(e),console.log(`Legend entry for primitive "${t}" removed.`);break}}mapToSeries(e){return{name:e.name,series:e.series,group:e.group||void 0,legendSymbol:e.legendSymbol||[],colors:e.colors||["#000"],seriesType:e.seriesType||"Line",div:document.createElement("div"),row:document.createElement("div"),toggle:document.createElement("div"),extraData:e.extraData||null}}addItemToGroup(e,t){let i=this._groups.find((e=>e.name===t));return i?(i.seriesList.push(e),this.makeSeriesRow(e,i.div),i.row):this.makeSeriesGroup(t,[e])}makeSeriesGroup(e,t){let i=this._groups.find((t=>t.name===e));if(i)return i.seriesList.push(...t),t.forEach((e=>this.makeSeriesRow(e,i.div))),i.row;{const i={name:e,seriesList:t,subGroups:[],div:document.createElement("div"),row:document.createElement("div"),toggle:document.createElement("div")};return this._groups.push(i),this.renderGroup(i,this.seriesContainer),i.row}}makeSeriesRow(e,t){const i=document.createElement("div");i.classList.add("legend-series-row"),i.style.display="flex",i.style.alignItems="center",i.style.justifyContent="space-between",i.style.marginBottom="4px";const o=document.createElement("div");o.classList.add("series-info"),o.style.flex="1";if(["Bar","Candlestick"].includes(e.seriesType||"")){const t="-",i="-",s=e.legendSymbol[0]||"▨",n=e.legendSymbol[1]||s,r=e.colors[0]||"#00FF00",a=e.colors[1]||"#FF0000";o.innerHTML=`\n ${s}\n ${n}\n ${e.name}: O ${t}, \n C ${i}\n `}else o.innerHTML=e.legendSymbol.map(((t,i)=>`${t}`)).join(" ")+` ${e.name}`;const s=document.createElement("div");s.classList.add("legend-toggle-switch"),s.style.cursor="pointer",s.style.display="flex",s.style.alignItems="center";const n=this.createSvgIcon(_),r=this.createSvgIcon(y);s.appendChild(n.cloneNode(!0));let a=!0;s.addEventListener("click",(t=>{a=!a,e.series.applyOptions({visible:a}),s.innerHTML="",s.appendChild(a?n.cloneNode(!0):r.cloneNode(!0)),s.setAttribute("aria-pressed",a.toString()),s.classList.toggle("inactive",!a),t.stopPropagation()})),s.setAttribute("role","button"),s.setAttribute("aria-label",`Toggle visibility for ${e.name}`),s.setAttribute("aria-pressed",a.toString()),i.appendChild(o),i.appendChild(s),t.appendChild(i),i.addEventListener("contextmenu",(e=>{e.preventDefault()}));const l={...e,div:o,row:i,toggle:s};return this._lines.push(l),i}deleteLegendEntry(e,t){if(t&&!e){const e=this._groups.findIndex((e=>e.name===t));if(-1!==e){const i=this._groups[e];this.seriesContainer.removeChild(i.row),this._groups.splice(e,1),this._items=this._items.filter((e=>e!==i)),console.log(`Group "${t}" removed.`)}else console.warn(`Legend group with name "${t}" not found.`)}else if(e){let i=!1;if(t){const o=this._groups.find((e=>e.name===t));if(o){const s=o.seriesList.findIndex((t=>t.name===e));-1!==s&&(o.seriesList.splice(s,1),0===o.seriesList.length?(this.seriesContainer.removeChild(o.row),this._groups=this._groups.filter((e=>e!==o)),this._items=this._items.filter((e=>e!==o)),console.log(`Group "${t}" is empty and has been removed.`)):this.renderGroup(o,this.seriesContainer),i=!0,console.log(`Series "${e}" removed from group "${t}".`))}else console.warn(`Legend group with name "${t}" not found.`)}if(!i){const t=this._lines.findIndex((t=>t.name===e));if(-1!==t){const o=this._lines[t];this.seriesContainer.removeChild(o.row),this._lines.splice(t,1),this._items=this._items.filter((e=>e!==o)),i=!0,console.log(`Series "${e}" removed.`)}}i||console.warn(`Legend item with name "${e}" not found.`)}else console.warn("No seriesName or groupName provided for deletion.")}getGroupOfSeries(e){for(const t of this._groups){const i=this.findGroupOfSeriesRecursive(t,e);if(i)return i}}findGroupOfSeriesRecursive(e,t){for(const i of e.seriesList)if(i.series===t)return e.name;for(const i of e.subGroups){const e=this.findGroupOfSeriesRecursive(i,t);if(e)return e}}moveSeriesToGroup(e,t){let i=this._lines.findIndex((t=>t.name===e)),o=null;if(-1!==i)o=this._lines[i];else for(const t of this._groups){const i=t.seriesList.findIndex((t=>t.name===e));if(-1!==i){o=t.seriesList[i],t.seriesList.splice(i,1),0===t.seriesList.length?(this.seriesContainer.removeChild(t.row),this._groups=this._groups.filter((e=>e!==t)),this._items=this._items.filter((e=>e!==t)),console.log(`Group "${t.name}" is empty and has been removed.`)):this.renderGroup(t,this.seriesContainer);break}}if(!o)return void console.warn(`Series "${e}" not found in legend.`);-1!==i?(this.seriesContainer.removeChild(o.row),this._lines.splice(i,1),this._items=this._items.filter((e=>e!==o))):this._items=this._items.filter((e=>e!==o));let s=this.findGroup(t);s?(s.seriesList.push(o),this.makeSeriesRow(o,s.div)):(s={name:t,seriesList:[o],subGroups:[],div:document.createElement("div"),row:document.createElement("div"),toggle:document.createElement("div")},this._groups.push(s),this.renderGroup(s,this.seriesContainer)),this._items.push(o),console.log(`Series "${e}" moved to group "${t}".`)}renderGroup(e,t){e.row.innerHTML="",e.row.style.display="flex",e.row.style.flexDirection="column",e.row.style.width="100%";const i=document.createElement("div");i.classList.add("group-header"),i.style.display="flex",i.style.alignItems="center",i.style.justifyContent="space-between",i.style.cursor="pointer";const o=document.createElement("span");o.style.fontWeight="bold",o.innerHTML=e.seriesList.map((e=>e.legendSymbol.map(((t,i)=>`${t}`)).join(" "))).join(" ")+` ${e.name}`;const s=document.createElement("span");s.classList.add("toggle-button"),s.style.marginLeft="auto",s.style.fontSize="1.2em",s.style.cursor="pointer",s.innerHTML="⌲",s.setAttribute("aria-expanded","true"),s.addEventListener("click",(t=>{t.stopPropagation(),"none"===e.div.style.display?(e.div.style.display="block",s.innerHTML="⌲",s.setAttribute("aria-expanded","true")):(e.div.style.display="none",s.innerHTML="☰",s.setAttribute("aria-expanded","false"))})),i.appendChild(o),i.appendChild(s),e.row.appendChild(i),e.div=document.createElement("div"),e.div.style.display="block",e.div.style.marginLeft="10px";for(const t of e.seriesList)this.makeSeriesRow(t,e.div);for(const t of e.subGroups){const i=document.createElement("div");i.style.display="flex",i.style.flexDirection="column",i.style.paddingLeft="5px",this.renderGroup(t,i),e.div.appendChild(i)}e.row.appendChild(e.div),t.contains(e.row)||t.appendChild(e.row),e.row.oncontextmenu=e=>{e.preventDefault()}}legendHandler(e,t=!1){if(!this.ohlcEnabled&&!this.linesEnabled&&!this.percentEnabled)return;const i=this.handler.series.options();if(!e.time)return this.candle.style.color="transparent",void(this.candle.innerHTML=this.candle.innerHTML.replace(i.upColor,"").replace(i.downColor,""));let o,s=null;if(t){const t=this.handler.chart.timeScale();let i=t.timeToCoordinate(e.time);i&&(s=t.coordinateToLogical(i.valueOf())),s&&(o=this.handler.series.dataByIndex(s.valueOf()))}else o=e.seriesData.get(this.handler.series);this.candle.style.color="";let n='';if(o&&(this.ohlcEnabled&&(n+=`O ${this.legendItemFormat(o.open,this.handler.precision)} `,n+=`| H ${this.legendItemFormat(o.high,this.handler.precision)} `,n+=`| L ${this.legendItemFormat(o.low,this.handler.precision)} `,n+=`| C ${this.legendItemFormat(o.close,this.handler.precision)} `),this.percentEnabled)){const e=(o.close-o.open)/o.open*100,t=e>0?i.upColor:i.downColor,s=`${e>=0?"+":""}${e.toFixed(2)} %`;n+=this.colorBasedOnCandle?`| ${s}`:`| ${s}`}this.candle.innerHTML=n+"",this.updateGroupDisplay(e,s,t),this.updateSeriesDisplay(e,s,t)}updateSeriesDisplay(e,t,i){this._lines&&this._lines.length?this._lines.forEach((t=>{const i=e.seriesData.get(t.series)||b(t.series);if(!i)return void(t.div.innerHTML=`${t.name}: -`);const o=t.seriesType||"Line",s=t.series.options().priceFormat;if("Line"===o||"Area"===o){const e=i;if(null==e.value)return void(t.div.innerHTML=`${t.name}: -`);const o=this.legendItemFormat(e.value,s.precision);t.div.innerHTML=`\n ${t.legendSymbol[0]||"▨"} \n ${t.name}: ${o}`}else if("Bar"===o||"customCandle"===o||"htfCandle"===o){const{open:e,close:o}=i;if(null==e||null==o)return void(t.div.innerHTML=`${t.name}: -`);const n=this.legendItemFormat(e,s.precision),r=this.legendItemFormat(o,s.precision),a=o>e,l=a?t.colors[0]:t.colors[1],c=a?t.legendSymbol[0]:t.legendSymbol[1];t.div.innerHTML=`\n ${c||"▨"}\n ${t.name}: \n O ${n}, \n C ${r}`}})):console.error("No lines available to update legend.")}updateGroupDisplay(e,t,i){this._groups.forEach((t=>{this.linesEnabled?(t.row.style.display="flex",t.seriesList.forEach((t=>{const i=e.seriesData.get(t.series)||b(t.series);if(!i)return void(t.div.innerHTML=`${t.name}: -`);const o=t.seriesType||"Line",s=t.name,n=t.series.options().priceFormat;if(["Bar","customCandle","htfCandle"].includes(o)){const{open:e,close:o,high:r,low:a}=i;if(null==e||null==o||null==r||null==a)return void(t.div.innerHTML=`${s}: -`);const l=this.legendItemFormat(e,n.precision),c=this.legendItemFormat(o,n.precision),d=o>e,h=d?t.colors[0]:t.colors[1],p=d?t.legendSymbol[0]:t.legendSymbol[1];t.div.innerHTML=`\n ${p||"▨"}\n ${s}: \n O ${l}, \n C ${c}\n `}else{const e="value"in i?i.value:void 0;if(null==e)return void(t.div.innerHTML=`${s}: -`);const o=this.legendItemFormat(e,n.precision),r=t.colors[0],a=t.legendSymbol[0]||"▨";t.div.innerHTML=`\n ${a}\n ${s}: ${o}\n `}}))):t.row.style.display="none"}))}findGroup(e,t=this._groups){for(const i of t){if(i.name===e)return i;const t=this.findGroup(e,i.subGroups);if(t)return t}}}const w={lineColor:"#1E80F0",lineStyle:t.LineStyle.Solid,width:4};var C;!function(e){e[e.NONE=0]="NONE",e[e.HOVERING=1]="HOVERING",e[e.DRAGGING=2]="DRAGGING",e[e.DRAGGINGP1=3]="DRAGGINGP1",e[e.DRAGGINGP2=4]="DRAGGINGP2",e[e.DRAGGINGP3=5]="DRAGGINGP3",e[e.DRAGGINGP4=6]="DRAGGINGP4"}(C||(C={}));class x extends o{_paneViews=[];_options;_points=[];_state=C.NONE;_startDragPoint=null;_latestHoverPoint=null;static _mouseIsDown=!1;static hoveredObject=null;static lastHoveredObject=null;_listeners=[];constructor(e){super(),this._options={...w,...e}}updateAllViews(){this._paneViews.forEach((e=>e.update()))}paneViews(){return this._paneViews}applyOptions(e){this._options={...this._options,...e},this.requestUpdate()}updatePoints(...e){for(let t=0;ti.name===e&&i.listener===t));this._listeners.splice(this._listeners.indexOf(i),1)}_handleHoverInteraction(e){if(this._latestHoverPoint=e.point,x._mouseIsDown)this._handleDragInteraction(e);else if(this._mouseIsOverDrawing(e)){if(this._state!=C.NONE)return;this._moveToState(C.HOVERING),x.hoveredObject=x.lastHoveredObject=this}else{if(this._state==C.NONE)return;this._moveToState(C.NONE),x.hoveredObject===this&&(x.hoveredObject=null)}}static _eventToPoint(e,t){if(!t||!e.point||!e.logical)return null;const i=t.coordinateToPrice(e.point.y);return null==i?null:{time:e.time||null,logical:e.logical,price:i.valueOf()}}static _getDiff(e,t){return{logical:e.logical-t.logical,price:e.price-t.price}}_addDiffToPoint(e,t,i){e&&(e.logical=e.logical+t,e.price=e.price+i,e.time=this.series.dataByIndex(e.logical)?.time||null)}_handleMouseDownInteraction=()=>{x._mouseIsDown=!0,this._onMouseDown()};_handleMouseUpInteraction=()=>{x._mouseIsDown=!1,this._moveToState(C.HOVERING)};_handleDragInteraction(e){if(this._state!=C.DRAGGING&&this._state!=C.DRAGGINGP1&&this._state!=C.DRAGGINGP2&&this._state!=C.DRAGGINGP3&&this._state!=C.DRAGGINGP4)return;const t=x._eventToPoint(e,this.series);if(!t)return;this._startDragPoint=this._startDragPoint||t;const i=x._getDiff(t,this._startDragPoint);this._onDrag(i),this.requestUpdate(),this._startDragPoint=t}}class M{_options;constructor(e){this._options=e}}class S extends M{_p1;_p2;_hovered;constructor(e,t,i,o){super(i),this._p1=e,this._p2=t,this._hovered=o}_getScaledCoordinates(e){return null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y?null:{x1:Math.round(this._p1.x*e.horizontalPixelRatio),y1:Math.round(this._p1.y*e.verticalPixelRatio),x2:Math.round(this._p2.x*e.horizontalPixelRatio),y2:Math.round(this._p2.y*e.verticalPixelRatio)}}_drawEndCircle(e,t,i){e.context.fillStyle="#000",e.context.beginPath(),e.context.arc(t,i,9,0,2*Math.PI),e.context.stroke(),e.context.fill()}}function L(e,i){const o={[t.LineStyle.Solid]:[],[t.LineStyle.Dotted]:[e.lineWidth,e.lineWidth],[t.LineStyle.Dashed]:[2*e.lineWidth,2*e.lineWidth],[t.LineStyle.LargeDashed]:[6*e.lineWidth,6*e.lineWidth],[t.LineStyle.SparseDotted]:[e.lineWidth,4*e.lineWidth]}[i];e.setLineDash(o)}class T extends M{_point={x:null,y:null};constructor(e,t){super(t),this._point=e}draw(e){e.useBitmapCoordinateSpace((e=>{if(null==this._point.y)return;const t=e.context,i=Math.round(this._point.y*e.verticalPixelRatio),o=this._point.x?this._point.x*e.horizontalPixelRatio:0;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,L(t,this._options.lineStyle),t.beginPath(),t.moveTo(o,i),t.lineTo(e.bitmapSize.width,i),t.stroke()}))}}class k{_source;constructor(e){this._source=e}}class E extends k{_p1={x:null,y:null};_p2={x:null,y:null};_source;constructor(e){super(e),this._source=e}update(){if(!this._source.p1||!this._source.p2)return;const e=this._source.series,t=e.priceToCoordinate(this._source.p1.price),i=e.priceToCoordinate(this._source.p2.price),o=this._getX(this._source.p1),s=this._getX(this._source.p2);this._p1={x:o,y:t},this._p2={x:s,y:i}}_getX(e){return this._source.chart.timeScale().logicalToCoordinate(e.logical)}}class D extends k{_source;_point={x:null,y:null};constructor(e){super(e),this._source=e}update(){const e=this._source._point,t=this._source.chart.timeScale(),i=this._source.series;"RayLine"==this._source._type&&(this._point.x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical)),this._point.y=i.priceToCoordinate(e.price)}renderer(){return new T(this._point,this._source._options)}}class I{_source;_y=null;_price=null;constructor(e){this._source=e}update(){if(!this._source.series||!this._source._point)return;this._y=this._source.series.priceToCoordinate(this._source._point.price);const e=this._source.series.options().priceFormat.precision;this._price=this._source._point.price.toFixed(e).toString()}visible(){return!0}tickVisible(){return!0}coordinate(){return this._y??0}text(){return this._source._options.text||this._price||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class P extends x{_type="HorizontalLine";_paneViews;_point;_callbackName;_priceAxisViews;_startDragPoint=null;constructor(e,t,i=null){super(t),this._point=e,this._point.time=null,this._paneViews=[new D(this)],this._priceAxisViews=[new I(this)],this._callbackName=i}get points(){return[this._point]}updatePoints(...e){for(const t of e)t&&(this._point.price=t.price);this.requestUpdate()}updateAllViews(){this._paneViews.forEach((e=>e.update())),this._priceAxisViews.forEach((e=>e.update()))}priceAxisViews(){return this._priceAxisViews}_moveToState(e){switch(e){case C.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case C.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case C.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._addDiffToPoint(this._point,0,e.price),this.requestUpdate()}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this.series.priceToCoordinate(this._point.price);return!!i&&Math.abs(i-e.point.y){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class ${_chart;_series;_finishDrawingCallback=null;_drawings=[];_activeDrawing=null;_isDrawing=!1;_drawingType=null;constructor(e,t,i=null){this._chart=e,this._series=t,this._finishDrawingCallback=i,this._chart.subscribeClick(this._clickHandler),this._chart.subscribeCrosshairMove(this._moveHandler)}_clickHandler=e=>this._onClick(e);_moveHandler=e=>this._onMouseMove(e);beginDrawing(e){this._drawingType=e,this._isDrawing=!0}stopDrawing(){this._isDrawing=!1,this._activeDrawing=null}get drawings(){return this._drawings}addNewDrawing(e){this._series.attachPrimitive(e),this._drawings.push(e)}delete(e){if(null==e)return;const t=this._drawings.indexOf(e);-1!=t&&(this._drawings.splice(t,1),e.detach())}clearDrawings(){for(const e of this._drawings)e.detach();this._drawings=[]}repositionOnTime(){for(const e of this.drawings){const t=[];for(const i of e.points){if(!i){t.push(i);continue}const e=i.time?this._chart.timeScale().coordinateToLogical(this._chart.timeScale().timeToCoordinate(i.time)||0):i.logical;t.push({time:i.time,logical:e,price:i.price})}e.updatePoints(...t)}}_onClick(e){if(!this._isDrawing)return;const t=x._eventToPoint(e,this._series);if(t)if(null==this._activeDrawing){if(null==this._drawingType)return;this._activeDrawing=new this._drawingType(t,t),this._series.attachPrimitive(this._activeDrawing),this._drawingType==P&&this._onClick(e)}else{if(this._drawings.push(this._activeDrawing),this.stopDrawing(),!this._finishDrawingCallback)return;this._finishDrawingCallback()}}_onMouseMove(e){if(!e)return;for(const t of this._drawings)t._handleHoverInteraction(e);if(!this._isDrawing||!this._activeDrawing)return;const t=x._eventToPoint(e,this._series);t&&this._activeDrawing.updatePoints(null,t)}}class G extends S{constructor(e,t,i,o){super(e,t,i,o)}draw(e){e.useBitmapCoordinateSpace((e=>{if(null===this._p1.x||null===this._p1.y||null===this._p2.x||null===this._p2.y)return;const t=e.context,i=this._getScaledCoordinates(e);i&&(t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,L(t,this._options.lineStyle),t.beginPath(),t.moveTo(i.x1,i.y1),t.lineTo(i.x2,i.y2),t.stroke(),this._hovered&&(this._drawEndCircle(e,i.x1,i.y1),this._drawEndCircle(e,i.x2,i.y2)))}))}}class O extends E{constructor(e){super(e)}renderer(){return new G(this._p1,this._p2,this._source._options,this._source.hovered)}}class A extends x{_paneViews=[];_hovered=!1;constructor(e,t,i){super(),this.points.push(e),this.points.push(t),this._options={...w,...i}}setFirstPoint(e){this.updatePoints(e)}setSecondPoint(e){this.updatePoints(null,e)}get p1(){return this.points[0]}get p2(){return this.points[1]}get hovered(){return this._hovered}}class B extends A{_type="TrendLine";constructor(e,t,i){super(e,t,i),this._paneViews=[new O(this)]}_moveToState(e){switch(e){case C.NONE:document.body.style.cursor="default",this._hovered=!1,this.requestUpdate(),this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case C.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this.requestUpdate(),this._subscribe("mousedown",this._handleMouseDownInteraction),this._unsubscribe("mouseup",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case C.DRAGGINGP1:case C.DRAGGINGP2:case C.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._state!=C.DRAGGING&&this._state!=C.DRAGGINGP1||this._addDiffToPoint(this.p1,e.logical,e.price),this._state!=C.DRAGGING&&this._state!=C.DRAGGINGP2||this._addDiffToPoint(this.p2,e.logical,e.price)}_onMouseDown(){this._startDragPoint=null;const e=this._latestHoverPoint;if(!e)return;const t=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(t.x&&i.x&&t.y&&i.y))return this._moveToState(C.DRAGGING);Math.abs(e.x-t.x)<10&&Math.abs(e.y-t.y)<10?this._moveToState(C.DRAGGINGP1):Math.abs(e.x-i.x)<10&&Math.abs(e.y-i.y)<10?this._moveToState(C.DRAGGINGP2):this._moveToState(C.DRAGGING)}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this._paneViews[0]._p1.x,o=this._paneViews[0]._p1.y,s=this._paneViews[0]._p2.x,n=this._paneViews[0]._p2.y;if(!(i&&s&&o&&n))return!1;const r=e.point.x,a=e.point.y;if(r<=Math.min(i,s)-t||r>=Math.max(i,s)+t)return!1;return Math.abs((n-o)*r-(s-i)*a+s*o-n*i)/Math.sqrt((n-o)**2+(s-i)**2)<=t}}class R extends S{constructor(e,t,i,o){super(e,t,i,o)}draw(e){e.useBitmapCoordinateSpace((e=>{const t=e.context,i=this._getScaledCoordinates(e);if(!i)return;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,L(t,this._options.lineStyle),t.fillStyle=this._options.fillColor;const o=Math.min(i.x1,i.x2),s=Math.min(i.y1,i.y2),n=Math.abs(i.x1-i.x2),r=Math.abs(i.y1-i.y2);t.strokeRect(o,s,n,r),t.fillRect(o,s,n,r),this._hovered&&(this._drawEndCircle(e,o,s),this._drawEndCircle(e,o+n,s),this._drawEndCircle(e,o+n,s+r),this._drawEndCircle(e,o,s+r))}))}}class F extends E{constructor(e){super(e)}renderer(){return new R(this._p1,this._p2,this._source._options,this._source.hovered)}}const N={fillEnabled:!0,fillColor:"rgba(255, 255, 255, 0.2)",...w};class V extends A{_type="Box";constructor(e,t,i){super(e,t,i),this._options={...N,...i},this._paneViews=[new F(this)]}_moveToState(e){switch(e){case C.NONE:document.body.style.cursor="default",this._hovered=!1,this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case C.HOVERING:document.body.style.cursor="pointer",this._hovered=!0,this._unsubscribe("mouseup",this._handleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case C.DRAGGINGP1:case C.DRAGGINGP2:case C.DRAGGINGP3:case C.DRAGGINGP4:case C.DRAGGING:document.body.style.cursor="grabbing",document.body.addEventListener("mouseup",this._handleMouseUpInteraction),this._subscribe("mouseup",this._handleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._state!=C.DRAGGING&&this._state!=C.DRAGGINGP1||this._addDiffToPoint(this.p1,e.logical,e.price),this._state!=C.DRAGGING&&this._state!=C.DRAGGINGP2||this._addDiffToPoint(this.p2,e.logical,e.price),this._state!=C.DRAGGING&&(this._state==C.DRAGGINGP3&&(this._addDiffToPoint(this.p1,e.logical,0),this._addDiffToPoint(this.p2,0,e.price)),this._state==C.DRAGGINGP4&&(this._addDiffToPoint(this.p1,0,e.price),this._addDiffToPoint(this.p2,e.logical,0)))}_onMouseDown(){this._startDragPoint=null;const e=this._latestHoverPoint,t=this._paneViews[0]._p1,i=this._paneViews[0]._p2;if(!(t.x&&i.x&&t.y&&i.y))return this._moveToState(C.DRAGGING);const o=10;Math.abs(e.x-t.x)l-p&&rc-p&&ao-t)}}class U extends M{_point={x:null,y:null};constructor(e,t){super(t),this._point=e}draw(e){e.useBitmapCoordinateSpace((e=>{if(null==this._point.x)return;const t=e.context,i=this._point.x*e.horizontalPixelRatio;t.lineWidth=this._options.width,t.strokeStyle=this._options.lineColor,L(t,this._options.lineStyle),t.beginPath(),t.moveTo(i,0),t.lineTo(i,e.bitmapSize.height),t.stroke()}))}}class W extends k{_source;_point={x:null,y:null};constructor(e){super(e),this._source=e}update(){const e=this._source._point,t=this._source.chart.timeScale(),i=this._source.series;this._point.x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical),this._point.y=i.priceToCoordinate(e.price)}renderer(){return new U(this._point,this._source._options)}}class z{_source;_x=null;constructor(e){this._source=e}update(){if(!this._source.chart||!this._source._point)return;const e=this._source._point,t=this._source.chart.timeScale();this._x=e.time?t.timeToCoordinate(e.time):t.logicalToCoordinate(e.logical)}visible(){return!!this._source._options.text}tickVisible(){return!0}coordinate(){return this._x??0}text(){return this._source._options.text||""}textColor(){return"white"}backColor(){return this._source._options.lineColor}}class j extends x{_type="VerticalLine";_paneViews;_timeAxisViews;_point;_callbackName;_startDragPoint=null;constructor(e,t,i=null){super(t),this._point=e,this._paneViews=[new W(this)],this._callbackName=i,this._timeAxisViews=[new z(this)]}updateAllViews(){this._paneViews.forEach((e=>e.update())),this._timeAxisViews.forEach((e=>e.update()))}timeAxisViews(){return this._timeAxisViews}updatePoints(...e){for(const t of e)t&&(!t.time&&t.logical&&(t.time=this.series.dataByIndex(t.logical)?.time||null),this._point=t);this.requestUpdate()}get points(){return[this._point]}_moveToState(e){switch(e){case C.NONE:document.body.style.cursor="default",this._unsubscribe("mousedown",this._handleMouseDownInteraction);break;case C.HOVERING:document.body.style.cursor="pointer",this._unsubscribe("mouseup",this._childHandleMouseUpInteraction),this._subscribe("mousedown",this._handleMouseDownInteraction),this.chart.applyOptions({handleScroll:!0});break;case C.DRAGGING:document.body.style.cursor="grabbing",this._subscribe("mouseup",this._childHandleMouseUpInteraction),this.chart.applyOptions({handleScroll:!1})}this._state=e}_onDrag(e){this._addDiffToPoint(this._point,e.logical,0),this.requestUpdate()}_mouseIsOverDrawing(e,t=4){if(!e.point)return!1;const i=this.chart.timeScale();let o;return o=this._point.time?i.timeToCoordinate(this._point.time):i.logicalToCoordinate(this._point.logical),!!o&&Math.abs(o-e.point.x){this._handleMouseUpInteraction(),this._callbackName&&window.callbackFunction(`${this._callbackName}_~_${this._point.price.toFixed(8)}`)}}class q{static TREND_SVG='';static HORZ_SVG='';static RAY_SVG='';static BOX_SVG='';static VERT_SVG=q.RAY_SVG;div;activeIcon=null;buttons=[];_commandFunctions;_handlerID;_drawingTool;handler;constructor(e,t,i,o,s){this._handlerID=t,this._commandFunctions=s,this._drawingTool=new $(i,o,(()=>this.removeActiveAndSave())),this.div=this._makeToolBox(),this.handler=e,this.handler.ContextMenu.setupDrawingTools(this.saveDrawings,this._drawingTool),s.push((e=>{if((e.metaKey||e.ctrlKey)&&"KeyZ"===e.code){const e=this._drawingTool.drawings.pop();return e&&this._drawingTool.delete(e),!0}return!1}))}toJSON(){const{...e}=this;return e}_makeToolBox(){let e=document.createElement("div");e.classList.add("toolbox"),this.buttons.push(this._makeToolBoxElement(B,"KeyT",q.TREND_SVG)),this.buttons.push(this._makeToolBoxElement(P,"KeyH",q.HORZ_SVG)),this.buttons.push(this._makeToolBoxElement(H,"KeyR",q.RAY_SVG)),this.buttons.push(this._makeToolBoxElement(V,"KeyB",q.BOX_SVG)),this.buttons.push(this._makeToolBoxElement(j,"KeyV",q.VERT_SVG,!0));for(const t of this.buttons)e.appendChild(t);return e}_makeToolBoxElement(e,t,i,o=!1){const s=document.createElement("div");s.classList.add("toolbox-button");const n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("width","29"),n.setAttribute("height","29");const r=document.createElementNS("http://www.w3.org/2000/svg","g");r.innerHTML=i,r.setAttribute("fill",window.pane.color),n.appendChild(r),s.appendChild(n);const a={div:s,group:r,type:e};return s.addEventListener("click",(()=>this._onIconClick(a))),this._commandFunctions.push((e=>this._handlerID===window.handlerInFocus&&(!(!e.altKey||e.code!==t)&&(e.preventDefault(),this._onIconClick(a),!0)))),1==o&&(n.style.transform="rotate(90deg)",n.style.transformBox="fill-box",n.style.transformOrigin="center"),s}_onIconClick(e){this.activeIcon&&(this.activeIcon.div.classList.remove("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.stopDrawing(),this.activeIcon===e)?this.activeIcon=null:(this.activeIcon=e,this.activeIcon.div.classList.add("active-toolbox-button"),window.setCursor("crosshair"),this._drawingTool?.beginDrawing(this.activeIcon.type))}removeActiveAndSave=()=>{window.setCursor("default"),this.activeIcon&&this.activeIcon.div.classList.remove("active-toolbox-button"),this.activeIcon=null,this.saveDrawings()};addNewDrawing(e){this._drawingTool.addNewDrawing(e)}clearDrawings(){this._drawingTool.clearDrawings()}saveDrawings=()=>{const e=[];for(const t of this._drawingTool.drawings)e.push({type:t._type,points:t.points,options:t._options});const t=JSON.stringify(e);window.callbackFunction(`save_drawings${this._handlerID}_~_${t}`)};loadDrawings(e){e.forEach((e=>{switch(e.type){case"Box":this._drawingTool.addNewDrawing(new V(e.points[0],e.points[1],e.options));break;case"TrendLine":this._drawingTool.addNewDrawing(new B(e.points[0],e.points[1],e.options));break;case"HorizontalLine":this._drawingTool.addNewDrawing(new P(e.points[0],e.options));break;case"RayLine":this._drawingTool.addNewDrawing(new H(e.points[0],e.options));break;case"VerticalLine":this._drawingTool.addNewDrawing(new j(e.points[0],e.options))}}))}}class Y{makeButton;callbackName;div;isOpen=!1;widget;constructor(e,t,i,o,s,n){this.makeButton=e,this.callbackName=t,this.div=document.createElement("div"),this.div.classList.add("topbar-menu"),this.widget=this.makeButton(o+" ↓",null,s,!0,n),this.updateMenuItems(i),this.widget.elem.addEventListener("click",(()=>{if(this.isOpen=!this.isOpen,!this.isOpen)return void(this.div.style.display="none");let e=this.widget.elem.getBoundingClientRect();this.div.style.display="flex",this.div.style.flexDirection="column";let t=e.x+e.width/2;this.div.style.left=t-this.div.clientWidth/2+"px",this.div.style.top=e.y+e.height+"px"})),document.body.appendChild(this.div)}updateMenuItems(e){this.div.innerHTML="",e.forEach((e=>{let t=this.makeButton(e,null,!1,!1);t.elem.addEventListener("click",(()=>{this._clickHandler(t.elem.innerText)})),t.elem.style.margin="4px 4px",t.elem.style.padding="2px 2px",this.div.appendChild(t.elem)})),this.widget.elem.innerText=e[0]+" ↓"}_clickHandler(e){this.widget.elem.innerText=e+" ↓",window.callbackFunction(`${this.callbackName}_~_${e}`),this.div.style.display="none",this.isOpen=!1}}class X{_handler;_div;left;right;constructor(e){this._handler=e,this._div=document.createElement("div"),this._div.classList.add("topbar");const t=e=>{const t=document.createElement("div");return t.classList.add("topbar-container"),t.style.justifyContent=e,this._div.appendChild(t),t};this.left=t("flex-start"),this.right=t("flex-end")}makeSwitcher(e,t,i,o="left"){const s=document.createElement("div");let n;s.style.margin="4px 12px";const r={elem:s,callbackName:i,intervalElements:e.map((e=>{const i=document.createElement("button");i.classList.add("topbar-button"),i.classList.add("switcher-button"),i.style.margin="0px 2px",i.innerText=e,e==t&&(n=i,i.classList.add("active-switcher-button"));const o=X.getClientWidth(i);return i.style.minWidth=o+1+"px",i.addEventListener("click",(()=>r.onItemClicked(i))),s.appendChild(i),i})),onItemClicked:e=>{e!=n&&(n.classList.remove("active-switcher-button"),e.classList.add("active-switcher-button"),n=e,window.callbackFunction(`${r.callbackName}_~_${e.innerText}`))}};return this.appendWidget(s,o,!0),r}makeTextBoxWidget(e,t="left",i=null){if(i){const o=document.createElement("input");return o.classList.add("topbar-textbox-input"),o.value=e,o.style.width=`${o.value.length+2}ch`,o.addEventListener("focus",(()=>{window.textBoxFocused=!0})),o.addEventListener("input",(e=>{e.preventDefault(),o.style.width=`${o.value.length+2}ch`})),o.addEventListener("keydown",(e=>{"Enter"==e.key&&(e.preventDefault(),o.blur())})),o.addEventListener("blur",(()=>{window.callbackFunction(`${i}_~_${o.value}`),window.textBoxFocused=!1})),this.appendWidget(o,t,!0),o}{const i=document.createElement("div");return i.classList.add("topbar-textbox"),i.innerText=e,this.appendWidget(i,t,!0),i}}makeMenu(e,t,i,o,s){return new Y(this.makeButton.bind(this),o,e,t,i,s)}makeButton(e,t,i,o=!0,s="left",n=!1){let r=document.createElement("button");r.classList.add("topbar-button"),r.innerText=e,document.body.appendChild(r),r.style.minWidth=r.clientWidth+1+"px",document.body.removeChild(r);let a={elem:r,callbackName:t};if(t){let e;if(n){let t=!1;e=()=>{t=!t,window.callbackFunction(`${a.callbackName}_~_${t}`),r.style.backgroundColor=t?"var(--active-bg-color)":"",r.style.color=t?"var(--active-color)":""}}else e=()=>window.callbackFunction(`${a.callbackName}_~_${r.innerText}`);r.addEventListener("click",e)}return o&&this.appendWidget(r,s,i),a}makeSeparator(e="left"){const t=document.createElement("div");t.classList.add("topbar-seperator");("left"==e?this.left:this.right).appendChild(t)}appendWidget(e,t,i){const o="left"==t?this.left:this.right;i?("left"==t&&o.appendChild(e),this.makeSeparator(t),"right"==t&&o.appendChild(e)):o.appendChild(e),this._handler.reSize()}static getClientWidth(e){document.body.appendChild(e);const t=e.clientWidth;return document.body.removeChild(e),t}}const K={title:"",followMode:"tracking",horizontalDeadzoneWidth:45,verticalDeadzoneHeight:100,verticalSpacing:20,topOffset:20};class Z{_chart;_element;_titleElement;_priceElement;_dateElement;_timeElement;_options;_lastTooltipWidth=null;constructor(e,t){this._options={...K,...t},this._chart=e;const i=document.createElement("div");Q(i,{display:"flex","flex-direction":"column","align-items":"center",position:"absolute",transform:"translate(calc(0px - 50%), 0px)",opacity:"0",left:"0%",top:"0","z-index":"100","background-color":"white","border-radius":"4px",padding:"5px 10px","font-family":"-apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif","font-size":"12px","font-weight":"400","box-shadow":"0px 2px 4px rgba(0, 0, 0, 0.2)","line-height":"16px","pointer-events":"none",color:"#131722"});const o=document.createElement("div");Q(o,{"font-size":"12px","line-height":"24px","font-weight":"590"}),J(o,this._options.title),i.appendChild(o);const s=document.createElement("div");Q(s,{"font-size":"12px","line-height":"18px","font-weight":"590"}),J(s,""),i.appendChild(s);const n=document.createElement("div");Q(n,{color:"#787B86"}),J(n,""),i.appendChild(n);const r=document.createElement("div");Q(r,{color:"#787B86"}),J(r,""),i.appendChild(r),this._element=i,this._titleElement=o,this._priceElement=s,this._dateElement=n,this._timeElement=r;const a=this._chart.chartElement();a.appendChild(this._element);const l=a.parentElement;if(!l)return void console.error("Chart Element is not attached to the page.");const c=getComputedStyle(l).position;"relative"!==c&&"absolute"!==c&&console.error("Chart Element position is expected be `relative` or `absolute`.")}destroy(){this._chart&&this._element&&this._chart.chartElement().removeChild(this._element)}applyOptions(e){this._options={...this._options,...e}}options(){return this._options}updateTooltipContent(e){if(!this._element)return void console.warn("Tooltip element not found.");const t=this._element.getBoundingClientRect();this._lastTooltipWidth=t.width,void 0!==e.title&&this._titleElement?(console.log(`Setting title: ${e.title}`),J(this._titleElement,e.title)):console.warn("Title element is missing or title data is undefined."),J(this._priceElement,e.price),J(this._dateElement,e.date),J(this._timeElement,e.time)}updatePosition(e){if(!this._chart||!this._element)return;if(this._element.style.opacity=e.visible?"1":"0",!e.visible)return;const t=this._calculateXPosition(e,this._chart),i=this._calculateYPosition(e);this._element.style.transform=`translate(${t}, ${i})`}_calculateXPosition(e,t){const i=e.paneX+t.priceScale("left").width(),o=this._lastTooltipWidth?Math.ceil(this._lastTooltipWidth/2):this._options.horizontalDeadzoneWidth;return`calc(${Math.min(Math.max(o,i),t.timeScale().width()-o)}px - 50%)`}_calculateYPosition(e){if("top"==this._options.followMode)return`${this._options.topOffset}px`;const t=e.paneY,i=t<=this._options.verticalSpacing+this._options.verticalDeadzoneHeight;return`calc(${t+(i?1:-1)*this._options.verticalSpacing}px${i?"":" - 100%"})`}}function J(e,t){e&&t!==e.innerText&&(e.innerText=t,e.style.display=t?"block":"none")}function Q(e,t){for(const[i,o]of Object.entries(t))e.style.setProperty(i,o)}function ee(e,t,i=1,o){const s=Math.round(t*e),n=Math.round(i*t),r=function(e){return Math.floor(.5*e)}(n);return{position:s-r,length:n}}class te{_data;constructor(e){this._data=e}draw(e){this._data.visible&&e.useBitmapCoordinateSpace((e=>{const t=e.context,i=ee(this._data.x,e.horizontalPixelRatio,1);t.fillStyle=this._data.color,t.fillRect(i.position,this._data.topMargin*e.verticalPixelRatio,i.length,e.bitmapSize.height)}))}}class ie{_data;constructor(e){this._data=e}update(e){this._data=e}renderer(){return new te(this._data)}zOrder(){return"bottom"}}const oe={lineColor:"rgba(0, 0, 0, 0.2)",priceExtractor:e=>void 0!==e.value?e.value.toFixed(2):void 0!==e.close?e.close.toFixed(2):""};class se{_options;_tooltip=void 0;_paneViews;_data={x:0,visible:!1,color:"rgba(0, 0, 0, 0.2)",topMargin:0};_attachedParams;constructor(e){this._options={...oe,...e},this._data.color=this._options.lineColor,this._paneViews=[new ie(this._data)]}attached(e){this._attachedParams=e;const t=this.series();if(t){const e=t.options(),i=e.lineColor||e.color||"rgba(0,0,0,0.2)";this._options.autoColor&&this.applyOptions({lineColor:i})}this._setCrosshairMode(),e.chart.subscribeCrosshairMove(this._moveHandler),this._createTooltipElement()}detached(){const e=this.chart();e&&e.unsubscribeCrosshairMove(this._moveHandler),this._hideCrosshair(),this._hideTooltip()}paneViews(){return this._paneViews}updateAllViews(){this._paneViews.forEach((e=>e.update(this._data)))}setData(e){this._data=e,this.updateAllViews(),this._attachedParams?.requestUpdate()}currentColor(){return this._options.lineColor}chart(){return this._attachedParams?.chart}series(){return this._attachedParams?.series}applyOptions(e){this._options={...this._options,...e},e.lineColor&&this.setData({...this._data,color:e.lineColor}),this._tooltip&&this._tooltip.applyOptions({...this._options.tooltip}),this._attachedParams?.requestUpdate()}_setCrosshairMode(){const e=this.chart();if(!e)throw new Error("Unable to change crosshair mode because the chart instance is undefined");e.applyOptions({crosshair:{mode:t.CrosshairMode.Magnet,vertLine:{visible:!1,labelVisible:!1},horzLine:{visible:!1,labelVisible:!1}}})}_moveHandler=e=>this._onMouseMove(e);switch(e){if(this.series()===e)return void console.log("Tooltip is already attached to this series.");this._hideCrosshair(),e.attachPrimitive(this,"Tooltip",!0,!1);const t=e.options(),i=t.lineColor||t.color||"rgba(0,0,0,0.2)";this._options.autoColor&&this.applyOptions({lineColor:i}),console.log("Switched tooltip to the new series.")}_hideCrosshair(){this._hideTooltip(),this.setData({x:0,visible:!1,color:this._options.lineColor,topMargin:0})}_hideTooltip(){this._tooltip&&(this._tooltip.updateTooltipContent({title:"",price:"",date:"",time:""}),this._tooltip.updatePosition({paneX:0,paneY:0,visible:!1}))}_onMouseMove(e){const i=this.chart(),o=this.series(),s=e.logical;if(!s||!i||!o)return void this._hideCrosshair();const n=e.seriesData.get(o);if(!n)return void this._hideCrosshair();const r=this._options.priceExtractor(n),a=i.timeScale().logicalToCoordinate(s),[l,c]=function(e){if(!e)return["",""];const t=new Date(e),i=t.getFullYear(),o=t.toLocaleString("default",{month:"short"});return[`${t.getDate().toString().padStart(2,"0")} ${o} ${i}`,`${t.getHours().toString().padStart(2,"0")}:${t.getMinutes().toString().padStart(2,"0")}`]}(e.time?function(e){if(t.isUTCTimestamp(e))return 1e3*e;if(t.isBusinessDay(e))return new Date(e.year,e.month,e.day).valueOf();const[i,o,s]=e.split("-").map(parseInt);return new Date(i,o,s).valueOf()}(e.time):void 0);if(this._tooltip){const t=o.options()?.title||"Unknown Series",i=this._tooltip.options(),s="top"===i.followMode?i.topOffset+10:0;this.setData({x:a??0,visible:null!==a,color:this._options.lineColor,topMargin:s}),this._tooltip.updateTooltipContent({title:t,price:r,date:l,time:c}),this._tooltip.updatePosition({paneX:e.point?.x??0,paneY:e.point?.y??0,visible:!0})}}_createTooltipElement(){const e=this.chart();if(!e)throw new Error("Unable to create Tooltip element. Chart not attached");this._tooltip=new Z(e,{...this._options.tooltip})}}let ne=class e{container;_opacitySlider;_opacity_label;exitButton;color="#ff0000";rgba;opacity;applySelection;constructor(t,i){this.applySelection=i,this.rgba=e.extractRGBA(t),this.opacity=this.rgba[3],this.container=document.createElement("div"),this.container.classList.add("color-picker"),this.container.style.display="flex",this.container.style.flexDirection="column",this.container.style.width="150px",this.container.style.height="300px",this.container.style.position="relative";const o=this.createColorGrid(),s=this.createOpacityUI();this.exitButton=this.createExitButton(),this.container.appendChild(o),this.container.appendChild(this.createSeparator()),this.container.appendChild(this.createSeparator()),this.container.appendChild(s),this.container.appendChild(this.exitButton)}createExitButton(){const e=document.createElement("div");return e.innerText="✕",e.title="Close",e.style.position="absolute",e.style.bottom="5px",e.style.right="5px",e.style.width="20px",e.style.height="20px",e.style.cursor="pointer",e.style.display="flex",e.style.justifyContent="center",e.style.alignItems="center",e.style.fontSize="16px",e.style.backgroundColor="#ccc",e.style.borderRadius="50%",e.style.color="#000",e.style.boxShadow="0 1px 3px rgba(0,0,0,0.3)",e.addEventListener("mouseover",(()=>{e.style.backgroundColor="#e74c3c",e.style.color="#fff"})),e.addEventListener("mouseout",(()=>{e.style.backgroundColor="#ccc",e.style.color="#000"})),e.addEventListener("click",(()=>{this.closeMenu()})),e}createColorGrid(){const t=document.createElement("div");t.style.display="grid",t.style.gridTemplateColumns="repeat(7, 1fr)",t.style.gap="5px",t.style.overflowY="auto",t.style.flex="1";return e.generateFullSpectrumColors(9).forEach((e=>{const i=this.createColorBox(e);t.appendChild(i)})),t}createColorBox(t){const i=document.createElement("div");return i.style.aspectRatio="1",i.style.borderRadius="6px",i.style.backgroundColor=t,i.style.cursor="pointer",i.addEventListener("click",(()=>{this.rgba=e.extractRGBA(t),this.updateTargetColor()})),i}static generateFullSpectrumColors(e){const t=[];for(let i=0;i<=255;i+=Math.floor(255/e))t.push(`rgba(255, ${i}, 0, 1)`);for(let i=255;i>=0;i-=Math.floor(255/e))t.push(`rgba(${i}, 255, 0, 1)`);for(let i=0;i<=255;i+=Math.floor(255/e))t.push(`rgba(0, 255, ${i}, 1)`);for(let i=255;i>=0;i-=Math.floor(255/e))t.push(`rgba(0, ${i}, 255, 1)`);for(let i=0;i<=255;i+=Math.floor(255/e))t.push(`rgba(${i}, 0, 255, 1)`);for(let i=255;i>=0;i-=Math.floor(255/e))t.push(`rgba(255, 0, ${i}, 1)`);for(let i=255;i>=0;i-=Math.floor(255/e))t.push(`rgba(${i}, ${i}, ${i}, 1)`);return t}createOpacityUI(){const e=document.createElement("div");e.style.margin="10px",e.style.display="flex",e.style.flexDirection="column",e.style.alignItems="center";const t=document.createElement("div");return t.style.color="lightgray",t.style.fontSize="12px",t.innerText="Opacity",this._opacitySlider=document.createElement("input"),this._opacitySlider.type="range",this._opacitySlider.min="0",this._opacitySlider.max="100",this._opacitySlider.value=(100*this.opacity).toString(),this._opacitySlider.style.width="80%",this._opacity_label=document.createElement("div"),this._opacity_label.style.color="lightgray",this._opacity_label.style.fontSize="12px",this._opacity_label.innerText=`${this._opacitySlider.value}%`,this._opacitySlider.oninput=()=>{this._opacity_label.innerText=`${this._opacitySlider.value}%`,this.opacity=parseInt(this._opacitySlider.value)/100,this.updateTargetColor()},e.appendChild(t),e.appendChild(this._opacitySlider),e.appendChild(this._opacity_label),e}createSeparator(){const e=document.createElement("div");return e.style.height="1px",e.style.width="100%",e.style.backgroundColor="#ccc",e.style.margin="5px 0",e}openMenu(e,t,i){this.applySelection=i,this.container.style.display="block",document.body.appendChild(this.container),console.log("Menu attached:",this.container);const o=this.container.offsetWidth||150,s=this.container.offsetHeight||250;console.log("Submenu dimensions:",{submenuWidth:o,submenuHeight:s});const n=e.clientX,r=e.clientY;console.log("Mouse position:",{cursorX:n,cursorY:r});const a=window.innerWidth,l=window.innerHeight;let c=n+t,d=r;const h=c+o>a?n-o:c,p=d+s>l?l-s-10:d;console.log({left:c,top:d,adjustedLeft:h,adjustedTop:p}),this.container.style.left=`${h}px`,this.container.style.top=`${p}px`,this.container.style.display="flex",this.container.style.position="absolute",this.exitButton.style.bottom="5px",this.exitButton.style.right="5px",document.addEventListener("mousedown",this._handleOutsideClick.bind(this),{once:!0})}closeMenu(){this.container.style.display="none",document.removeEventListener("mousedown",this._handleOutsideClick)}_handleOutsideClick(e){this.container.contains(e.target)||this.closeMenu()}static extractRGBA(e){const t=document.createElement("div");t.style.color=e,document.body.appendChild(t);const i=getComputedStyle(t).color;document.body.removeChild(t);const o=i.match(/\d+/g)?.map(Number)||[0,0,0],s=i.includes("rgba")?parseFloat(i.split(",")[3]):1;return[o[0],o[1],o[2],s]}getElement(){return this.container}update(t,i){this.rgba=e.extractRGBA(t),this.opacity=this.rgba[3],this.applySelection=i,this.updateTargetColor()}updateTargetColor(){this.color=`rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`,this.applySelection(this.color)}};class re{colorOption;static colors=["#EBB0B0","#E9CEA1","#E5DF80","#ADEB97","#A3C3EA","#D8BDED","#E15F5D","#E1B45F","#E2D947","#4BE940","#639AE1","#D7A0E8","#E42C2A","#E49D30","#E7D827","#3CFF0A","#3275E4","#B06CE3","#F3000D","#EE9A14","#F1DA13","#2DFC0F","#1562EE","#BB00EF","#B50911","#E3860E","#D2BD11","#48DE0E","#1455B4","#6E009F","#7C1713","#B76B12","#8D7A13","#479C12","#165579","#51007E"];_div;saveDrawings;opacity=0;_opacitySlider;_opacityLabel;rgba;constructor(e,t){this.colorOption=t,this.saveDrawings=e,this._div=document.createElement("div"),this._div.classList.add("color-picker");let i=document.createElement("div");i.style.margin="10px",i.style.display="flex",i.style.flexWrap="wrap",re.colors.forEach((e=>i.appendChild(this.makeColorBox(e))));let o=document.createElement("div");o.style.backgroundColor=window.pane.borderColor,o.style.height="1px",o.style.width="130px";let s=document.createElement("div");s.style.margin="10px";let n=document.createElement("div");n.style.color="lightgray",n.style.fontSize="12px",n.innerText="Opacity",this._opacityLabel=document.createElement("div"),this._opacityLabel.style.color="lightgray",this._opacityLabel.style.fontSize="12px",this._opacitySlider=document.createElement("input"),this._opacitySlider.type="range",this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%",this._opacitySlider.oninput=()=>{this._opacityLabel.innerText=this._opacitySlider.value+"%",this.opacity=parseInt(this._opacitySlider.value)/100,this.updateColor()},s.appendChild(n),s.appendChild(this._opacitySlider),s.appendChild(this._opacityLabel),this._div.appendChild(i),this._div.appendChild(o),this._div.appendChild(s),window.containerDiv.appendChild(this._div)}_updateOpacitySlider(){this._opacitySlider.value=(100*this.opacity).toString(),this._opacityLabel.innerText=this._opacitySlider.value+"%"}makeColorBox(e){const t=document.createElement("div");t.style.width="18px",t.style.height="18px",t.style.borderRadius="3px",t.style.margin="3px",t.style.boxSizing="border-box",t.style.backgroundColor=e,t.addEventListener("mouseover",(()=>t.style.border="2px solid lightgray")),t.addEventListener("mouseout",(()=>t.style.border="none"));const i=re.extractRGBA(e);return t.addEventListener("click",(()=>{this.rgba=i,this.updateColor()})),t}static extractRGBA(e){const t=document.createElement("div");t.style.color=e,document.body.appendChild(t);const i=getComputedStyle(t).color;document.body.removeChild(t);const o=i.match(/\d+/g)?.map(Number);if(!o)return[];let s=i.includes("rgba")?parseFloat(i.split(",")[3]):1;return[o[0],o[1],o[2],s]}updateColor(){if(!x.lastHoveredObject||!this.rgba)return;const e=`rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`;x.lastHoveredObject.applyOptions({[this.colorOption]:e}),this.saveDrawings()}openMenu(e){x.lastHoveredObject&&(this.rgba=re.extractRGBA(x.lastHoveredObject._options[this.colorOption]),this.opacity=this.rgba[3],this._updateOpacitySlider(),this._div.style.top=e.top-30+"px",this._div.style.left=e.right+"px",this._div.style.display="flex",setTimeout((()=>document.addEventListener("mousedown",(e=>{this._div.contains(e.target)||this.closeMenu()}))),10))}closeMenu(){document.body.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}class ae{static _styles=[{name:"Solid",var:t.LineStyle.Solid},{name:"Dotted",var:t.LineStyle.Dotted},{name:"Dashed",var:t.LineStyle.Dashed},{name:"Large Dashed",var:t.LineStyle.LargeDashed},{name:"Sparse Dotted",var:t.LineStyle.SparseDotted}];_div;_saveDrawings;constructor(e){this._saveDrawings=e,this._div=document.createElement("div"),this._div.classList.add("context-menu"),ae._styles.forEach((e=>{this._div.appendChild(this._makeTextBox(e.name,e.var))})),window.containerDiv.appendChild(this._div)}_makeTextBox(e,t){const i=document.createElement("span");return i.classList.add("context-menu-item"),i.innerText=e,i.addEventListener("click",(()=>{x.lastHoveredObject?.applyOptions({lineStyle:t}),this._saveDrawings()})),i}openMenu(e){this._div.style.top=e.top-30+"px",this._div.style.left=e.right+"px",this._div.style.display="block",setTimeout((()=>document.addEventListener("mousedown",(e=>{this._div.contains(e.target)||this.closeMenu()}))),10)}closeMenu(){document.removeEventListener("click",this.closeMenu),this._div.style.display="none"}}function le(e,t){const i=e.split("."),o={};let s=o;for(let e=0;ee.toUpperCase()))}class pe{handler;handlerMap;getMouseEventParams;div;hoverItem;items=[];colorPicker=new ne("#ff0000",(()=>null));saveDrawings=null;drawingTool=null;mouseEventParams=null;constraints={baseline:{skip:!0},title:{skip:!0},PriceLineSource:{skip:!0},tickInterval:{min:0,max:100},lastPriceAnimation:{skip:!0},lineType:{min:0,max:2}};setupDrawingTools(e,t){this.saveDrawings=e,this.drawingTool=t}shouldSkipOption(e){return!!(this.constraints[e]||{}).skip}separator(){const e=document.createElement("div");e.style.width="90%",e.style.height="1px",e.style.margin="3px 0px",e.style.backgroundColor=window.pane.borderColor,this.div.appendChild(e),this.items.push(e)}menuItem(e,t,i=null){const o=document.createElement("span");o.classList.add("context-menu-item"),this.div.appendChild(o);const s=document.createElement("span");if(s.innerText=e,s.style.pointerEvents="none",o.appendChild(s),i){let e=document.createElement("span");e.innerText="►",e.style.fontSize="8px",e.style.pointerEvents="none",o.appendChild(e)}if(o.addEventListener("mouseover",(()=>{this.hoverItem&&this.hoverItem.closeAction&&this.hoverItem.closeAction(),this.hoverItem={elem:s,action:t,closeAction:i}})),i){let e;o.addEventListener("mouseover",(()=>e=setTimeout((()=>t(o.getBoundingClientRect())),100))),o.addEventListener("mouseout",(()=>clearTimeout(e)))}else o.addEventListener("click",(e=>{t(e),this.div.style.display="none"}));this.items.push(o)}constructor(e,t,i){this.handler=e,this.handlerMap=t,this.getMouseEventParams=i,this.div=document.createElement("div"),this.div.classList.add("context-menu"),document.body.appendChild(this.div),this.div.style.overflowY="scroll",this.hoverItem=null,this.mouseEventParams=i(),document.body.addEventListener("contextmenu",this._onRightClick.bind(this)),document.body.addEventListener("click",this._onClick.bind(this)),this.setupMenu()}_onClick(e){const t=e.target;[this.colorPicker].forEach((e=>{e.getElement().contains(t)||e.closeMenu()}))}_onRightClick(e){e.preventDefault();const t=this.getMouseEventParams(),i=this.getProximitySeries(this.getMouseEventParams()),o=this.getProximityDrawing();console.log("Mouse Event Params:",t),console.log("Proximity Series:",i),console.log("Proximity Drawing:",o),this.clearMenu(),this.clearAllMenus(),i?(console.log("Right-click detected on a series (proximity)."),this.populateSeriesMenu(i,e)):o?(console.log("Right-click detected on a drawing."),this.populateDrawingMenu(o,e)):t?.hoveredSeries?(console.log("Right-click detected on a series (hovered)."),this.populateSeriesMenu(t.hoveredSeries,e)):(console.log("Right-click detected on the chart background."),this.populateChartMenu(e)),this.showMenu(e),e.preventDefault(),e.stopPropagation()}getProximityDrawing(){return x.hoveredObject?x.hoveredObject:null}getProximitySeries(e){if(!e||!e.seriesData)return console.warn("No mouse event parameters or series data available."),null;if(!e.point)return console.warn("No point data in MouseEventParams."),null;const t=e.point.y;let i=null;const o=this.handler._seriesList[0];if(this.handler.series)i=this.handler.series,console.log("Using handler.series for coordinate conversion.");else{if(!o)return console.warn("No handler.series or referenceSeries available."),null;i=o,console.log("Using referenceSeries for coordinate conversion.")}const s=i.coordinateToPrice(t);if(console.log(`Converted chart Y (${t}) to Price: ${s}`),null===s)return console.warn("Cursor price is null. Unable to determine proximity."),null;const n=[];return e.seriesData.forEach(((e,t)=>{let i;if(!function(e){return"value"in e}(e)?function(e){return"close"in e&&"open"in e&&"high"in e&&"low"in e}(e)&&(i=e.close):i=e.value,void 0!==i&&!isNaN(i)){const e=Math.abs(i-s);e/s*100<=3.33&&n.push({distance:e,series:t})}})),n.sort(((e,t)=>e.distance-t.distance)),n.length>0?(console.log("Closest series found."),n[0].series):(console.log("No series found within the proximity threshold."),null)}showMenu(e){const t=e.clientX,i=e.clientY;this.div.style.position="absolute",this.div.style.zIndex="1000",this.div.style.left=`${t}px`,this.div.style.top=`${i}px`,this.div.style.width="225px",this.div.style.maxHeight="400px",this.div.style.overflowY="scroll",this.div.style.display="block",console.log("Displaying Menu at:",t,i),de=this.div,console.log("Displaying Menu",t,i),document.addEventListener("mousedown",this.hideMenuOnOutsideClick.bind(this),{once:!0})}hideMenuOnOutsideClick(e){this.div.contains(e.target)||this.hideMenu()}hideMenu(){this.div.style.display="none",de===this.div&&(de=null)}resetView(){this.handler.chart.timeScale().resetTimeScale(),this.handler.chart.timeScale().fitContent()}clearAllMenus(){this.handlerMap.forEach((e=>{e.ContextMenu&&e.ContextMenu.clearMenu()}))}setupMenu(){if(!this.div.querySelector(".chart-options-container")){const e=document.createElement("div");e.classList.add("chart-options-container"),this.div.appendChild(e)}this.div.querySelector(".context-menu-item.close-menu")||this.addMenuItem("Close Menu",(()=>this.hideMenu()))}addNumberInput(e,t,i,o,s){return this.addMenuInput(this.div,{type:"number",label:e,value:t,onChange:i,min:o,max:s},"")}addCheckbox(e,t,i){return this.addMenuInput(this.div,{type:"boolean",label:e,value:t,onChange:i})}addMenuInput(e,t,i=""){let o;if("number"===t.type||"string"===t.type||"boolean"===t.type){if(o=document.createElement("div"),o.classList.add("context-menu-item"),o.style.display="flex",o.style.alignItems="center",o.style.justifyContent="space-between",t.label){const e=document.createElement("label");e.innerText=t.label,e.htmlFor=`${i}${t.label.toLowerCase()}`,e.style.marginRight="8px",o.appendChild(e)}let e;"number"===t.type?(e=document.createElement("input"),e.type="number",e.value=void 0!==t.value?t.value.toString():"",e.style.width="45px",e.style.marginLeft="auto",e.style.cursor="pointer",void 0!==t.min&&(e.min=t.min.toString()),void 0!==t.max&&(e.max=t.max.toString()),e.addEventListener("input",(i=>{const o=i.target;let s=parseFloat(o.value);const n=t.label,r=this.constraints[n.toLowerCase()];r&&!r.skip&&(void 0!==r.min&&sr.max&&(s=r.max,e.value=s.toString())),isNaN(s)||t.onChange(s)})),o.appendChild(e)):"boolean"===t.type?(e=document.createElement("input"),e.type="checkbox",e.checked=t.value??!1,e.style.marginLeft="auto",e.style.cursor="pointer",e.addEventListener("change",(e=>{const i=e.target;t.onChange(i.checked)})),o.appendChild(e)):(e=document.createElement("input"),e.type="text",e.value=t.value??"",e.style.marginLeft="auto",e.style.cursor="pointer",e.addEventListener("input",(e=>{const i=e.target;t.onChange(i.value)})),o.appendChild(e))}else if("select"===t.type){if(o=document.createElement("div"),o.classList.add("context-menu-item"),o.style.display="flex",o.style.alignItems="center",o.style.justifyContent="space-between",t.label){const e=document.createElement("label");e.innerText=t.label,e.htmlFor=`${i}${t.label.toLowerCase()}`,e.style.marginRight="8px",o.appendChild(e)}const e=document.createElement("select");e.id=`${i}${t.label?t.label.toLowerCase():"select"}`,e.style.marginLeft="auto",e.style.cursor="pointer",t.options?.forEach((i=>{const o=document.createElement("option");o.value=i,o.text=i,i===t.value&&(o.selected=!0),e.appendChild(o)})),e.addEventListener("change",(e=>{const i=e.target;t.onChange&&t.onChange(i.value)})),o.appendChild(e)}else o=document.createElement("span"),o.classList.add("context-menu-item"),o.innerText=t.label||"Action",o.style.cursor="pointer",o.addEventListener("click",(e=>{e.stopPropagation(),t.action&&t.action()}));return e.appendChild(o),o}addMenuItem(e,t,i=!0,o=!1,s=1){const n=document.createElement("span");if(n.classList.add("context-menu-item"),n.innerText=e,o){const e=document.createElement("span");e.classList.add("submenu-arrow"),e.innerText="ː".repeat(s),n.appendChild(e)}n.addEventListener("click",(e=>{e.stopPropagation(),t(),i&&this.hideMenu()}));const r=["➩","➯","➱","➬","➫"];return n.addEventListener("mouseenter",(()=>{if(n.style.backgroundColor="royalblue",n.style.color="white",!n.querySelector(".hover-arrow")){const e=document.createElement("span");e.classList.add("hover-arrow");const t=Math.floor(Math.random()*r.length),i=r[t];e.innerText=i,e.style.marginLeft="auto",e.style.fontSize="14px",e.style.color="white",n.appendChild(e)}})),n.addEventListener("mouseleave",(()=>{n.style.backgroundColor="",n.style.color="";const e=n.querySelector(".hover-arrow");e&&n.removeChild(e)})),this.div.appendChild(n),this.items.push(n),n}clearMenu(){this.div.querySelectorAll(".context-menu-item:not(.close-menu), .context-submenu").forEach((e=>e.remove())),this.items=[]}addColorPickerMenuItem(e,t,i,o){const s=document.createElement("span");s.classList.add("context-menu-item"),s.innerText=e,this.div.appendChild(s);const n=e=>{const t=le(i,e);o.applyOptions(t),console.log(`Updated ${i} to ${e}`)};return s.addEventListener("click",(e=>{e.stopPropagation(),this.colorPicker||(this.colorPicker=new ne(t??"#000000",n)),this.colorPicker.openMenu(e,225,n)})),s}currentWidthOptions=[];currentStyleOptions=[];populateSeriesMenu(e,i){const o=l(e,this.handler.legend),s=e.options();if(!s)return void console.warn("No options found for the selected series.");this.div.innerHTML="";const n=[],r=[],a=[],c=[],d=[];for(const e of Object.keys(s)){const i=s[e];if(this.shouldSkipOption(e))continue;if(e.toLowerCase().includes("base"))continue;const o=he(e).toLowerCase();if(o.includes("color"))"string"==typeof i?n.push({label:e,value:i}):console.warn(`Expected string value for color option "${e}".`);else if(o.includes("width"))"number"==typeof i?c.push({name:e,label:e,value:i}):console.warn(`Expected number value for width option "${e}".`);else if(o.includes("visible")||o.includes("visibility"))"boolean"==typeof i?r.push({label:e,value:i}):console.warn(`Expected boolean value for visibility option "${e}".`);else if("lineType"===e){const t=this.getPredefinedOptions(he(e));d.push({name:e,label:e,value:i,options:t})}else if("crosshairMarkerRadius"===e)"number"==typeof i?c.push({name:e,label:e,value:i,min:1,max:50}):console.warn(`Expected number value for crosshairMarkerRadius option "${e}".`);else if(o.includes("style"))if("string"==typeof i||Object.values(t.LineStyle).includes(i)||"number"==typeof i){const t=["Solid","Dotted","Dashed","Large Dashed","Sparse Dotted"];d.push({name:e,label:e,value:i,options:t})}else console.warn(`Expected string/number value for style-related option "${e}".`);else a.push({label:e,value:i})}this.currentWidthOptions=c,this.currentStyleOptions=d,r.length>0&&this.addMenuItem("Visibility Options ▸",(()=>{this.populateVisibilityMenu(i,e)}),!1,!0),this.currentStyleOptions.length>0&&this.addMenuItem("Style Options ▸",(()=>{this.populateStyleMenu(i,e)}),!1,!0),this.currentWidthOptions.length>0&&this.addMenuItem("Width Options ▸",(()=>{this.populateWidthMenu(i,e)}),!1,!0),n.length>0&&this.addMenuItem("Color Options ▸",(()=>{this.populateColorOptionsMenu(n,e,i)}),!1,!0),a.forEach((t=>{const i=he(t.label);if(!this.constraints[t.label]?.skip)if("boolean"==typeof t.value)this.addMenuItem(`${i} ▸`,(()=>{this.div.innerHTML="";const i=!t.value,o=le(t.label,i);e.applyOptions(o),console.log(`Toggled ${t.label} to ${i}`)}),t.value);else if("string"==typeof t.value){const o=this.getPredefinedOptions(t.label);o&&o.length>0?this.addMenuItem(`${i} ▸`,(()=>{this.div.innerHTML="",this.addSelectInput(i,t.value,o,(i=>{const o=le(t.label,i);e.applyOptions(o),console.log(`Updated ${t.label} to ${i}`)}))}),!1,!0):this.addMenuItem(`${i} ▸`,(()=>{this.div.innerHTML="",this.addTextInput(i,t.value,(i=>{const o=le(t.label,i);e.applyOptions(o),console.log(`Updated ${t.label} to ${i}`)}))}),!1,!0)}else{if("number"!=typeof t.value)return;{const o=this.constraints[t.label]?.min,s=this.constraints[t.label]?.max;this.addMenuItem(`${i} ▸`,(()=>{this.div.innerHTML="",this.addNumberInput(i,t.value,(i=>{const o=le(t.label,i);e.applyOptions(o),console.log(`Updated ${t.label} to ${i}`)}),o,s)}),!1,!0)}}})),this.addMenuItem("Fill Area Between",(()=>{this.startFillAreaBetween(i,o)}),!1,!1);const h=o.primitives;console.log("Primitives:",h);const p=h?.FillArea??h?.pt;h.FillArea&&this.addMenuItem("Customize Fill Area",(()=>{this.customizeFillAreaOptions(i,p)}),!1,!0),this.addMenuItem("⤝ Main Menu",(()=>{this.populateChartMenu(i)}),!1,!1),this.showMenu(i)}populateDrawingMenu(e,t){this.div.innerHTML="";for(const t of Object.keys(e._options)){let e;if(t.toLowerCase().includes("color"))e=new re(this.saveDrawings,t);else{if("lineStyle"!==t)continue;e=new ae(this.saveDrawings)}const i=t=>e.openMenu(t);this.menuItem(he(t),i,(()=>{document.removeEventListener("click",e.closeMenu),e._div.style.display="none"}))}this.separator(),this.menuItem("Delete Drawing",(()=>this.drawingTool.delete(e))),this.addMenuItem("⤝ Main Menu",(()=>{this.populateChartMenu(t)}),!1,!1),this.showMenu(t)}populateChartMenu(e){this.div.innerHTML="",console.log("Displaying Menu Options: Chart"),this.addResetViewOption(),this.addMenuItem("⌯ Layout Options ",(()=>this.populateLayoutMenu(e)),!1,!0),this.addMenuItem("⌗ Grid Options ",(()=>this.populateGridMenu(e)),!1,!0),this.addMenuItem("⊹ Crosshair Options ",(()=>this.populateCrosshairOptionsMenu(e)),!1,!0),this.addMenuItem("ⴵ Time Scale Options ",(()=>this.populateTimeScaleMenu(e)),!1,!0),this.addMenuItem("$ Price Scale Options ",(()=>this.populatePriceScaleMenu(e,"right")),!1,!0),this.showMenu(e)}populateLayoutMenu(e){this.div.innerHTML="";const t="Text Color",i="layout.textColor",o=this.getCurrentOptionValue(i)||"#000000";this.addColorPickerMenuItem(he(t),o,i,this.handler.chart),this.addMenuItem("Background Options",(()=>this.populateBackgroundMenu(e)),!1,!0),this.addMenuItem("⤝ Main Menu",(()=>{this.populateChartMenu(e)}),!1,!1),this.showMenu(e)}populateWidthMenu(e,t){this.div.innerHTML="",this.currentWidthOptions.forEach((e=>{"number"==typeof e.value&&this.addNumberInput(he(e.label),e.value,(i=>{const o=le(e.name,i);t.applyOptions(o),console.log(`Updated ${e.label} to ${i}`)}),e.min,e.max)})),this.addMenuItem("⤝ Back to Series Options",(()=>{this.populateSeriesMenu(t,e)}),!1,!1),this.showMenu(e)}populateStyleMenu(e,t){this.div.innerHTML="",this.currentStyleOptions.forEach((e=>{const i=this.getPredefinedOptions(e.name);i&&this.addSelectInput(he(e.name),e.value.toString(),i,(o=>{const s=i.indexOf(o),n=le(e.name,s);t.applyOptions(n),console.log(`Updated ${e.name} to ${o}`)}))})),this.addMenuItem("⤝ Main Menu",(()=>{this.populateChartMenu(e)}),!1),this.showMenu(e)}populateLineTypeMenu(e,t,i){this.div.innerHTML="",i.options&&(this.addSelectInput(i.label,i.value.toString(),i.options,(e=>{const o="Simple"===e?0:1,s=le(i.name,o);t.applyOptions(s),console.log(`Updated ${i.label} to ${e}`)})),this.addMenuItem("⤝ Back to Style Options",(()=>{this.populateStyleMenu(e,t)}),!1),this.showMenu(e))}addTextInput(e,t,i){const o=document.createElement("div");o.classList.add("context-menu-item"),o.style.display="flex",o.style.alignItems="center",o.style.justifyContent="space-between";const s=document.createElement("label");s.innerText=e,s.htmlFor=`${e.toLowerCase()}-input`,s.style.marginRight="8px",o.appendChild(s);const n=document.createElement("input");return n.type="text",n.value=t,n.id=`${e.toLowerCase()}-input`,n.style.flex="1",n.style.marginLeft="auto",n.style.cursor="pointer",n.addEventListener("input",(e=>{const t=e.target;i(t.value)})),o.appendChild(n),this.div.appendChild(o),o}addSelectInput(e,t,i,o){const s=document.createElement("div");s.className="menu-item select-input";const n=document.createElement("span");n.innerText=e,s.appendChild(n);const r=document.createElement("select");i.forEach((e=>{const i=document.createElement("option");i.value=e,i.text=e,e===t&&(i.selected=!0),r.appendChild(i)})),r.addEventListener("change",(e=>{const t=e.target.value;o(t)})),s.appendChild(r),this.div.appendChild(s)}populateColorOptionsMenu(e,t,i){this.div.innerHTML="",e.forEach((e=>{this.addColorPickerMenuItem(he(e.label),e.value,e.label,t)})),this.addMenuItem("⤝ Back to Series Options",(()=>{this.populateSeriesMenu(t,i)}),!1,!1),this.showMenu(i)}populateVisibilityMenu(e,t){this.div.innerHTML="";const i=t.options();["visible","crosshairMarkerVisible","priceLineVisible"].forEach((e=>{const o=i[e];"boolean"==typeof o&&this.addCheckbox(he(e),o,(i=>{const o=le(e,i);t.applyOptions(o),console.log(`Toggled ${e} to ${i}`)}))})),this.addMenuItem("⤝ Back to Series Options",(()=>{this.populateSeriesMenu(t,e)}),!1,!1),this.showMenu(e)}populateBackgroundTypeMenu(e){this.div.innerHTML="";[{text:"Solid",action:()=>this.setBackgroundType(e,t.ColorType.Solid)},{text:"Vertical Gradient",action:()=>this.setBackgroundType(e,t.ColorType.VerticalGradient)}].forEach((e=>{this.addMenuItem(e.text,e.action,!1,!1,1)})),this.addMenuItem("⤝ Chart Menu",(()=>{this.populateChartMenu(e)}),!1),this.showMenu(e)}populateGradientBackgroundMenuInline(e,t){this.div.innerHTML="",this.addColorPickerMenuItem(he("Top Color"),t.topColor,"layout.background.topColor",this.handler.chart),this.addColorPickerMenuItem(he("Bottom Color"),t.bottomColor,"layout.background.bottomColor",this.handler.chart),this.addMenuItem("⤝ Background Type & Colors",(()=>{this.populateBackgroundTypeMenu(e)}),!1),this.showMenu(e)}populateGridMenu(e){this.div.innerHTML="";[{name:"Vertical Grid Color",valuePath:"grid.vertLines.color"},{name:"Horizontal Grid Color",valuePath:"grid.horzLines.color"}].forEach((e=>{const t=this.getCurrentOptionValue(e.valuePath)||"#FFFFFF";this.addColorPickerMenuItem(he(e.name),t,e.valuePath,this.handler.chart)})),this.addMenuItem("⤝ Main Menu",(()=>{this.populateChartMenu(e)}),!1),this.showMenu(e)}populateBackgroundMenu(e){this.div.innerHTML="",this.addMenuItem("Type & Colors",(()=>{this.populateBackgroundTypeMenu(e)}),!1,!0),this.addMenuItem("Options",(()=>{this.populateBackgroundOptionsMenu(e)}),!1,!0),this.addMenuItem("⤝ Layout Options",(()=>{this.populateLayoutMenu(e)}),!1),this.showMenu(e)}populateBackgroundOptionsMenu(e){this.div.innerHTML="";[{name:"Background Color",valuePath:"layout.background.color"},{name:"Background Top Color",valuePath:"layout.background.topColor"},{name:"Background Bottom Color",valuePath:"layout.background.bottomColor"}].forEach((e=>{const t=this.getCurrentOptionValue(e.valuePath)||"#FFFFFF";this.addColorPickerMenuItem(he(e.name),t,e.valuePath,this.handler.chart)})),this.addMenuItem("⤝ Background",(()=>{this.populateBackgroundMenu(e)}),!1),this.showMenu(e)}populateSolidBackgroundMenuInline(e,t){this.div.innerHTML="",this.addColorPickerMenuItem(he("Background Color"),t.color,"layout.background.color",this.handler.chart),this.addMenuItem("⤝ Type & Colors",(()=>{this.populateBackgroundTypeMenu(e)}),!1),this.showMenu(e)}populateCrosshairOptionsMenu(e){this.div.innerHTML="";[{name:"Line Color",valuePath:"crosshair.lineColor"},{name:"Vertical Line Color",valuePath:"crosshair.vertLine.color"},{name:"Horizontal Line Color",valuePath:"crosshair.horzLine.color"}].forEach((e=>{const t=this.getCurrentOptionValue(e.valuePath)||"#000000";this.addColorPickerMenuItem(he(e.name),t,e.valuePath,this.handler.chart)})),this.addMenuItem("⤝ Main Menu",(()=>{this.populateChartMenu(e)}),!1),this.showMenu(e)}populateTimeScaleMenu(e){this.div.innerHTML="";[{name:"Right Offset",type:"number",valuePath:"timeScale.rightOffset",min:0,max:100},{name:"Bar Spacing",type:"number",valuePath:"timeScale.barSpacing",min:1,max:100},{name:"Fix Left Edge",type:"boolean",valuePath:"timeScale.fixLeftEdge"},{name:"Border Color",type:"color",valuePath:"timeScale.borderColor"}].forEach((e=>{if("number"===e.type){const t=this.getCurrentOptionValue(e.valuePath);this.addNumberInput(he(e.name),t,(t=>{const i=le(e.valuePath,t);this.handler.chart.applyOptions(i),console.log(`Updated TimeScale ${e.name} to: ${t}`)}),e.min,e.max)}else if("boolean"===e.type){const t=this.getCurrentOptionValue(e.valuePath);this.addCheckbox(he(e.name),t,(t=>{const i=le(e.valuePath,t);this.handler.chart.applyOptions(i),console.log(`Updated TimeScale ${e.name} to: ${t}`)}))}else if("color"===e.type){const t=this.getCurrentOptionValue(e.valuePath)||"#000000";this.addColorPickerMenuItem(he(e.name),t,e.valuePath,this.handler.chart)}})),this.showMenu(e),this.addMenuItem("⤝ Main Menu",(()=>{this.populateChartMenu(e)}),!1)}populatePriceScaleMenu(e,t="right"){this.div.innerHTML="",this.addMenuItem("Set Price Scale Mode",(()=>{this.populatePriceScaleModeMenu(e,t)}),!0,!0,1),this.addMenuItem("⤝ Main Menu",(()=>{this.populateChartMenu(e)}),!1),this.showMenu(e)}populatePriceScaleModeMenu(e,i){this.div.innerHTML="";const o=this.handler.chart.priceScale(i).options().mode??t.PriceScaleMode.Normal;[{name:"Normal",value:t.PriceScaleMode.Normal},{name:"Logarithmic",value:t.PriceScaleMode.Logarithmic},{name:"Percentage",value:t.PriceScaleMode.Percentage},{name:"Indexed To 100",value:t.PriceScaleMode.IndexedTo100}].forEach((e=>{const t=o===e.value;this.addMenuItem(e.name,(()=>{this.applyPriceScaleOptions(i,{mode:e.value}),this.hideMenu(),console.log(`Price scale (${i}) mode set to: ${e.name}`)}),t,!1)})),this.addMenuItem("⤝ Back",(()=>{this.populatePriceScaleMenu(e,i)}),!1,!1,1),this.showMenu(e)}applyPriceScaleOptions(e,t){const i=this.handler.chart.priceScale(e);i?(i.applyOptions(t),console.log(`Applied options to price scale "${e}":`,t)):console.warn(`Price scale with ID "${e}" not found.`)}getCurrentOptionValue(e){const t=e.split(".");let i=this.handler.chart.options();for(const o of t){if(!i||!(o in i))return console.warn(`Option path "${e}" is invalid.`),null;i=i[o]}return i}mapStyleChoice(e){switch(e){case"Solid":default:return 0;case"Dotted":return 1;case"Dashed":return 2;case"Large Dashed":return 3;case"Sparse Dotted":return 4}}setBackgroundType(e,i){const o=this.handler.chart.options().layout?.background;let s;if(i===t.ColorType.Solid)s=o.type===t.ColorType.Solid?{type:t.ColorType.Solid,color:o.color}:{type:t.ColorType.Solid,color:"#FFFFFF"};else{if(i!==t.ColorType.VerticalGradient)return void console.error(`Unsupported ColorType: ${i}`);s=function(e){return e.type===t.ColorType.VerticalGradient}(o)?{type:t.ColorType.VerticalGradient,topColor:o.topColor,bottomColor:o.bottomColor}:{type:t.ColorType.VerticalGradient,topColor:"#FFFFFF",bottomColor:"#000000"}}this.handler.chart.applyOptions({layout:{background:s}}),i===t.ColorType.Solid?this.populateSolidBackgroundMenuInline(e,s):i===t.ColorType.VerticalGradient&&this.populateGradientBackgroundMenuInline(e,s)}startFillAreaBetween(e,t){console.log("Fill Area Between started. Origin series set:",t.options().title),this.populateSeriesListMenu(e,(e=>{e&&e!==t?(console.log("Destination series selected:",e.options().title),t.primitives.FillArea=new c(t,e,{...p}),t.attachPrimitive(t.primitives.FillArea,"Fill Area"),console.log("Fill Area successfully added between selected series."),alert(`Fill Area added between ${t.options().title} and ${e.options().title}`)):alert("Invalid selection. Please choose a different series as the destination.")}))}getPredefinedOptions(e){return{"Series Type":["Line","Histogram","Area","Bar","Candlestick"],"Line Style":["Solid","Dotted","Dashed","Large Dashed","Sparse Dotted"],"Line Type":["Simple","WithSteps","Curved"],seriesType:["Line","Histogram","Area","Bar","Candlestick"],lineStyle:["Solid","Dotted","Dashed","Large Dashed","Sparse Dotted"],lineType:["Simple","WithSteps","Curved"]}[he(e)]||null}populateSeriesListMenu(e,t){this.div.innerHTML="";Array.from(this.handler.seriesMap.entries()).map((([e,t])=>({label:e,value:t}))).forEach((e=>{this.addMenuItem(e.label,(()=>{t(e.value),this.hideMenu()}))})),this.addMenuItem("Cancel",(()=>{console.log("Operation canceled."),this.hideMenu()})),this.showMenu(e)}customizeFillAreaOptions(e,t){this.div.innerHTML="",this.addColorPickerMenuItem("Origin Top Color",t.options.originColor,"originColor",t),this.addColorPickerMenuItem("Destination Top Color",t.options.destinationColor,"destinationColor",t),this.addMenuItem("⤝ Back to Main Menu",(()=>this.populateChartMenu(e)),!1),this.showMenu(e)}addResetViewOption(){const e=this.addMenuItem("Reset chart view ⟲",(()=>{this.resetView()}));this.div.appendChild(e)}}var ue;function me(e){switch(e.trim().toLowerCase()){case"rectangle":return ue.Rectangle;case"rounded":return ue.Rounded;case"ellipse":return ue.Ellipse;case"arrow":return ue.Arrow;case"3d":return ue.Cube;case"polygon":return ue.Polygon;default:return console.warn(`Unknown CandleShape: ${e}`),ue.Rectangle}}function ge(e,t){if(e.startsWith("#"))return function(e,t){if(e=e.replace(/^#/,""),!/^([0-9A-F]{3}){1,2}$/i.test(e))throw new Error("Invalid hex color format.");const[i,o,s]=(e=>3===e.length?[parseInt(e[0]+e[0],16),parseInt(e[1]+e[1],16),parseInt(e[2]+e[2],16)]:[parseInt(e.slice(0,2),16),parseInt(e.slice(2,4),16),parseInt(e.slice(4,6),16)])(e);return`rgba(${i}, ${o}, ${s}, ${t})`}(e,t);if(e.startsWith("rgba")||e.startsWith("rgb"))return e.replace(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*[\d.]+)?\)/,`rgba($1, $2, $3, ${t})`);throw new Error("Unsupported color format. Use hex, rgb, or rgba.")}function _e(e,t=.2){let[i,o,s,n=1]=e.startsWith("#")?[...(r=e,3===(r=r.replace(/^#/,"")).length?[parseInt(r[0]+r[0],16),parseInt(r[1]+r[1],16),parseInt(r[2]+r[2],16)]:[parseInt(r.slice(0,2),16),parseInt(r.slice(2,4),16),parseInt(r.slice(4,6),16)]),1]:e.match(/\d+(\.\d+)?/g).map(Number);var r;return i=Math.max(0,Math.min(255,i*(1-t))),o=Math.max(0,Math.min(255,o*(1-t))),s=Math.max(0,Math.min(255,s*(1-t))),e.startsWith("#")?`#${((1<<24)+(Math.round(i)<<16)+(Math.round(o)<<8)+Math.round(s)).toString(16).slice(1)}`:`rgba(${Math.round(i)}, ${Math.round(o)}, ${Math.round(s)}, ${n})`}function ye(e){return function(e){return Math.max(1,Math.floor(e))}(e)/e}!function(e){e.Rectangle="Rectangle",e.Rounded="Rounded",e.Ellipse="Ellipse",e.Arrow="Arrow",e.Cube="3d",e.Polygon="Polygon"}(ue||(ue={}));class ve{_options;constructor(e){this._options=e}aggregate(e,t){const i=this._options?.chandelierSize??1,o=[];for(let s=0;se.originalData?.high??e.high)),d=e.map((e=>e.originalData?.low??e.low)),h=c.length>0?Math.max(...c):0,p=d.length>0?Math.min(...d):0,u=o(h)??0,m=o(p)??0,g=e[0].x,_=r>n,y=_?this._options?.upColor||"rgba(0,255,0,0.333)":this._options?.downColor||"rgba(255,0,0,0.333)",v=_?this._options?.borderUpColor||ge(y,1):this._options?.borderDownColor||ge(y,1),b=_?this._options?.wickUpColor||v:this._options?.wickDownColor||v,f=e.reduce(((e,t)=>t.lineStyle??t.originalData?.lineStyle??e),this._options?.lineStyle??1),w=e.reduce(((e,t)=>t.lineWidth??t.originalData?.lineWidth??e),this._options?.lineWidth??1);return{open:a,high:u,low:m,close:l,x:g,isUp:_,startIndex:t,endIndex:i,isInProgress:s,color:y,borderColor:v,wickColor:b,shape:e.reduce(((e,t)=>(t.shape?me(t.shape):t.originalData?.shape?me(t.originalData.shape):void 0)??e),this._options?.shape??ue.Rectangle)||ue.Rectangle,lineStyle:f,lineWidth:w}}}class be{_data=null;_options=null;_aggregator=null;draw(e,t){e.useBitmapCoordinateSpace((e=>this._drawImpl(e,t)))}update(e,t){this._data=e,this._options=t,this._aggregator=new ve(t)}_drawImpl(e,t){if(!this._data||0===this._data.bars.length||!this._data.visibleRange||!this._options)return;const i=this._data.bars.map(((e,t)=>({open:e.originalData?.open??0,high:e.originalData?.high??0,low:e.originalData?.low??0,close:e.originalData?.close??0,x:e.x,shape:e.originalData?.shape??this._options?.shape??"Rectangle",lineStyle:e.originalData?.lineStyle??this._options?.lineStyle??1,lineWidth:e.originalData?.lineWidth??this._options?.lineWidth??1,isUp:(e.originalData?.close??0)>=(e.originalData?.open??0),color:this._options?.color??"rgba(0,0,0,0)",borderColor:this._options?.borderColor??"rgba(0,0,0,0)",wickColor:this._options?.wickColor??"rgba(0,0,0,0)",startIndex:t,endIndex:t}))),o=this._aggregator?.aggregate(i,t)??[],s=this._options.radius(this._data.barSpacing),{horizontalPixelRatio:n,verticalPixelRatio:r}=e,a=this._data.barSpacing*n;this._drawCandles(e,o,this._data.visibleRange,s,a,n,r),this._drawWicks(e,o,this._data.visibleRange)}_drawWicks(e,t,i){if(null===this._data||null===this._options)return;if("3d"===this._options.shape)return;const{context:o,horizontalPixelRatio:s,verticalPixelRatio:n}=e,r=this._data.barSpacing*s,a=ye(s);for(const e of t){if(e.startIndexi.to)continue;const t=e.low*n,l=e.high*n,c=Math.min(e.open,e.close)*n,d=Math.max(e.open,e.close)*n;let h=e.x*s;const p=e.endIndex-e.startIndex;p&&p>1&&(h+=r*Math.max(1,p)/2);let u=l,m=c,g=d,_=t;"Polygon"===this._options.shape&&(m=(l+c)/2,g=(t+d)/2),o.fillStyle=e.color,o.strokeStyle=e.wickColor??e.color;const y=(e,t,i,s,n)=>{o.roundRect?o.roundRect(e,t,i,s,n):o.rect(e,t,i,s)},v=m-u;v>0&&(o.beginPath(),y(h-Math.floor(a/2),u,a,v,a/2),o.fill(),o.stroke());const b=_-g;b>0&&(o.beginPath(),y(h-Math.floor(a/2),g,a,b,a/2),o.fill(),o.stroke())}}_drawCandles(e,t,i,o,s,n,r){const{context:a}=e,l=this._options?.barSpacing??.8;a.save();for(const e of t){const t=e.endIndex-e.startIndex,c=1!==this._options?.chandelierSize?s*Math.max(1,t+1)-(1-l)*s:s*l,d=e.x*n,h=s*l;if(e.startIndexi.to)continue;const p=Math.min(e.open,e.close)*r,u=Math.max(e.open,e.close)*r,m=p-u,g=(p+u)/2,_=d-h/2,y=_+c,v=_+c/2;switch(a.fillStyle=e.color??this._options?.color??"rgba(255,255,255,1)",a.strokeStyle=e.borderColor??this._options?.borderColor??e.color??"rgba(255,255,255,1)",L(a,e.lineStyle),a.lineWidth=e.lineWidth??1,e.shape){case"Rectangle":default:this._drawCandle(a,_,y,g,m);break;case"Rounded":this._drawRounded(a,_,y,g,m,o);break;case"Ellipse":this._drawEllipse(a,_,y,v,g,m);break;case"Arrow":this._drawArrow(a,_,y,v,g,m,e.high*r,e.low*r,e.isUp);break;case"3d":this._draw3d(a,d,e.high*r,e.low*r,e.open*r,e.close*r,h,c,e.color,e.borderColor,e.isUp,l);break;case"Polygon":this._drawPolygon(a,_,y,g,m,e.high*r,e.low*r,e.isUp)}}a.restore()}_drawCandle(e,t,i,o,s){const n=o-s/2,r=o+s/2;e.beginPath(),e.moveTo(t,n),e.lineTo(t,r),e.lineTo(i,r),e.lineTo(i,n),e.closePath(),e.fill(),e.stroke()}_drawRounded(e,t,i,o,s,n){const r=o-s/2,a=i-t,l=Math.abs(Math.min(n,.1*Math.min(a,s),5));e.beginPath(),e.roundRect?e.roundRect(t,r,a,s,l):e.rect(t,r,a,s),e.fill(),e.stroke()}_drawEllipse(e,t,i,o,s,n){const r=(i-t)/2,a=n/2,l=o;e.beginPath(),e.ellipse(l,s,Math.abs(r),Math.abs(a),0,0,2*Math.PI),e.fill(),e.stroke()}_draw3d(e,t,i,o,s,n,r,a,l,c,d,h){const p=-Math.max(a,1)*(1-h),u=_e(l,.666),m=_e(l,.333),g=_e(l,.2),_=t-r/2,y=t-r/2+a+p,v=_-p,b=y-p;let f,w,C,x;d?(f=s,w=o,C=i,x=n):(f=s,w=i,C=o,x=n),e.fillStyle=m,e.strokeStyle=c,e.fillStyle=g,d?(e.fillStyle=u,e.beginPath(),e.moveTo(_,w),e.lineTo(v,x),e.lineTo(b,x),e.lineTo(y,w),e.closePath(),e.fill(),e.stroke(),e.fillStyle=u,e.beginPath(),e.moveTo(_,f),e.lineTo(v,C),e.lineTo(v,x),e.lineTo(_,w),e.closePath(),e.fill(),e.stroke(),e.fillStyle=u,e.beginPath(),e.moveTo(y,f),e.lineTo(b,C),e.lineTo(b,x),e.lineTo(y,w),e.closePath(),e.fill(),e.stroke(),e.fillStyle=g,e.beginPath(),e.moveTo(_,f),e.lineTo(v,C),e.lineTo(b,C),e.lineTo(y,f),e.closePath(),e.fill(),e.stroke()):(e.fillStyle=g,e.beginPath(),e.moveTo(_,f),e.lineTo(v,C),e.lineTo(b,C),e.lineTo(y,f),e.closePath(),e.fill(),e.stroke(),e.fillStyle=m,e.beginPath(),e.moveTo(y,f),e.lineTo(b,C),e.lineTo(b,x),e.lineTo(y,w),e.closePath(),e.fill(),e.stroke(),e.fillStyle=m,e.beginPath(),e.moveTo(_,f),e.lineTo(v,C),e.lineTo(v,x),e.lineTo(_,w),e.closePath(),e.fill(),e.stroke(),e.fillStyle=m,e.beginPath(),e.moveTo(_,w),e.lineTo(v,x),e.lineTo(b,x),e.lineTo(y,w),e.closePath(),e.fill(),e.stroke())}_drawPolygon(e,t,i,o,s,n,r,a){const l=o+s/2,c=o-s/2;e.save(),e.beginPath(),a?(e.moveTo(t,l),e.lineTo(i,n),e.lineTo(i,c),e.lineTo(t,r)):(e.moveTo(t,n),e.lineTo(i,l),e.lineTo(i,r),e.lineTo(t,c)),e.closePath(),e.stroke(),e.fill(),e.restore()}_drawArrow(e,t,i,o,s,n,r,a,l){e.save(),e.beginPath(),l?(e.moveTo(t,a),e.lineTo(t,s+n/2),e.lineTo(o,r),e.lineTo(i,s+n/2),e.lineTo(i,a),e.lineTo(o,s-n/2),e.lineTo(t,a)):(e.moveTo(t,r),e.lineTo(t,s-n/2),e.lineTo(o,a),e.lineTo(i,s-n/2),e.lineTo(i,r),e.lineTo(o,s+n/2),e.lineTo(t,r)),e.closePath(),e.fill(),e.stroke(),e.restore()}}const fe={...t.customSeriesDefaultOptions,upColor:"#26a69a",downColor:"#ef5350",wickVisible:!0,borderVisible:!0,borderColor:"#378658",borderUpColor:"#26a69a",borderDownColor:"#ef5350",wickColor:"#737375",wickUpColor:"#26a69a",wickDownColor:"#ef5350",radius:function(e){return e<4?0:e/3},shape:"Rectangle",chandelierSize:1,barSpacing:.8,lineStyle:0,lineWidth:1};class we{_renderer;constructor(){this._renderer=new be}priceValueBuilder(e){return[e.high,e.low,e.close]}renderer(){return this._renderer}isWhitespace(e){return void 0===e.close}update(e,t){this._renderer.update(e,t)}defaultOptions(){return fe}}g();class Ce{id;commandFunctions=[];static handlers=new Map;seriesOriginMap=new WeakMap;wrapper;div;chart;scale;precision=2;series;volumeSeries;legend;_topBar;toolBox;spinner;_seriesList=[];seriesMap=new Map;seriesMetadata;ContextMenu;currentMouseEventParams=null;constructor(e,t,i,o,s){this.reSize=this.reSize.bind(this),this.id=e,this.scale={width:t,height:i},Ce.handlers.set(e,this),this.wrapper=document.createElement("div"),this.wrapper.classList.add("handler"),this.wrapper.style.float=o,this.div=document.createElement("div"),this.div.style.position="relative",this.wrapper.appendChild(this.div),window.containerDiv.append(this.wrapper),this.chart=this._createChart(),this.series=this.createCandlestickSeries(),this.volumeSeries=this.createVolumeSeries(),this.series.applyOptions,this.legend=new f(this),this.chart.subscribeCrosshairMove((e=>{this.currentMouseEventParams=e,window.MouseEventParams=e})),document.addEventListener("keydown",(e=>{for(let t=0;t{window.handlerInFocus=this.id,window.MouseEventParams=this.currentMouseEventParams||null})),this.seriesMetadata=new WeakMap,this.reSize(),s&&(window.addEventListener("resize",(()=>this.reSize())),this.chart.subscribeCrosshairMove((e=>{this.currentMouseEventParams=e})),this.ContextMenu=new pe(this,Ce.handlers,(()=>window.MouseEventParams??null)))}reSize(){let e=0!==this.scale.height&&this._topBar?._div.offsetHeight||0;this.chart.resize(window.innerWidth*this.scale.width,window.innerHeight*this.scale.height-e),this.wrapper.style.width=100*this.scale.width+"%",this.wrapper.style.height=100*this.scale.height+"%",0===this.scale.height||0===this.scale.width?this.toolBox&&(this.toolBox.div.style.display="none"):this.toolBox&&(this.toolBox.div.style.display="flex")}primitives=new Map;_createChart(){return t.createChart(this.div,{width:window.innerWidth*this.scale.width,height:window.innerHeight*this.scale.height,layout:{textColor:window.pane.color,background:{color:"#000000",type:t.ColorType.Solid},fontSize:12},rightPriceScale:{scaleMargins:{top:.3,bottom:.25}},timeScale:{timeVisible:!0,secondsVisible:!1},crosshair:{mode:t.CrosshairMode.Normal,vertLine:{labelBackgroundColor:"rgb(46, 46, 46)"},horzLine:{labelBackgroundColor:"rgb(55, 55, 55)"}},grid:{vertLines:{color:"rgba(29, 30, 38, 5)"},horzLines:{color:"rgba(29, 30, 58, 5)"}},handleScroll:{vertTouchDrag:!0}})}createCandlestickSeries(){const e="rgba(39, 157, 130, 100)",t="rgba(200, 97, 100, 100)",i=this.chart.addCandlestickSeries({upColor:e,borderUpColor:e,wickUpColor:e,downColor:t,borderDownColor:t,wickDownColor:t});i.priceScale().applyOptions({scaleMargins:{top:.2,bottom:.2}});const o=r(i,this.legend);return o.applyOptions({title:"candles"}),o}createVolumeSeries(){const e=this.chart.addHistogramSeries({color:"#26a69a",priceFormat:{type:"volume"},priceScaleId:"volume_scale"});e.priceScale().applyOptions({scaleMargins:{top:.8,bottom:0}});const t=r(e,this.legend);return t.applyOptions({title:"Volume"}),t}createLineSeries(e,t){const{group:i,legendSymbol:o="▨",...s}=t,n=r(this.chart.addLineSeries(s),this.legend);n.applyOptions({title:e}),this._seriesList.push(n),this.seriesMap.set(e,n);const a=n.options().color||"rgba(255,0,0,1)",l={name:e,series:n,colors:[a.startsWith("rgba")?a.replace(/[^,]+(?=\))/,"1"):a],legendSymbol:Array.isArray(o)?o:o?[o]:[],seriesType:"Line",group:i};return this.legend.addLegendItem(l),{name:e,series:n}}createHistogramSeries(e,t){const{group:i,legendSymbol:o="▨",...s}=t,n=r(this.chart.addHistogramSeries(s),this.legend);n.applyOptions({title:e}),this._seriesList.push(n),this.seriesMap.set(e,n);const a=n.options().color||"rgba(255,0,0,1)",l={name:e,series:n,colors:[a.startsWith("rgba")?a.replace(/[^,]+(?=\))/,"1"):a],legendSymbol:Array.isArray(o)?o:[o],seriesType:"Histogram",group:i};return this.legend.addLegendItem(l),{name:e,series:n}}createAreaSeries(e,t){const{group:i,legendSymbol:o="▨",...s}=t,n=r(this.chart.addAreaSeries(s),this.legend);this._seriesList.push(n),this.seriesMap.set(e,n);const a=n.options().lineColor||"rgba(255,0,0,1)",l={name:e,series:n,colors:[a.startsWith("rgba")?a.replace(/[^,]+(?=\))/,"1"):a],legendSymbol:Array.isArray(o)?o:o?[o]:[],seriesType:"Area",group:i};return this.legend.addLegendItem(l),{name:e,series:n}}createBarSeries(e,t){const{group:i,legendSymbol:o=["▨","▨"],...s}=t,n=this.chart.addBarSeries(s),a=r(n,this.legend);a.applyOptions({title:e}),this._seriesList.push(a),this.seriesMap.set(e,a);const l=a.options().upColor||"rgba(0,255,0,1)",c=a.options().downColor||"rgba(255,0,0,1)",d={name:e,series:a,colors:[l,c],legendSymbol:Array.isArray(o)?o:o?[o]:[],seriesType:"Bar",group:i};return this.legend.addLegendItem(d),{name:e,series:n}}createCustomOHLCSeries(e,t={}){const i="ohlc",o={...fe,...t,seriesType:i},{group:s,legendSymbol:n=["⑃","⑂"],chandelierSize:a=1,...l}=o,c=new we,d=this.chart.addCustomSeries(c,{...l,chandelierSize:a}),h=r(d,this.legend);this._seriesList.push(h),this.seriesMap.set(e,h);const p={name:e,series:h,colors:[o.borderUpColor||o.upColor,o.borderDownColor||o.downColor],legendSymbol:a>1?n.map((e=>`${e} (${a})`)):n,seriesType:i,group:s};return this.legend.addLegendItem(p),{name:e,series:d}}createFillArea(e,t,i,o,s){const n=this._seriesList.find((e=>e.options()?.title===t)),r=this._seriesList.find((e=>e.options()?.title===i));if(!n)return void console.warn(`Origin series with title "${t}" not found.`);if(!r)return void console.warn(`Destination series with title "${i}" not found.`);const a=l(n,this.legend),d=new c(n,r,{originColor:o||null,destinationColor:s||null,lineWidth:null});return a.attachPrimitive(d,e),d}attachPrimitive(e,t,i,o){let s=i;try{if(o&&!i&&(s=this.seriesMap.get(o)),!s)return void console.warn(`Series with the name "${o}" not found.`);const n=l(s,this.legend);let r;if("Tooltip"!==t)return void console.warn(`Unknown primitive type: ${t}`);r=new se({lineColor:e}),n.attachPrimitive(r,"Tooltip"),this.primitives.set(s,r)}catch(e){console.error(`Failed to attach ${t}:`,e)}}removeSeries(e){const t=this.seriesMap.get(e);t&&(this.chart.removeSeries(t),this._seriesList=this._seriesList.filter((e=>e!==t)),this.seriesMap.delete(e),this.legend.deleteLegendEntry(e),console.log(`Series "${e}" removed.`))}createToolBox(){this.toolBox=new q(this,this.id,this.chart,this.series,this.commandFunctions),this.div.appendChild(this.toolBox.div)}createTopBar(){return this._topBar=new X(this),this.wrapper.prepend(this._topBar._div),this._topBar}toJSON(){const{chart:e,...t}=this;return t}extractSeriesData(e){const t=e.data();return Array.isArray(t)?t.map((e=>[e.time,e.value||e.close||0])):(console.warn("Failed to extract data: series data is not in array format."),[])}static syncCharts(e,t,i=!1){function o(e,t){t?(e.chart.setCrosshairPosition(t.value||t.close,t.time,e.series),e.legend.legendHandler(t,!0)):e.chart.clearCrosshairPosition()}function s(e,t){return t.time&&t.seriesData.get(e)||null}const n=e.chart.timeScale(),r=t.chart.timeScale(),a=e=>{e&&n.setVisibleLogicalRange(e)},l=e=>{e&&r.setVisibleLogicalRange(e)},c=i=>{o(t,s(e.series,i))},d=i=>{o(e,s(t.series,i))};let h=t;function p(e,t,o,s,n,r){e.wrapper.addEventListener("mouseover",(()=>{h!==e&&(h=e,t.chart.unsubscribeCrosshairMove(o),e.chart.subscribeCrosshairMove(s),i||(t.chart.timeScale().unsubscribeVisibleLogicalRangeChange(n),e.chart.timeScale().subscribeVisibleLogicalRangeChange(r)))}))}p(t,e,c,d,l,a),p(e,t,d,c,a,l),t.chart.subscribeCrosshairMove(d);const u=r.getVisibleLogicalRange();u&&n.setVisibleLogicalRange(u),i||t.chart.timeScale().subscribeVisibleLogicalRangeChange(a)}static makeSearchBox(e){const t=document.createElement("div");t.classList.add("searchbox"),t.style.display="none";const i=document.createElement("div");i.innerHTML='';const o=document.createElement("input");return o.type="text",t.appendChild(i),t.appendChild(o),e.div.appendChild(t),e.commandFunctions.push((i=>window.handlerInFocus===e.id&&!window.textBoxFocused&&("none"===t.style.display?!!/^[a-zA-Z0-9]$/.test(i.key)&&(t.style.display="flex",o.focus(),!0):("Enter"===i.key||"Escape"===i.key)&&("Enter"===i.key&&window.callbackFunction(`search${e.id}_~_${o.value}`),t.style.display="none",o.value="",!0)))),o.addEventListener("input",(()=>o.value=o.value.toUpperCase())),{window:t,box:o}}static makeSpinner(e){e.spinner=document.createElement("div"),e.spinner.classList.add("spinner"),e.wrapper.appendChild(e.spinner);let t=0;!function i(){e.spinner&&(t+=10,e.spinner.style.transform=`translate(-50%, -50%) rotate(${t}deg)`,requestAnimationFrame(i))}()}static _styleMap={"--bg-color":"backgroundColor","--hover-bg-color":"hoverBackgroundColor","--click-bg-color":"clickBackgroundColor","--active-bg-color":"activeBackgroundColor","--muted-bg-color":"mutedBackgroundColor","--border-color":"borderColor","--color":"color","--active-color":"activeColor"};static setRootStyles(e){const t=document.documentElement.style;for(const[i,o]of Object.entries(this._styleMap))t.setProperty(i,e[o])}}return e.Box=V,e.FillArea=c,e.Handler=Ce,e.HorizontalLine=P,e.Legend=f,e.RayLine=H,e.Table=class{_div;callbackName;borderColor;borderWidth;table;rows={};headings;widths;alignments;footer;header;constructor(e,t,i,o,s,n,r=!1,a,l,c,d,h){this._div=document.createElement("div"),this.callbackName=null,this.borderColor=l,this.borderWidth=c,r?(this._div.style.position="absolute",this._div.style.cursor="move"):(this._div.style.position="relative",this._div.style.float=n),this._div.style.zIndex="2000",this.reSize(e,t),this._div.style.display="flex",this._div.style.flexDirection="column",this._div.style.borderRadius="5px",this._div.style.color="white",this._div.style.fontSize="12px",this._div.style.fontVariantNumeric="tabular-nums",this.table=document.createElement("table"),this.table.style.width="100%",this.table.style.borderCollapse="collapse",this._div.style.overflow="hidden",this.headings=i,this.widths=o.map((e=>100*e+"%")),this.alignments=s;let p=this.table.createTHead().insertRow();for(let e=0;e0?h[e]:a,t.style.color=d[e],p.appendChild(t)}let u,m,g=document.createElement("div");if(g.style.overflowY="auto",g.style.overflowX="hidden",g.style.backgroundColor=a,g.appendChild(this.table),this._div.appendChild(g),window.containerDiv.appendChild(this._div),!r)return;let _=e=>{this._div.style.left=e.clientX-u+"px",this._div.style.top=e.clientY-m+"px"},y=()=>{document.removeEventListener("mousemove",_),document.removeEventListener("mouseup",y)};this._div.addEventListener("mousedown",(e=>{u=e.clientX-this._div.offsetLeft,m=e.clientY-this._div.offsetTop,document.addEventListener("mousemove",_),document.addEventListener("mouseup",y)}))}divToButton(e,t){e.addEventListener("mouseover",(()=>e.style.backgroundColor="rgba(60, 60, 60, 0.6)")),e.addEventListener("mouseout",(()=>e.style.backgroundColor="transparent")),e.addEventListener("mousedown",(()=>e.style.backgroundColor="rgba(60, 60, 60)")),e.addEventListener("click",(()=>window.callbackFunction(t))),e.addEventListener("mouseup",(()=>e.style.backgroundColor="rgba(60, 60, 60, 0.6)"))}newRow(e,t=!1){let i=this.table.insertRow();i.style.cursor="default";for(let o=0;o{e&&(window.cursor=e),document.body.style.cursor=window.cursor},e}({},LightweightCharts); diff --git a/src/context-menu/color-picker_.ts b/src/context-menu/color-picker_.ts new file mode 100644 index 0000000..8ea48e5 --- /dev/null +++ b/src/context-menu/color-picker_.ts @@ -0,0 +1,291 @@ + +export class ColorPicker { + + + private container: HTMLDivElement; + private _opacitySlider!: HTMLInputElement; + private _opacity_label!: HTMLDivElement; + private exitButton!: HTMLDivElement; + public color: string = "#ff0000"; + private rgba: number[]; // [R, G, B, A] + private opacity: number; + private applySelection: (color: string) => void; + +constructor(initialValue: string, applySelection: (color: string) => void) { + this.applySelection = applySelection; + this.rgba = ColorPicker.extractRGBA(initialValue); + this.opacity = this.rgba[3]; + this.container = document.createElement("div"); + this.container.classList.add("color-picker"); + this.container.style.display = "flex"; + this.container.style.flexDirection = "column"; + this.container.style.width = "150px"; + this.container.style.height = "300px"; + this.container.style.position = "relative"; // Ensure proper positioning for the exit button. + + // Build UI elements + const colorGrid = this.createColorGrid(); + const opacityUI = this.createOpacityUI(); + this.exitButton = this.createExitButton(); // Create the exit button. + + // Append elements to the container + this.container.appendChild(colorGrid); + this.container.appendChild(this.createSeparator()); + this.container.appendChild(this.createSeparator()); + this.container.appendChild(opacityUI); + this.container.appendChild(this.exitButton); // Append the exit button last +} + + + private createExitButton(): HTMLDivElement { + const button = document.createElement('div'); + button.innerText = '✕'; // Close icon + button.title = 'Close'; + button.style.position = 'absolute'; + button.style.bottom = '5px'; // Move to the bottom + button.style.right = '5px'; // Default bottom-right corner + button.style.width = '20px'; + button.style.height = '20px'; + button.style.cursor = 'pointer'; + button.style.display = 'flex'; + button.style.justifyContent = 'center'; + button.style.alignItems = 'center'; + button.style.fontSize = '16px'; + button.style.backgroundColor = '#ccc'; + button.style.borderRadius = '50%'; + button.style.color = '#000'; + button.style.boxShadow = '0 1px 3px rgba(0,0,0,0.3)'; + + // Add hover effect + button.addEventListener('mouseover', () => { + button.style.backgroundColor = '#e74c3c'; // Red hover color + button.style.color = '#fff'; // White text on hover + }); + button.addEventListener('mouseout', () => { + button.style.backgroundColor = '#ccc'; + button.style.color = '#000'; + }); + + // Close the menu when clicked + button.addEventListener('click', () => { + this.closeMenu(); + }); + + return button; + } + private createColorGrid(): HTMLDivElement { + const colorGrid = document.createElement('div'); + colorGrid.style.display = 'grid'; + colorGrid.style.gridTemplateColumns = 'repeat(7, 1fr)'; // 5 columns + colorGrid.style.gap = '5px'; + colorGrid.style.overflowY = 'auto'; + colorGrid.style.flex = '1'; + + const colors = ColorPicker.generateFullSpectrumColors(9); // Generate vibrant colors + colors.forEach((color) => { + const box = this.createColorBox(color); + colorGrid.appendChild(box); + }); + + return colorGrid; + } + + private createColorBox(color: string): HTMLDivElement { + const box = document.createElement("div"); + box.style.aspectRatio = "1"; // Maintain square shape + box.style.borderRadius = "6px"; + box.style.backgroundColor = color; + box.style.cursor = "pointer"; + + box.addEventListener("click", () => { + this.rgba = ColorPicker.extractRGBA(color); + this.updateTargetColor(); + }); + + return box; + } + + + private static generateFullSpectrumColors(stepsPerTransition: number): string[] { + const colors: string[] = []; + + // Red to Green (255, 0, 0 → 255, 255, 0) + for (let g = 0; g <= 255; g += Math.floor(255 / stepsPerTransition)) { + colors.push(`rgba(255, ${g}, 0, 1)`); + } + + // Green to Yellow-Green to Green-Blue (255, 255, 0 → 0, 255, 0) + for (let r = 255; r >= 0; r -= Math.floor(255 / stepsPerTransition)) { + colors.push(`rgba(${r}, 255, 0, 1)`); + } + + // Green to Cyan (0, 255, 0 → 0, 255, 255) + for (let b = 0; b <= 255; b += Math.floor(255 / stepsPerTransition)) { + colors.push(`rgba(0, 255, ${b}, 1)`); + } + + // Cyan to Blue (0, 255, 255 → 0, 0, 255) + for (let g = 255; g >= 0; g -= Math.floor(255 / stepsPerTransition)) { + colors.push(`rgba(0, ${g}, 255, 1)`); + } + + // Blue to Magenta (0, 0, 255 → 255, 0, 255) + for (let r = 0; r <= 255; r += Math.floor(255 / stepsPerTransition)) { + colors.push(`rgba(${r}, 0, 255, 1)`); + } + + // Magenta to Red (255, 0, 255 → 255, 0, 0) + for (let b = 255; b >= 0; b -= Math.floor(255 / stepsPerTransition)) { + colors.push(`rgba(255, 0, ${b}, 1)`); + } + + // White to Black (255, 255, 255 → 0, 0, 0) + for (let i = 255; i >= 0; i -= Math.floor(255 / stepsPerTransition)) { + colors.push(`rgba(${i}, ${i}, ${i}, 1)`); + } + + return colors; + } + private createOpacityUI(): HTMLDivElement { + const opacityContainer = document.createElement("div"); + opacityContainer.style.margin = "10px"; + opacityContainer.style.display = "flex"; + opacityContainer.style.flexDirection = "column"; + opacityContainer.style.alignItems = "center"; + + const opacityText = document.createElement("div"); + opacityText.style.color = "lightgray"; + opacityText.style.fontSize = "12px"; + opacityText.innerText = "Opacity"; + + this._opacitySlider = document.createElement("input"); + this._opacitySlider.type = "range"; + this._opacitySlider.min = "0"; + this._opacitySlider.max = "100"; + this._opacitySlider.value = (this.opacity * 100).toString(); + this._opacitySlider.style.width = "80%"; + + this._opacity_label = document.createElement("div"); + this._opacity_label.style.color = "lightgray"; + this._opacity_label.style.fontSize = "12px"; + this._opacity_label.innerText = `${this._opacitySlider.value}%`; + + this._opacitySlider.oninput = () => { + this._opacity_label.innerText = `${this._opacitySlider.value}%`; + this.opacity = parseInt(this._opacitySlider.value) / 100; + this.updateTargetColor(); + }; + + opacityContainer.appendChild(opacityText); + opacityContainer.appendChild(this._opacitySlider); + opacityContainer.appendChild(this._opacity_label); + + return opacityContainer; + } + + + + private createSeparator(): HTMLDivElement { + const separator = document.createElement("div"); + separator.style.height = "1px"; + separator.style.width = "100%"; + separator.style.backgroundColor = "#ccc"; + separator.style.margin = "5px 0"; + return separator; + } + public openMenu( + event: MouseEvent, + parentMenuWidth: number, // Width of the parent menu + applySelection: (color: string) => void + ): void { + this.applySelection = applySelection; + + // Attach menu to the DOM temporarily to calculate dimensions + this.container.style.display = 'block'; + document.body.appendChild(this.container); + + console.log('Menu attached:', this.container); + + // Calculate submenu dimensions + const submenuWidth = this.container.offsetWidth || 150; // Default submenu width + const submenuHeight = this.container.offsetHeight || 250; // Default submenu height + + console.log('Submenu dimensions:', { submenuWidth, submenuHeight }); + + // Get mouse position + const cursorX = event.clientX; + const cursorY = event.clientY; + + console.log('Mouse position:', { cursorX, cursorY }); + + // Get viewport dimensions + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Calculate position relative to the parent menu + let left = cursorX + parentMenuWidth; // Offset by parent menu width + let top = cursorY; + + // Adjust position to avoid overflowing viewport + const adjustedLeft = + left + submenuWidth > viewportWidth ? cursorX - submenuWidth : left; + const adjustedTop = + top + submenuHeight > viewportHeight ? viewportHeight - submenuHeight - 10 : top; + + console.log({ left, top, adjustedLeft, adjustedTop }); + + // Apply calculated position + this.container.style.left = `${adjustedLeft}px`; + this.container.style.top = `${adjustedTop}px`; + this.container.style.display = 'flex'; + this.container.style.position = 'absolute' + // Ensure the exit button stays within bounds + this.exitButton.style.bottom = '5px'; + this.exitButton.style.right = '5px'; + + // Close menu when clicking outside + document.addEventListener('mousedown', this._handleOutsideClick.bind(this), { once: true }); + } + + + public closeMenu(): void { + this.container.style.display = 'none'; + document.removeEventListener('mousedown', this._handleOutsideClick); + } + private _handleOutsideClick(event: MouseEvent): void { + if (!this.container.contains(event.target as Node)) { + this.closeMenu(); + } + } + + + private static extractRGBA(color: string): number[] { + const dummyElem = document.createElement('div'); + dummyElem.style.color = color; + document.body.appendChild(dummyElem); + const computedColor = getComputedStyle(dummyElem).color; + document.body.removeChild(dummyElem); + + const rgb = computedColor.match(/\d+/g)?.map(Number) || [0, 0, 0]; + const opacity = computedColor.includes("rgba") + ? parseFloat(computedColor.split(",")[3]) + : 1; + return [rgb[0], rgb[1], rgb[2], opacity]; + } + public getElement(): HTMLDivElement { + return this.container; + } + // Dynamically updates the label and selection function + public update( initialValue: string, applySelection: (color: string) => void): void { + this.rgba = ColorPicker.extractRGBA(initialValue); + this.opacity = this.rgba[3]; + this.applySelection = applySelection; + this.updateTargetColor(); + } + + private updateTargetColor(): void { + this.color = `rgba(${this.rgba[0]}, ${this.rgba[1]}, ${this.rgba[2]}, ${this.opacity})`; + this.applySelection(this.color); // Apply color selection immediately + } + } + diff --git a/src/context-menu/context-menu.ts b/src/context-menu/context-menu.ts index 15c58cf..5c2b655 100644 --- a/src/context-menu/context-menu.ts +++ b/src/context-menu/context-menu.ts @@ -1,153 +1,2119 @@ -import { Drawing } from "../drawing/drawing"; +import { + Background, + ColorType, + IChartApi, + ISeriesApi, + LineStyle, + MouseEventParams, + SeriesType, + SolidColor, + VerticalGradientColor, + PriceScaleMode, + PriceScaleOptions, + CandlestickSeriesOptions, +} from "lightweight-charts"; import { DrawingTool } from "../drawing/drawing-tool"; -import { DrawingOptions } from "../drawing/options"; -import { GlobalParams } from "../general/global-params"; +import { ColorPicker as seriesColorPicker } from "./color-picker_"; import { ColorPicker } from "./color-picker"; +import { + AreaSeriesOptions, + BarSeriesOptions, + LineSeriesOptions, + ISeriesApiExtended, + SeriesOptionsExtended +} from "../helpers/general"; +import { GlobalParams } from "../general/global-params"; +//import { TooltipPrimitive } from "../tooltip/tooltip"; import { StylePicker } from "./style-picker"; +import { Drawing } from "../drawing/drawing"; +import { DrawingOptions } from "../drawing/options"; +import { FillArea, defaultFillAreaOptions} from "../fill-area/fill-area"; +import { ensureExtendedSeries, isOHLCData, isSingleValueData, isSolidColor, isVerticalGradientColor } from "../helpers/typeguards"; +import { Handler } from "../general/handler"; +export function buildOptions(optionPath: string, value: any): any { + const keys = optionPath.split("."); + const options: any = {}; + let current = options; -export function camelToTitle(inputString: string) { - const result = []; - for (const c of inputString) { - if (result.length == 0) { - result.push(c.toUpperCase()); - } else if (c == c.toUpperCase()) { - result.push(' '+c); - } else result.push(c); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (i === keys.length - 1) { + current[key] = value; + } else { + current[key] = {}; + current = current[key]; } - return result.join(''); + } + + return options; +} + +// series-types.ts +export enum SeriesTypeEnum { + Line = "Line", + Histogram = "Histogram", + Area = "Area", + Bar = "Bar", + Candlestick = "Candlestick", +} + +export type SupportedSeriesType = keyof typeof SeriesTypeEnum; +export let activeMenu: HTMLElement | null = null; + +/** + * Closes the currently active menu. + */ +export function closeActiveMenu() { + if (activeMenu) { + activeMenu.style.display = "none"; + activeMenu = null; + } +} + +/** + * Utility function to convert camelCase to Title Case + * @param inputString The camelCase string. + * @returns The Title Case string. + */ +export function camelToTitle(inputString: string): string { + return inputString + .replace(/([A-Z])/g, " $1") + .replace(/^./, (str) => str.toUpperCase()); } interface Item { - elem: HTMLSpanElement; - action: Function; - closeAction: Function | null; + elem: HTMLSpanElement; + action: Function; + closeAction: Function | null; } declare const window: GlobalParams; - export class ContextMenu { - private div: HTMLDivElement - private hoverItem: Item | null; - private items: HTMLElement[] = [] + private div: HTMLDivElement; + private hoverItem: Item | null; + private items: HTMLElement[] = []; + private colorPicker: seriesColorPicker = new seriesColorPicker( + "#ff0000", + () => null + ); + private saveDrawings: Function | null = null; + private drawingTool: DrawingTool | null = null; + ///private globalTooltipEnabled: boolean = false; + ///private Tooltip: TooltipPrimitive | null = null; + ///private currentTooltipSeries: ISeriesApiExtended | null = null; - constructor( - private saveDrawings: Function, - private drawingTool: DrawingTool, - ) { - this._onRightClick = this._onRightClick.bind(this); - this.div = document.createElement('div'); - this.div.classList.add('context-menu'); - document.body.appendChild(this.div); - this.hoverItem = null; - document.body.addEventListener('contextmenu', this._onRightClick); + private mouseEventParams: MouseEventParams | null = null; + + private constraints: Record< + string, + { skip?: boolean; min?: number; max?: number } + > = { + baseline: { skip: true }, + title: { skip: true }, + PriceLineSource: { skip: true }, + tickInterval: { min: 0, max: 100 }, + lastPriceAnimation: { skip: true }, + lineType: { min: 0, max: 2 }, + }; + public setupDrawingTools(saveDrawings: Function, drawingTool: DrawingTool) { + this.saveDrawings = saveDrawings; + this.drawingTool = drawingTool; + } + + private shouldSkipOption(optionName: string): boolean { + const constraints = this.constraints[optionName] || {}; + return !!constraints.skip; + } + public separator() { + const separator = document.createElement("div"); + separator.style.width = "90%"; + separator.style.height = "1px"; + separator.style.margin = "3px 0px"; + separator.style.backgroundColor = window.pane.borderColor; + this.div.appendChild(separator); + + this.items.push(separator); + } + + public menuItem( + text: string, + action: Function, + hover: Function | null = null + ) { + const item = document.createElement("span"); + item.classList.add("context-menu-item"); + this.div.appendChild(item); + + const elem = document.createElement("span"); + elem.innerText = text; + elem.style.pointerEvents = "none"; + item.appendChild(elem); + + if (hover) { + let arrow = document.createElement("span"); + arrow.innerText = `►`; + arrow.style.fontSize = "8px"; + arrow.style.pointerEvents = "none"; + item.appendChild(arrow); } - _handleClick = (ev: MouseEvent) => this._onClick(ev); + item.addEventListener("mouseover", () => { + if (this.hoverItem && this.hoverItem.closeAction) + this.hoverItem.closeAction(); + this.hoverItem = { elem: elem, action: action, closeAction: hover }; + }); + if (!hover) + item.addEventListener("click", (event) => { + action(event); + this.div.style.display = "none"; + }); + else { + let timeout: any; + item.addEventListener( + "mouseover", + () => + (timeout = setTimeout( + () => action(item.getBoundingClientRect()), + 100 + )) + ); + item.addEventListener("mouseout", () => clearTimeout(timeout)); + } + + this.items.push(item); + } + + constructor( + private handler: Handler, + private handlerMap: Map, + private getMouseEventParams: () => MouseEventParams | null + ) { + this.div = document.createElement("div"); + this.div.classList.add("context-menu"); + document.body.appendChild(this.div); + this.div.style.overflowY = "scroll"; + this.hoverItem = null; + this.mouseEventParams = getMouseEventParams(); + document.body.addEventListener( + "contextmenu", + this._onRightClick.bind(this) + ); + document.body.addEventListener("click", this._onClick.bind(this)); + //this.handler.chart.subscribeCrosshairMove((param: MouseEventParams) => { + // this.handleCrosshairMove(param); + //}); + + this.setupMenu(); + } + + private _onClick(ev: MouseEvent) { + const target = ev.target as Node; + const menus = [this.colorPicker]; + + menus.forEach((menu) => { + if (!menu.getElement().contains(target)) { + menu.closeMenu(); + } + }); + } + + // series-context-menu.ts + + private _onRightClick(event: MouseEvent): void { + event.preventDefault(); // Prevent the browser's context menu - private _onClick(ev: MouseEvent) { - if (!ev.target) return; - if (!this.div.contains(ev.target as Node)) { - this.div.style.display = 'none'; - document.body.removeEventListener('click', this._handleClick); + const mouseEventParams = this.getMouseEventParams(); + const seriesFromProximity = this.getProximitySeries( + this.getMouseEventParams()! + ); + const drawingFromProximity = this.getProximityDrawing(); // Implement this method based on your drawing logic + + console.log("Mouse Event Params:", mouseEventParams); + console.log("Proximity Series:", seriesFromProximity); + console.log("Proximity Drawing:", drawingFromProximity); + + this.clearMenu(); // Clear existing menu items + this.clearAllMenus(); // Clear other menus if necessary + + if (seriesFromProximity) { + // Right-click on a series + console.log("Right-click detected on a series (proximity)."); + this.populateSeriesMenu(seriesFromProximity, event); + } else if (drawingFromProximity) { + // Right-click on a drawing + console.log("Right-click detected on a drawing."); + this.populateDrawingMenu(drawingFromProximity, event); + } else if (mouseEventParams?.hoveredSeries) { + // Fallback to hovered series + console.log("Right-click detected on a series (hovered)."); + this.populateSeriesMenu(mouseEventParams.hoveredSeries, event); + } else { + // Right-click on chart background + console.log("Right-click detected on the chart background."); + this.populateChartMenu(event); + } + + // Position the menu at cursor location + this.showMenu(event); + event.preventDefault(); + event.stopPropagation(); // Prevent event bubbling + + } + + // series-context-menu.ts + + private getProximityDrawing(): Drawing | null { + // Implement your logic to determine if a drawing is under the cursor + // For example: + if (Drawing.hoveredObject) { + return Drawing.hoveredObject; + } + return null; + } + private getProximitySeries( + param: MouseEventParams + ): ISeriesApi | null { + if (!param || !param.seriesData) { + console.warn("No mouse event parameters or series data available."); + return null; + } + + if (!param.point) { + console.warn("No point data in MouseEventParams."); + return null; + } + + const cursorY = param.point.y; + let sourceSeries: ISeriesApi | null = null; + const referenceSeries = this.handler._seriesList[0] as ISeriesApiExtended; + + if (this.handler.series) { + sourceSeries = this.handler.series; + console.log(`Using handler.series for coordinate conversion.`); + } else if (referenceSeries) { + sourceSeries = referenceSeries; + console.log(`Using referenceSeries for coordinate conversion.`); + } else { + console.warn("No handler.series or referenceSeries available."); + return null; + } + + const cursorPrice = sourceSeries.coordinateToPrice(cursorY); + console.log(`Converted chart Y (${cursorY}) to Price: ${cursorPrice}`); + + if (cursorPrice === null) { + console.warn("Cursor price is null. Unable to determine proximity."); + return null; + } + + const seriesByDistance: { + distance: number; + series: ISeriesApi; + }[] = []; + + param.seriesData.forEach((data, series) => { + + let refPrice: number | undefined; + if (isSingleValueData(data)) { + refPrice = data.value; + } else if (isOHLCData(data)) { + refPrice = data.close; + } + + if (refPrice !== undefined && !isNaN(refPrice)) { + const distance = Math.abs(refPrice - cursorPrice); + const percentageDifference = (distance / cursorPrice) * 100; + + if (percentageDifference <= 3.33) { + seriesByDistance.push({ distance, series }); + } } + }); + + // Sort series by proximity (distance) + seriesByDistance.sort((a, b) => a.distance - b.distance); + + if (seriesByDistance.length > 0) { + console.log("Closest series found."); + return seriesByDistance[0].series; + } + + console.log("No series found within the proximity threshold."); + return null; +} + + private showMenu(event: MouseEvent): void { + const x = event.clientX; + const y = event.clientY; + + this.div.style.position = "absolute"; + this.div.style.zIndex = "1000"; + this.div.style.left = `${x}px`; + this.div.style.top = `${y}px`; + this.div.style.width = "225px"; + this.div.style.maxHeight = `400px`; + this.div.style.overflowY = "scroll"; + this.div.style.display = "block"; + + console.log("Displaying Menu at:", x, y); + + activeMenu = this.div; + console.log("Displaying Menu", x, y); + + document.addEventListener( + "mousedown", + this.hideMenuOnOutsideClick.bind(this), + { once: true } + ); + } + + private hideMenuOnOutsideClick(event: MouseEvent): void { + if (!this.div.contains(event.target as Node)) { + this.hideMenu(); } + } + + private hideMenu() { + this.div.style.display = "none"; + if (activeMenu === this.div) { + activeMenu = null; + } + } + + private resetView(): void { + this.handler.chart.timeScale().resetTimeScale(); + this.handler.chart.timeScale().fitContent(); + } - private _onRightClick(ev: MouseEvent) { - if (!Drawing.hoveredObject) return; + private clearAllMenus() { + this.handlerMap.forEach((handler) => { + if (handler.ContextMenu) { + handler.ContextMenu.clearMenu(); + } + }); + } + + public setupMenu() { + if (!this.div.querySelector(".chart-options-container")) { + const chartOptionsContainer = document.createElement("div"); + chartOptionsContainer.classList.add("chart-options-container"); + this.div.appendChild(chartOptionsContainer); + } + + if (!this.div.querySelector(".context-menu-item.close-menu")) { + this.addMenuItem("Close Menu", () => this.hideMenu()); + } + } + + private addNumberInput( + label: string, + defaultValue: number, + onChange: (value: number) => void, + min?: number, + max?: number + ): HTMLElement { + return this.addMenuInput( + this.div, + { + type: "number", + label, + value: defaultValue, + onChange, + min, + max, + }, + "" + ); + } + + private addCheckbox( + label: string, + defaultValue: boolean, + onChange: (value: boolean) => void + ): HTMLElement { + return this.addMenuInput(this.div, { + type: "boolean", + label, + value: defaultValue, + onChange, + }); + } + + private addMenuInput( + parent: HTMLElement, + config: { + type: "string" | "color" | "number" | "boolean" | "select"; + label: string; + value: any; + onChange: (newValue: any) => void; + action?: () => void; + min?: number; + max?: number; + options?: string[]; + }, + idPrefix: string = "" + ): HTMLElement { + let item: HTMLElement; + + if ( + config.type === "number" || + config.type === "string" || + config.type === "boolean" + ) { + item = document.createElement("div"); + item.classList.add("context-menu-item"); + item.style.display = "flex"; + item.style.alignItems = "center"; + item.style.justifyContent = "space-between"; - for (const item of this.items) { - this.div.removeChild(item); + if (config.label) { + const label = document.createElement("label"); + label.innerText = config.label; + label.htmlFor = `${idPrefix}${config.label.toLowerCase()}`; + label.style.marginRight = "8px"; + item.appendChild(label); + } + + let input: HTMLInputElement; + + if (config.type === "number") { + input = document.createElement("input"); + input.type = "number"; + input.value = config.value !== undefined ? config.value.toString() : ""; + input.style.width = "45px"; + input.style.marginLeft = "auto"; + input.style.cursor = "pointer"; + + if (config.min !== undefined) { + input.min = config.min.toString(); } - this.items = []; - - for (const optionName of Object.keys(Drawing.hoveredObject._options)) { - let subMenu; - if (optionName.toLowerCase().includes('color')) { - subMenu = new ColorPicker(this.saveDrawings, optionName as keyof DrawingOptions); - } else if (optionName === 'lineStyle') { - subMenu = new StylePicker(this.saveDrawings) - } else continue; - - let onClick = (rect: DOMRect) => subMenu.openMenu(rect) - this.menuItem(camelToTitle(optionName), onClick, () => { - document.removeEventListener('click', subMenu.closeMenu) - subMenu._div.style.display = 'none' - }) + if (config.max !== undefined) { + input.max = config.max.toString(); } - let onClickDelete = () => this.drawingTool.delete(Drawing.lastHoveredObject); - this.separator() - this.menuItem('Delete Drawing', onClickDelete) - - // const colorPicker = new ColorPicker(this.saveDrawings) - // const stylePicker = new StylePicker(this.saveDrawings) - - // let onClickDelete = () => this._drawingTool.delete(Drawing.lastHoveredObject); - // let onClickColor = (rect: DOMRect) => colorPicker.openMenu(rect) - // let onClickStyle = (rect: DOMRect) => stylePicker.openMenu(rect) - - // contextMenu.menuItem('Color Picker', onClickColor, () => { - // document.removeEventListener('click', colorPicker.closeMenu) - // colorPicker._div.style.display = 'none' - // }) - // contextMenu.menuItem('Style', onClickStyle, () => { - // document.removeEventListener('click', stylePicker.closeMenu) - // stylePicker._div.style.display = 'none' - // }) - // contextMenu.separator() - // contextMenu.menuItem('Delete Drawing', onClickDelete) - - - ev.preventDefault(); - this.div.style.left = ev.clientX + 'px'; - this.div.style.top = ev.clientY + 'px'; - this.div.style.display = 'block'; - document.body.addEventListener('click', this._handleClick); - } - - public menuItem(text: string, action: Function, hover: Function | null = null) { - const item = document.createElement('span'); - item.classList.add('context-menu-item'); - this.div.appendChild(item); - - const elem = document.createElement('span'); - elem.innerText = text; - elem.style.pointerEvents = 'none'; - item.appendChild(elem); - - if (hover) { - let arrow = document.createElement('span') - arrow.innerText = `►` - arrow.style.fontSize = '8px' - arrow.style.pointerEvents = 'none' - item.appendChild(arrow) + input.addEventListener("input", (event) => { + const target = event.target as HTMLInputElement; + let newValue: number = parseFloat(target.value); + const optionName = config.label; + const constraints = this.constraints[optionName.toLowerCase()]; + + if (constraints && !constraints.skip) { + if (constraints.min !== undefined && newValue < constraints.min) { + newValue = constraints.min; + input.value = newValue.toString(); + } + if (constraints.max !== undefined && newValue > constraints.max) { + newValue = constraints.max; + input.value = newValue.toString(); + } + } + + if (!isNaN(newValue)) { + config.onChange(newValue); + } + }); + + item.appendChild(input); + } else if (config.type === "boolean") { + input = document.createElement("input"); + input.type = "checkbox"; + input.checked = config.value ?? false; + input.style.marginLeft = "auto"; + input.style.cursor = "pointer"; + + input.addEventListener("change", (event) => { + const target = event.target as HTMLInputElement; + config.onChange(target.checked); + }); + + item.appendChild(input); + } else { + input = document.createElement("input"); + input.type = "text"; + input.value = config.value ?? ""; + input.style.marginLeft = "auto"; + input.style.cursor = "pointer"; + + input.addEventListener("input", (event) => { + const target = event.target as HTMLInputElement; + config.onChange(target.value); + }); + + item.appendChild(input); + } + } else if (config.type === "select") { + item = document.createElement("div"); + item.classList.add("context-menu-item"); + item.style.display = "flex"; + item.style.alignItems = "center"; + item.style.justifyContent = "space-between"; + + if (config.label) { + const label = document.createElement("label"); + label.innerText = config.label; + label.htmlFor = `${idPrefix}${config.label.toLowerCase()}`; + label.style.marginRight = "8px"; + item.appendChild(label); + } + + const select = document.createElement("select"); + select.id = `${idPrefix}${ + config.label ? config.label.toLowerCase() : "select" + }`; + select.style.marginLeft = "auto"; + select.style.cursor = "pointer"; + + config.options?.forEach((optionValue) => { + const option = document.createElement("option"); + option.value = optionValue; + option.text = optionValue; + if (optionValue === config.value) { + option.selected = true; + } + select.appendChild(option); + }); + + select.addEventListener("change", (event) => { + const target = event.target as HTMLSelectElement; + if (config.onChange) { + config.onChange(target.value); } + }); + + item.appendChild(select); + } else { + item = document.createElement("span"); + item.classList.add("context-menu-item"); + item.innerText = config.label || "Action"; + item.style.cursor = "pointer"; + + item.addEventListener("click", (event) => { + event.stopPropagation(); + config.action && config.action(); + }); + } + + parent.appendChild(item); + return item; + } + + private addMenuItem( + text: string, + action: () => void, + shouldHide: boolean = true, + hasSubmenu: boolean = false, + submenuLevel: number = 1 + ): HTMLElement { + const item = document.createElement("span"); + item.classList.add("context-menu-item"); + item.innerText = text; + + if (hasSubmenu) { + const defaultArrow = document.createElement("span"); + defaultArrow.classList.add("submenu-arrow"); + defaultArrow.innerText = "ː".repeat(submenuLevel); + item.appendChild(defaultArrow); + } + + item.addEventListener("click", (event) => { + event.stopPropagation(); + action(); + if (shouldHide) { + this.hideMenu(); + } + }); + + const arrows: string[] = ["➩", "➯", "➱", "➬", "➫"]; + + item.addEventListener("mouseenter", () => { + item.style.backgroundColor = "royalblue"; + item.style.color = "white"; + + if (!item.querySelector(".hover-arrow")) { + const hoverArrow = document.createElement("span"); + hoverArrow.classList.add("hover-arrow"); + const randomIndex = Math.floor(Math.random() * arrows.length); + const selectedArrow = arrows[randomIndex]; + hoverArrow.innerText = selectedArrow; + hoverArrow.style.marginLeft = "auto"; + hoverArrow.style.fontSize = "14px"; + hoverArrow.style.color = "white"; + item.appendChild(hoverArrow); + } + }); + + item.addEventListener("mouseleave", () => { + item.style.backgroundColor = ""; + item.style.color = ""; + const hoverArrow = item.querySelector(".hover-arrow"); + if (hoverArrow) { + item.removeChild(hoverArrow); + } + }); + + this.div.appendChild(item); + this.items.push(item); + + return item; + } + + public clearMenu() { + const dynamicItems = this.div.querySelectorAll( + ".context-menu-item:not(.close-menu), .context-submenu" + ); + dynamicItems.forEach((item) => item.remove()); + this.items = []; + } + + + + /** + * Unified color picker menu item. + * @param label Display label for the menu item + * @param currentColor The current color value + * @param optionPath The dot-separated path to the option + * @param optionTarget The chart or series to apply the color to + */ + private addColorPickerMenuItem( + label: string, + currentColor: string|null, + optionPath: string, + optionTarget: IChartApi | ISeriesApiExtended | any + ): HTMLElement { + const menuItem = document.createElement("span"); + menuItem.classList.add("context-menu-item"); + menuItem.innerText = label; + + this.div.appendChild(menuItem); + + const applyColor = (newColor: string) => { + const options = buildOptions(optionPath, newColor); + optionTarget.applyOptions(options); + console.log(`Updated ${optionPath} to ${newColor}`); + }; + + menuItem.addEventListener("click", (event: MouseEvent) => { + event.stopPropagation(); + if (!this.colorPicker) { + this.colorPicker = new seriesColorPicker(currentColor??'#000000', applyColor); + } + this.colorPicker.openMenu(event, 225, applyColor); + }); + + return menuItem; + } + + // Class-level arrays to store current options for width and style. + private currentWidthOptions: { + name: keyof (LineSeriesOptions & + BarSeriesOptions & + AreaSeriesOptions + ); + label: string; + min?: number; + max?: number; + value: number; + }[] = []; - item.addEventListener('mouseover', () => { - if (this.hoverItem && this.hoverItem.closeAction) this.hoverItem.closeAction() - this.hoverItem = {elem: elem, action: action, closeAction: hover} - }) - if (!hover) item.addEventListener('click', (event) => {action(event); this.div.style.display = 'none'}) - else { - let timeout: number; - item.addEventListener('mouseover', () => timeout = setTimeout(() => action(item.getBoundingClientRect()), 100)) - item.addEventListener('mouseout', () => clearTimeout(timeout)) + private currentStyleOptions: { + name: keyof (LineSeriesOptions & + BarSeriesOptions & + AreaSeriesOptions + ); + label: string; + value: string | number; + options?: string[]; + }[] = []; + + + /** + * Populates the clone series submenu. + * + * @param series - The original series to clone. + * @param event - The mouse event triggering the context menu. + */ + + + private populateSeriesMenu( + series: ISeriesApi | ISeriesApiExtended, + event: MouseEvent + ): void { + // Type guard to check if series is extended + const _series = ensureExtendedSeries(series,this.handler.legend) + + // Now `series` is guaranteed to be extended + const seriesOptions = series.options() as Partial< + LineSeriesOptions & + BarSeriesOptions & + AreaSeriesOptions & + CandlestickSeriesOptions & + SeriesOptionsExtended + >; + + if (!seriesOptions) { + console.warn("No options found for the selected series."); + return; + } + + this.div.innerHTML = ""; + + const colorOptions: { label: string; value: string }[] = []; + const visibilityOptions: { label: string; value: boolean }[] = []; + const otherOptions: { label: string; value: any }[] = []; + + // Temporary arrays before assigning to class-level variables + const tempWidthOptions: { + name: keyof (LineSeriesOptions & + BarSeriesOptions & + AreaSeriesOptions ); + label: string; + value: number; + min?: number; + max?: number; + }[] = []; + + const tempStyleOptions: { + name: keyof (LineSeriesOptions & + BarSeriesOptions & + AreaSeriesOptions + ); + label: string; + value: string | number; + options?: string[]; + }[] = []; + + for (const optionName of Object.keys(seriesOptions) as Array< + keyof (LineSeriesOptions & + BarSeriesOptions & + AreaSeriesOptions + ) + >) { + const optionValue = seriesOptions[optionName]; + if (this.shouldSkipOption(optionName)) continue; + if (optionName.toLowerCase().includes("base")) continue; + + const lowerOptionName = camelToTitle(optionName).toLowerCase(); + + if (lowerOptionName.includes("color")) { + // Color options + if (typeof optionValue === "string") { + colorOptions.push({ label: optionName, value: optionValue }); + } else { + console.warn( + `Expected string value for color option "${optionName}".` + ); + } + } else if (lowerOptionName.includes("width")) { + // Width options + // This includes things like lineWidth, priceLineWidth, crosshairMarkerBorderWidth, etc. + if (typeof optionValue === "number") { + tempWidthOptions.push({ + name: optionName, + label: optionName, + value: optionValue, + }); + } else { + console.warn( + `Expected number value for width option "${optionName}".` + ); + } + } else if ( + lowerOptionName.includes("visible") || + lowerOptionName.includes("visibility") + ) { + // Visibility options + if (typeof optionValue === "boolean") { + visibilityOptions.push({ label: optionName, value: optionValue }); + } else { + console.warn( + `Expected boolean value for visibility option "${optionName}".` + ); + } + } else if (optionName === "lineType") { + // lineType is a style option + // LineType: Simple=0, WithSteps=1 + const possibleLineTypes = this.getPredefinedOptions(camelToTitle(optionName))!; + tempStyleOptions.push({ + name: optionName, + label: optionName, + value: optionValue as string, + options: possibleLineTypes, + }); + } else if (optionName === "crosshairMarkerRadius") { + // crosshairMarkerRadius should appear under Width Options + if (typeof optionValue === "number") { + tempWidthOptions.push({ + name: optionName, + label: optionName, + value: optionValue, + min: 1, + max: 50, + }); + } else { + console.warn( + `Expected number value for crosshairMarkerRadius option "${optionName}".` + ); } + } else if (lowerOptionName.includes("style")) { + // Style options (e.g. lineStyle) + if ( + typeof optionValue === "string" || + Object.values(LineStyle).includes(optionValue as LineStyle) || + typeof optionValue === "number" + ) { + const possibleStyles = [ + "Solid", + "Dotted", + "Dashed", + "Large Dashed", + "Sparse Dotted", + ]; + tempStyleOptions.push({ + name: optionName, + label: optionName, + value: optionValue as string, + options: possibleStyles, + }); + } else { + console.warn( + `Expected string/number value for style-related option "${optionName}".` + ); + } + } else { + // Other options go directly to otherOptions + otherOptions.push({ label: optionName, value: optionValue }); + } + } + + // Assign the temp arrays to class-level arrays for use in submenus + this.currentWidthOptions = tempWidthOptions; + this.currentStyleOptions = tempStyleOptions; + + + + // Add main menu items only if these arrays have content + if (visibilityOptions.length > 0) { + this.addMenuItem( + "Visibility Options ▸", + () => { + this.populateVisibilityMenu(event, series); + }, + false, + true + ); + } + + if (this.currentStyleOptions.length > 0) { + this.addMenuItem( + "Style Options ▸", + () => { + this.populateStyleMenu(event, series); + }, + false, + true + ); + } + + if (this.currentWidthOptions.length > 0) { + this.addMenuItem( + "Width Options ▸", + () => { + this.populateWidthMenu(event, series); + }, + false, + true + ); + } + + if (colorOptions.length > 0) { + this.addMenuItem( + "Color Options ▸", + () => { + this.populateColorOptionsMenu(colorOptions, series, event); + }, + false, + true + ); + } + +// Add other options dynamically +otherOptions.forEach((option) => { + const optionLabel = camelToTitle(option.label); // Human-readable label + + // Skip if explicitly marked as skippable + if (this.constraints[option.label]?.skip) { + return; + } + + if (typeof option.value === "boolean") { + // Add a menu item with a checkbox for boolean options + this.addMenuItem( + `${optionLabel} ▸`, + () => { + this.div.innerHTML = ""; // Clear existing menu items + + const newValue = !option.value; // Toggle the value + const options = buildOptions(option.label, newValue); + series.applyOptions(options); + console.log(`Toggled ${option.label} to ${newValue}`); + + // Repopulate the menu dynamically + }, + option.value // The checkbox state matches the current value + ); + } else if (typeof option.value === "string") { + // Add a submenu or text input for string options + const predefinedOptions = this.getPredefinedOptions(option.label); + + if (predefinedOptions && predefinedOptions.length > 0) { + this.addMenuItem( + `${optionLabel} ▸`, + () => { + this.div.innerHTML = ""; // Clear existing menu items + + this.addSelectInput( + optionLabel, + option.value, + predefinedOptions, + (newValue: string) => { + const options = buildOptions(option.label, newValue); + series.applyOptions(options); + console.log(`Updated ${option.label} to ${newValue}`); + + // Repopulate the menu dynamically + } + ); + }, + false, + true // Mark as a submenu + ); + } else { + this.addMenuItem( + `${optionLabel} ▸`, + () => { + this.div.innerHTML = ""; // Clear existing menu items + + this.addTextInput( + optionLabel, + option.value, + (newValue: string) => { + const options = buildOptions(option.label, newValue); + series.applyOptions(options); + console.log(`Updated ${option.label} to ${newValue}`); + + // Repopulate the menu dynamically + } + ); + }, + false, + true // Mark as a submenu + ); + } + } else if (typeof option.value === "number") { + // Add a submenu or number input for numeric options + const min = this.constraints[option.label]?.min; + const max = this.constraints[option.label]?.max; - this.items.push(item); + this.addMenuItem( + `${optionLabel} ▸`, + () => { + this.div.innerHTML = ""; // Clear existing menu items + this.addNumberInput( + optionLabel, + option.value, + (newValue: number) => { + const options = buildOptions(option.label, newValue); + series.applyOptions(options); + console.log(`Updated ${option.label} to ${newValue}`); + + // Repopulate the menu dynamically + }, + min, + max + ); + }, + false, + true // Mark as a submenu + ); + } else { + return; // Skip unsupported data types + } +}); + + + // Add "Fill Area Between" menu option + this.addMenuItem( + "Fill Area Between", + () => { + this.startFillAreaBetween(event, _series); // Define the method below + }, + false, + false + ); + + + // Access the primitives + const primitives = _series.primitives; + + // Debugging output + console.log("Primitives:", primitives); + + // Add "Customize Fill Area" option if `FillArea` is present + const hasFillArea = primitives?.FillArea ?? primitives?.pt; + + if (primitives["FillArea"]) { + this.addMenuItem( + "Customize Fill Area", + () => { + this.customizeFillAreaOptions(event, hasFillArea); + }, + false, + true + ); + } + + // Add remaining existing menu items + this.addMenuItem( + "⤝ Main Menu", + () => { + this.populateChartMenu(event); + }, + false, + false + ); + + this.showMenu(event); } - public separator() { - const separator = document.createElement('div') - separator.style.width = '90%' - separator.style.height = '1px' - separator.style.margin = '3px 0px' - separator.style.backgroundColor = window.pane.borderColor - this.div.appendChild(separator) - this.items.push(separator); + private populateDrawingMenu(drawing: Drawing, event: MouseEvent): void { + this.div.innerHTML = ""; // Clear existing menu items + + // Add drawing-specific menu items + for (const optionName of Object.keys(drawing._options)) { + let subMenu; + if (optionName.toLowerCase().includes("color")) { + subMenu = new ColorPicker( + this.saveDrawings!, + optionName as keyof DrawingOptions + ); + } else if (optionName === "lineStyle") { + subMenu = new StylePicker(this.saveDrawings!); + } else { + continue; + } + + const onClick = (rect: DOMRect) => subMenu.openMenu(rect); + this.menuItem(camelToTitle(optionName), onClick, () => { + document.removeEventListener("click", subMenu.closeMenu); + subMenu._div.style.display = "none"; + }); } + const onClickDelete = () => this.drawingTool!.delete(drawing); + this.separator(); + this.menuItem("Delete Drawing", onClickDelete); + + // Optionally, add a back button or main menu option + this.addMenuItem( + "⤝ Main Menu", + () => { + this.populateChartMenu(event); + }, + false, + false + ); + + this.showMenu(event); + } + private populateChartMenu(event: MouseEvent): void { + this.div.innerHTML = ""; + console.log(`Displaying Menu Options: Chart`); + this.addResetViewOption(); + //const tooltipLabel = this.globalTooltipEnabled + // ? "Disable Global Tooltip" + // : "Enable Global Tooltip"; + //this.addMenuItem(tooltipLabel, () => { + // this.globalTooltipEnabled = !this.globalTooltipEnabled; +// + // if (!this.globalTooltipEnabled) { + // // Detach tooltip from current series + // this.Tooltip?.detached(); + // } else { + // // Reattach tooltip to the closest series if applicable + // const series = this.getProximitySeries(this.mouseEventParams!); + // if (series) { + // let _series = ensureExtendedSeries(series, this.handler.legend) + // this.switchTooltipToSeries(_series); + // } + // } + //}); + + // Layout menu + this.addMenuItem( + "⌯ Layout Options ", + () => this.populateLayoutMenu(event), + false, + true + ); + this.addMenuItem( + "⌗ Grid Options ", + () => this.populateGridMenu(event), + false, + true + ); + this.addMenuItem( + "⊹ Crosshair Options ", + () => this.populateCrosshairOptionsMenu(event), + false, + true + ); + this.addMenuItem( + "ⴵ Time Scale Options ", + () => this.populateTimeScaleMenu(event), + false, + true + ); + this.addMenuItem( + "$ Price Scale Options ", + () => this.populatePriceScaleMenu(event, "right"), + false, + true + ); + + this.showMenu(event); + } + + private populateLayoutMenu(event: MouseEvent): void { + this.div.innerHTML = ""; + const layoutOptions = { name: "Text Color", valuePath: "layout.textColor" }; + const initialColor = + (this.getCurrentOptionValue(layoutOptions.valuePath) as string) || + "#000000"; + + // Layout text color + this.addColorPickerMenuItem( + camelToTitle(layoutOptions.name), + initialColor, + layoutOptions.valuePath, + this.handler.chart + ); + + // If you intended to show a background menu with "Type & Colors" and "Options": + // Call populateBackgroundMenu, not populateBackgroundOptionsMenu directly. + this.addMenuItem( + "Background Options", + () => this.populateBackgroundMenu(event), + false, + true + ); + + this.addMenuItem( + "⤝ Main Menu", + () => { + this.populateChartMenu(event); + }, + false, + false + ); + + this.showMenu(event); + } + private populateWidthMenu(event: MouseEvent, series: ISeriesApi): void { + this.div.innerHTML = ""; // Clear current menu + + // Use the stored currentWidthOptions array + this.currentWidthOptions.forEach((option) => { + if (typeof option.value === "number") { + this.addNumberInput( + camelToTitle(option.label), + option.value, + (newValue: number) => { + const options = buildOptions(option.name, newValue); + series.applyOptions(options); + console.log(`Updated ${option.label} to ${newValue}`); + }, + option.min, + option.max + ); + } + }); + + this.addMenuItem( + "⤝ Back to Series Options", + () => { + this.populateSeriesMenu(series, event); + }, + false, + false + ); + + this.showMenu(event); + } + private populateStyleMenu(event: MouseEvent, series: ISeriesApi): void { + this.div.innerHTML = ""; // Clear current menu + + this.currentStyleOptions.forEach((option) => { + const predefinedOptions = this.getPredefinedOptions(option.name); + + if (predefinedOptions) { + // Use a dropdown for options with predefined values + this.addSelectInput( + camelToTitle(option.name), // Display a human-readable label + option.value.toString(), // Current value of the option + predefinedOptions, // Predefined options for the dropdown + (newValue: string) => { + const newVal = predefinedOptions.indexOf(newValue); // Map new value to its index + const options = buildOptions(option.name, newVal); // Build the updated options + series.applyOptions(options); // Apply the new options to the series + console.log(`Updated ${option.name} to ${newValue}`); + } + ); + } + }); + + + + this.addMenuItem( + "⤝ Main Menu", + () => { + this.populateChartMenu(event); + }, + false + ); + + this.showMenu(event); + } +private populateLineTypeMenu( + event: MouseEvent, + series: ISeriesApi, + option: { + name: keyof (LineSeriesOptions & + BarSeriesOptions & + AreaSeriesOptions); + label: string; + value: string | number; + options?: string[]; + } +) { + this.div.innerHTML = ""; // Clear current menu + + if (!option.options) return; + + // Use the addSelectInput method to add a dropdown + this.addSelectInput( + option.label, // Label for the dropdown + option.value.toString(), // Current value as string + option.options, // List of options + (newValue: string) => { + const newVal = newValue === "Simple" ? 0 : 1; // Map option to value (you can adjust this logic) + const options = buildOptions(option.name, newVal); + series.applyOptions(options); + console.log(`Updated ${option.label} to ${newValue}`); + } + ); + + // Add a "Back" button to navigate to the Style Options menu + this.addMenuItem( + "⤝ Back to Style Options", + () => { + this.populateStyleMenu(event, series); + }, + false + ); + + this.showMenu(event); // Display the menu +} + + + + private addTextInput( + label: string, + defaultValue: string, + onChange: (value: string) => void + ): HTMLElement { + const container = document.createElement("div"); + container.classList.add("context-menu-item"); + container.style.display = "flex"; + container.style.alignItems = "center"; + container.style.justifyContent = "space-between"; + + const labelElem = document.createElement("label"); + labelElem.innerText = label; + labelElem.htmlFor = `${label.toLowerCase()}-input`; + labelElem.style.marginRight = "8px"; + container.appendChild(labelElem); + + const input = document.createElement("input"); + input.type = "text"; + input.value = defaultValue; + input.id = `${label.toLowerCase()}-input`; + input.style.flex = "1"; + input.style.marginLeft = "auto"; + input.style.cursor = "pointer"; + + input.addEventListener("input", (event) => { + const target = event.target as HTMLInputElement; + onChange(target.value); + }); + + container.appendChild(input); + + this.div.appendChild(container); + + return container; + } + + private addSelectInput( + label: string, + currentValue: string, + options: string[], + onSelectChange: (newValue: string) => void + ): void { + const selectContainer = document.createElement("div"); + selectContainer.className = "menu-item select-input"; + + const selectLabel = document.createElement("span"); + selectLabel.innerText = label; + selectContainer.appendChild(selectLabel); + + const selectField = document.createElement("select"); + options.forEach((option) => { + const optionElement = document.createElement("option"); + optionElement.value = option; + optionElement.text = option; + if (option === currentValue) { + optionElement.selected = true; + } + selectField.appendChild(optionElement); + }); + selectField.addEventListener("change", (e) => { + const newValue = (e.target as HTMLSelectElement).value; + onSelectChange(newValue); + }); + selectContainer.appendChild(selectField); + + this.div.appendChild(selectContainer); + } + + private populateColorOptionsMenu( + colorOptions: { label: string; value: string }[], + series: ISeriesApi, + event: MouseEvent + ): void { + this.div.innerHTML = ""; + + colorOptions.forEach((option) => { + this.addColorPickerMenuItem( + camelToTitle(option.label), + option.value, + option.label, + series + ); + }); + + this.addMenuItem( + "⤝ Back to Series Options", + () => { + this.populateSeriesMenu(series, event); + }, + false, + false + ); + + this.showMenu(event); + } + + private populateVisibilityMenu( + event: MouseEvent, + series: ISeriesApi + ): void { + this.div.innerHTML = ""; + + const seriesOptions = series.options() as Partial< + LineSeriesOptions & + BarSeriesOptions & + AreaSeriesOptions + + >; + + const visibilityOptionNames: Array< + keyof (LineSeriesOptions & + BarSeriesOptions & + AreaSeriesOptions + ) + > = ["visible", "crosshairMarkerVisible", "priceLineVisible"]; + + visibilityOptionNames.forEach((optionName) => { + const optionValue = seriesOptions[optionName]; + if (typeof optionValue === "boolean") { + this.addCheckbox( + camelToTitle(optionName), + optionValue, + (newValue: boolean) => { + const options = buildOptions(optionName, newValue); + series.applyOptions(options); + console.log(`Toggled ${optionName} to ${newValue}`); + } + ); + } + }); + + this.addMenuItem( + "⤝ Back to Series Options", + () => { + this.populateSeriesMenu(series, event); + }, + false, + false + ); + + this.showMenu(event); + } + + private populateBackgroundTypeMenu(event: MouseEvent): void { + this.div.innerHTML = ""; + + const backgroundOptions = [ + { + text: "Solid", + action: () => this.setBackgroundType(event, ColorType.Solid), + }, + { + text: "Vertical Gradient", + action: () => this.setBackgroundType(event, ColorType.VerticalGradient), + }, + ]; + + backgroundOptions.forEach((option) => { + // Use shouldHide = false if you want to move to another menu without closing + this.addMenuItem( + option.text, + option.action, + false, // don't hide immediately if you want subsequent menus + false, + 1 + ); + }); + + // Back to Chart Menu + this.addMenuItem( + "⤝ Chart Menu", + () => { + this.populateChartMenu(event); + }, + false + ); + + this.showMenu(event); + } + + private populateGradientBackgroundMenuInline( + event: MouseEvent, + gradientBackground: VerticalGradientColor + ): void { + this.div.innerHTML = ""; + + this.addColorPickerMenuItem( + camelToTitle("Top Color"), + gradientBackground.topColor, + "layout.background.topColor", + this.handler.chart + ); + + this.addColorPickerMenuItem( + camelToTitle("Bottom Color"), + gradientBackground.bottomColor, + "layout.background.bottomColor", + this.handler.chart + ); + + // Back to Background Type Menu + this.addMenuItem( + "⤝ Background Type & Colors", + () => { + this.populateBackgroundTypeMenu(event); + }, + false + ); + + this.showMenu(event); + } + + + private populateGridMenu(event: MouseEvent): void { + this.div.innerHTML = ""; + + const gridOptions = [ + { name: "Vertical Grid Color", valuePath: "grid.vertLines.color" }, + { name: "Horizontal Grid Color", valuePath: "grid.horzLines.color" }, + ]; + + gridOptions.forEach((option) => { + const initialColor = + (this.getCurrentOptionValue(option.valuePath) as string) || "#FFFFFF"; + this.addColorPickerMenuItem( + camelToTitle(option.name), + initialColor, + option.valuePath, + this.handler.chart + ); + }); + + this.addMenuItem( + "⤝ Main Menu", + () => { + this.populateChartMenu(event); + }, + false + ); + + this.showMenu(event); + } + + private populateBackgroundMenu(event: MouseEvent): void { + this.div.innerHTML = ""; + + this.addMenuItem( + "Type & Colors", + () => { + this.populateBackgroundTypeMenu(event); + }, + false, + true + ); + + this.addMenuItem( + "Options", + () => { + this.populateBackgroundOptionsMenu(event); + }, + false, + true + ); + + this.addMenuItem( + "⤝ Layout Options", + () => { + this.populateLayoutMenu(event); + }, + false + ); + + this.showMenu(event); + } + + private populateBackgroundOptionsMenu(event: MouseEvent): void { + this.div.innerHTML = ""; + + const backgroundOptions = [ + { name: "Background Color", valuePath: "layout.background.color" }, + { name: "Background Top Color", valuePath: "layout.background.topColor" }, + { + name: "Background Bottom Color", + valuePath: "layout.background.bottomColor", + }, + ]; + + backgroundOptions.forEach((option) => { + const initialColor = + (this.getCurrentOptionValue(option.valuePath) as string) || "#FFFFFF"; + this.addColorPickerMenuItem( + camelToTitle(option.name), + initialColor, + option.valuePath, + this.handler.chart + ); + }); + + // Back to Background Menu + this.addMenuItem( + "⤝ Background", + () => { + this.populateBackgroundMenu(event); + }, + false + ); + + this.showMenu(event); + } + + private populateSolidBackgroundMenuInline( + event: MouseEvent, + solidBackground: SolidColor + ): void { + this.div.innerHTML = ""; + + this.addColorPickerMenuItem( + camelToTitle("Background Color"), + solidBackground.color, + "layout.background.color", + this.handler.chart + ); + + // Back to Type & Colors + this.addMenuItem( + "⤝ Type & Colors", + () => { + this.populateBackgroundTypeMenu(event); + }, + false + ); + + this.showMenu(event); + } + + private populateCrosshairOptionsMenu(event: MouseEvent): void { + this.div.innerHTML = ""; + + const crosshairOptions = [ + { name: "Line Color", valuePath: "crosshair.lineColor" }, + { name: "Vertical Line Color", valuePath: "crosshair.vertLine.color" }, + { name: "Horizontal Line Color", valuePath: "crosshair.horzLine.color" }, + ]; + + crosshairOptions.forEach((option) => { + const initialColor = + (this.getCurrentOptionValue(option.valuePath) as string) || "#000000"; + this.addColorPickerMenuItem( + camelToTitle(option.name), + initialColor, + option.valuePath, + this.handler.chart + ); + }); + + this.addMenuItem( + "⤝ Main Menu", + () => { + this.populateChartMenu(event); + }, + false + ); + + this.showMenu(event); + } + + private populateTimeScaleMenu(event: MouseEvent): void { + this.div.innerHTML = ""; + + const timeScaleOptions = [ + { + name: "Right Offset", + type: "number", + valuePath: "timeScale.rightOffset", + min: 0, + max: 100, + }, + { + name: "Bar Spacing", + type: "number", + valuePath: "timeScale.barSpacing", + min: 1, + max: 100, + }, + { + name: "Fix Left Edge", + type: "boolean", + valuePath: "timeScale.fixLeftEdge", + }, + { + name: "Border Color", + type: "color", + valuePath: "timeScale.borderColor", + }, + ]; + + timeScaleOptions.forEach((option) => { + if (option.type === "number") { + const currentValue = this.getCurrentOptionValue( + option.valuePath! + ) as number; + this.addNumberInput( + camelToTitle(option.name), + currentValue, + (newValue) => { + const updatedOptions = buildOptions(option.valuePath!, newValue); + this.handler.chart.applyOptions(updatedOptions); + console.log(`Updated TimeScale ${option.name} to: ${newValue}`); + }, + option.min, + option.max + ); + } else if (option.type === "boolean") { + const currentValue = this.getCurrentOptionValue( + option.valuePath! + ) as boolean; + this.addCheckbox( + camelToTitle(option.name), + currentValue, + (newValue) => { + const updatedOptions = buildOptions(option.valuePath!, newValue); + this.handler.chart.applyOptions(updatedOptions); + console.log(`Updated TimeScale ${option.name} to: ${newValue}`); + } + ); + } else if (option.type === "color") { + const currentColor = + (this.getCurrentOptionValue(option.valuePath!) as string) || + "#000000"; + this.addColorPickerMenuItem( + camelToTitle(option.name), + currentColor, + option.valuePath!, + this.handler.chart + ); + } + }); + + this.showMenu(event); + + this.addMenuItem( + "⤝ Main Menu", + () => { + this.populateChartMenu(event); + }, + false + ); + } + + private populatePriceScaleMenu( + event: MouseEvent, + priceScaleId: "left" | "right" = "right" + ): void { + this.div.innerHTML = ""; + + + + this.addMenuItem( + "Set Price Scale Mode", + () => { + this.populatePriceScaleModeMenu(event, priceScaleId); + }, + true, + true, + 1 + ); + + this.addMenuItem( + "⤝ Main Menu", + () => { + this.populateChartMenu(event); + }, + false + ); + + this.showMenu(event); + } + + + + private populatePriceScaleModeMenu( + event: MouseEvent, + priceScaleId: "left" | "right" + ): void { + this.div.innerHTML = ""; + + const currentMode: PriceScaleMode = + this.handler.chart.priceScale(priceScaleId).options().mode ?? + PriceScaleMode.Normal; + + const modeOptions: { name: string; value: PriceScaleMode }[] = [ + { name: "Normal", value: PriceScaleMode.Normal }, + { name: "Logarithmic", value: PriceScaleMode.Logarithmic }, + { name: "Percentage", value: PriceScaleMode.Percentage }, + { name: "Indexed To 100", value: PriceScaleMode.IndexedTo100 }, + ]; + + modeOptions.forEach((option) => { + const isActive = currentMode === option.value; + + this.addMenuItem( + option.name, + () => { + this.applyPriceScaleOptions(priceScaleId, { mode: option.value }); + this.hideMenu(); + console.log( + `Price scale (${priceScaleId}) mode set to: ${option.name}` + ); + }, + isActive, + false // Not a submenu + ); + }); + + this.addMenuItem( + "⤝ Back", + () => { + this.populatePriceScaleMenu(event, priceScaleId); + }, + false, // Not active + false, // Not a submenu + 1 // Add separator space + ); + + this.showMenu(event); + } + + private applyPriceScaleOptions( + priceScaleId: "left" | "right", + options: Partial +): void { + // Access the price scale from the chart using its ID + const priceScale = this.handler.chart.priceScale(priceScaleId); + + if (!priceScale) { + console.warn(`Price scale with ID "${priceScaleId}" not found.`); + return; + } + + // Apply the provided options to the price scale + priceScale.applyOptions(options); + + console.log(`Applied options to price scale "${priceScaleId}":`, options); +} + + private getCurrentOptionValue(optionPath: string): any { + const keys = optionPath.split("."); + let options: any = this.handler.chart.options(); + + for (const key of keys) { + if (options && key in options) { + options = options[key]; + } else { + console.warn(`Option path "${optionPath}" is invalid.`); + return null; + } + } + + return options; + } + + //// Class properties assumed to exist + //private handleCrosshairMove(param: MouseEventParams): void { + // if (!this.globalTooltipEnabled) { + // return; + // } +// + // const closestSeries = this.getProximitySeries(param); +// + // // Only switch if the closest series has changed + // if (closestSeries && closestSeries !== this.currentTooltipSeries) { + // let _series = ensureExtendedSeries(closestSeries, this.handler.legend) +// + // this.switchTooltipToSeries(_series); + // } + //} +// + // +// + //private switchTooltipToSeries(series: ISeriesApiExtended | null): void { + // if (series === this.currentTooltipSeries) { + // return; // Already attached to the same series + // } +// + // + // if (series) { + // this.attachTooltipToSeries(series); + // } else { + // this.currentTooltipSeries = null; + // } + //} + + + private mapStyleChoice(choice: string): number { + switch (choice) { + case "Solid": + return 0; + case "Dotted": + return 1; + case "Dashed": + return 2; + case "Large Dashed": + return 3; + case "Sparse Dotted": + return 4; + default: + return 0; + } + } +// +// private attachTooltipToSeries(series: ISeriesApiExtended): void { +// if (!this.Tooltip) { +// this.Tooltip = new TooltipPrimitive({ lineColor: "rgba(255, 0, 0, 1)" }); +// } +// +// this.Tooltip.switch(series); // Call the `switch(series)` method +// this.currentTooltipSeries = series; +// +// console.log( +// `Tooltip switched to series: ${series.options().title || "Untitled"}` +// ); +// } +// + + private setBackgroundType(event: MouseEvent, type: ColorType): void { + const currentBackground = this.handler.chart.options().layout?.background; + let updatedBackground: Background; + + if (type === ColorType.Solid) { + updatedBackground = isSolidColor(currentBackground) + ? { type: ColorType.Solid, color: currentBackground.color } + : { type: ColorType.Solid, color: "#FFFFFF" }; + } else if (type === ColorType.VerticalGradient) { + updatedBackground = isVerticalGradientColor(currentBackground) + ? { + type: ColorType.VerticalGradient, + topColor: currentBackground.topColor, + bottomColor: currentBackground.bottomColor, + } + : { + type: ColorType.VerticalGradient, + topColor: "#FFFFFF", + bottomColor: "#000000", + }; + } else { + console.error(`Unsupported ColorType: ${type}`); + return; + } + + this.handler.chart.applyOptions({ + layout: { + background: updatedBackground, + }, + }); + + if (type === ColorType.Solid) { + this.populateSolidBackgroundMenuInline( + event, + updatedBackground as SolidColor + ); + } else if (type === ColorType.VerticalGradient) { + this.populateGradientBackgroundMenuInline( + event, + updatedBackground as VerticalGradientColor + ); + } + } + private startFillAreaBetween(event: MouseEvent, originSeries: ISeriesApiExtended): void { + console.log("Fill Area Between started. Origin series set:", originSeries.options().title); + + // Ensure the series is decorated + + // Populate the Series List Menu + this.populateSeriesListMenu(event, (destinationSeries: ISeriesApi) => { + if (destinationSeries && destinationSeries !== originSeries) { + console.log("Destination series selected:", destinationSeries.options().title); + + // Ensure the destination series is also decorated + + // Instantiate and attach the FillArea + originSeries.primitives["FillArea"] = new FillArea(originSeries, destinationSeries, { + ...defaultFillAreaOptions, + }); + originSeries.attachPrimitive(originSeries.primitives['FillArea'],"Fill Area") + // Attach the FillArea as a primitive + //if (!originSeries.primitives['FillArea']) { + // originSeries.attachPrimitive(originSeries.primitives["FillArea"]) + //} + console.log("Fill Area successfully added between selected series."); + alert(`Fill Area added between ${originSeries.options().title} and ${destinationSeries.options().title}`); + } else { + alert("Invalid selection. Please choose a different series as the destination."); + } + }); +} + + +private getPredefinedOptions(label: string): string[] | null { + const predefined: Record = { + "Series Type": ["Line", "Histogram", "Area", "Bar", "Candlestick"], + "Line Style": [ + "Solid", + "Dotted", + "Dashed", + "Large Dashed", + "Sparse Dotted", + ], + "Line Type": ["Simple", "WithSteps", "Curved"], + "seriesType": ["Line", "Histogram", "Area", "Bar", "Candlestick"], + "lineStyle": [ + "Solid", + "Dotted", + "Dashed", + "Large Dashed", + "Sparse Dotted", + ], + "lineType": ["Simple", "WithSteps", "Curved"], + }; + + return predefined[camelToTitle(label)] || null; +} +/** + * Populates the Series List Menu for selecting the destination series. + * @param onSelect Callback when a series is selected. + */ +private populateSeriesListMenu(event: MouseEvent, onSelect: (series: ISeriesApi) => void): void { + this.div.innerHTML = ""; // Clear the current menu + + // Fetch all available series + const seriesOptions = Array.from(this.handler.seriesMap.entries()).map(([seriesName, series]) => ({ + label: seriesName, + value: series, + })); + + // Display series in the menu + seriesOptions.forEach((option) => { + this.addMenuItem(option.label, () => { + // Call the onSelect callback with the selected series + onSelect(option.value); + this.hideMenu(); // Close the menu after selection + }); + }); + + // Add a "Cancel" option to go back or exit + this.addMenuItem("Cancel", () => { + console.log("Operation canceled."); + this.hideMenu(); + }); + + this.showMenu(event); // Show the menu at the current mouse position +} + +private customizeFillAreaOptions(event: MouseEvent, FillArea: FillArea): void { + this.div.innerHTML = ""; // Clear current menu + + // Add color pickers for each color-related option + this.addColorPickerMenuItem( + "Origin Top Color", + FillArea.options.originColor, + "originColor", + FillArea + ); + + + this.addColorPickerMenuItem( + "Destination Top Color", + FillArea.options.destinationColor, + "destinationColor", + FillArea + ); + + + // Back to main menu + this.addMenuItem("⤝ Back to Main Menu", () => this.populateChartMenu(event), false); + + this.showMenu(event); +} + + + public addResetViewOption(): void { + const resetMenuItem = this.addMenuItem("Reset chart view ⟲", () => { + this.resetView(); + }); + this.div.appendChild(resetMenuItem); + } + } diff --git a/src/fill-area/fill-area.ts b/src/fill-area/fill-area.ts new file mode 100644 index 0000000..4aa74d3 --- /dev/null +++ b/src/fill-area/fill-area.ts @@ -0,0 +1,363 @@ +import { CanvasRenderingTarget2D } from "fancy-canvas"; +import { ISeriesPrimitivePaneRenderer, Coordinate, ISeriesPrimitivePaneView, Time, ISeriesPrimitive, SeriesAttachedParameter, DataChangedScope, SeriesDataItemTypeMap, SeriesType, Logical, AutoscaleInfo, BarData, LineData, ISeriesApi } from "lightweight-charts"; +import { PluginBase } from "../plugin-base"; +import { setOpacity } from "../helpers/colors"; +import { ClosestTimeIndexFinder } from '../helpers/closest-index'; +import { hasColorOption } from "../helpers/typeguards"; +export class FillArea extends PluginBase implements ISeriesPrimitive