diff --git a/docs/source/reference/abstract_chart.md b/docs/source/reference/abstract_chart.md index cddb08e..9a042dd 100644 --- a/docs/source/reference/abstract_chart.md +++ b/docs/source/reference/abstract_chart.md @@ -76,7 +76,7 @@ ___ -```{py:method} trend_line(start_time: str | datetime, start_value: NUM, end_time: str | datetime, end_value: NUM, color: COLOR, width: int) -> Line +```{py:method} trend_line(start_time: str | datetime, start_value: NUM, end_time: str | datetime, end_value: NUM, color: COLOR, width: int, style: LINE_STYLE) -> Line Creates a trend line, drawn from the first point (`start_time`, `start_value`) to the last point (`end_time`, `end_value`). @@ -85,7 +85,7 @@ ___ -```{py:method} ray_line(start_time: str | datetime, value: NUM, color: COLOR, width: int) -> Line +```{py:method} ray_line(start_time: str | datetime, value: NUM, color: COLOR, width: int, style: LINE_STYLE) -> Line Creates a ray line, drawn from the first point (`start_time`, `value`) and onwards. @@ -94,12 +94,14 @@ ___ -```{py:method} vertical_span(start_time: TIME, end_time: TIME = None, color: COLOR = 'rgba(252, 219, 3, 0.2)') +```{py:method} vertical_span(start_time: TIME | list | tuple, end_time: TIME = None, color: COLOR = 'rgba(252, 219, 3, 0.2)') Creates and returns a `VerticalSpan` object. If `end_time` is not given, then a single vertical line will be placed at `start_time`. +If a list/tuple is passed to `start_time`, vertical lines will be placed at each time. + This should be used after calling [`set`](#AbstractChart.set). ``` ___ diff --git a/docs/source/reference/toolbox.md b/docs/source/reference/toolbox.md index d340d53..d5bdcaf 100644 --- a/docs/source/reference/toolbox.md +++ b/docs/source/reference/toolbox.md @@ -15,7 +15,7 @@ The following hotkeys can also be used when the Toolbox is enabled: | `alt R` | Ray Line | | `⌘ Z` or `ctrl Z` | Undo | -Right-clicking on a drawing will open a context menu, allowing for color selection and deletion. +Right-clicking on a drawing will open a context menu, allowing for color selection, style selection and deletion. ___ diff --git a/lightweight_charts/abstract.py b/lightweight_charts/abstract.py index 7793e9c..d065fe0 100644 --- a/lightweight_charts/abstract.py +++ b/lightweight_charts/abstract.py @@ -121,7 +121,7 @@ class SeriesCommon(Pane): return self._interval = common_interval.index[0] self.run_script( - f'if ({self.id}.toolBox) {self.id}.toolBox.interval = {self._interval.total_seconds() * 1000}' + f'if ({self.id}.toolBox) {self.id}.interval = {self._interval.total_seconds() * 1000}' ) @staticmethod @@ -163,7 +163,7 @@ class SeriesCommon(Pane): def marker(self, time: datetime = None, position: MARKER_POSITION = 'below', shape: MARKER_SHAPE = 'arrow_up', color: str = '#2196F3', text: str = '' - ) -> 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. @@ -310,11 +310,11 @@ class HorizontalLine(Pane): class VerticalSpan(Pane): - def __init__(self, chart: 'AbstractChart', start_time: TIME, end_time: TIME = None, + def __init__(self, chart: 'AbstractChart', start_time: Union[TIME, tuple, list], end_time: TIME = None, color: str = 'rgba(252, 219, 3, 0.2)'): super().__init__(chart.win) self._chart = chart - start_date, end_date = pd.to_datetime(start_time), pd.to_datetime(end_time) + start_time, end_time = pd.to_datetime(start_time), pd.to_datetime(end_time) self.run_script(f''' {self.id} = {chart.id}.chart.addHistogramSeries({{ color: '{color}', @@ -327,12 +327,16 @@ class VerticalSpan(Pane): scaleMargins: {{top: 0, bottom: 0}} }}) ''') - if end_date is None: - self.run_script(f'{self.id}.setData([{{time: {start_date.timestamp()}, value: 1}}])') + if end_time is None: + if isinstance(start_time, pd.DatetimeIndex): + data = [{'time': time.timestamp(), 'value': 1} for time in start_time] + else: + data = [{'time': start_time.timestamp(), 'value': 1}] + self.run_script(f'{self.id}.setData({data})') else: self.run_script(f''' {self.id}.setData(calculateTrendLine( - {start_date.timestamp()}, 1, {end_date.timestamp()}, 1, + {start_time.timestamp()}, 1, {end_time.timestamp()}, 1, {chart._interval.total_seconds() * 1000}, {chart.id})) ''') @@ -340,7 +344,7 @@ class VerticalSpan(Pane): """ Irreversibly deletes the vertical span. """ - self.run_script(f'{self._chart.id}.chart.removeSeries({self.id}.series); delete {self.id}') + self.run_script(f'{self._chart.id}.chart.removeSeries({self.id})') class Line(SeriesCommon): @@ -370,6 +374,10 @@ class Line(SeriesCommon): color: '{color}', precision: 2, }} + ''') + + def _push_to_legend(self): + self.run_script(f''' {self._chart.id}.lines.push({self.id}) if ('legend' in {self._chart.id}) {{ {self._chart.id}.legend.lines.push({self._chart.id}.legend.makeLineRow({self.id})) @@ -621,6 +629,7 @@ class AbstractChart(Candlestick, Pane): Creates and returns a Line object.)\n """ self._lines.append(Line(self, name, color, style, width, price_line, price_label)) + self._lines[-1]._push_to_legend() return self._lines[-1] def lines(self) -> List[Line]: @@ -630,26 +639,24 @@ class AbstractChart(Candlestick, Pane): return self._lines.copy() def trend_line(self, start_time: TIME, start_value: NUM, end_time: TIME, end_value: NUM, - color: str = '#1E80F0', width: int = 2 + color: str = '#1E80F0', width: int = 2, style: LINE_STYLE = 'solid' ) -> Line: - line = Line(self, '', color, 'solid', width, False, False, False) + line = Line(self, '', color, style, width, False, False, False) line._set_trend(start_time, start_value, end_time, end_value) return line def ray_line(self, start_time: TIME, value: NUM, - color: str = '#1E80F0', width: int = 2 + color: str = '#1E80F0', width: int = 2, style: LINE_STYLE = 'solid' ) -> Line: - line = Line(self, '', color, 'solid', width, False, False, False) + line = Line(self, '', color, style, width, False, False, False) line._set_trend(start_time, value, start_time, value, ray=True) return line - def vertical_span(self, start_time: TIME, end_time: TIME = None, color: str = 'rgba(252, 219, 3, 0.2)'): + def vertical_span(self, start_time: Union[TIME, tuple, list], end_time: TIME = None, color: str = 'rgba(252, 219, 3, 0.2)'): """ - Creates a vertical line or span across the chart. - :param start_time: Start time of the span. - :param end_time: End time of the span (can be omitted for a single vertical line). - :param color: CSS color. - :return: + 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. """ return VerticalSpan(self, start_time, end_time, color) diff --git a/lightweight_charts/js/funcs.js b/lightweight_charts/js/funcs.js index 11075aa..1397ace 100644 --- a/lightweight_charts/js/funcs.js +++ b/lightweight_charts/js/funcs.js @@ -5,6 +5,7 @@ if (!window.Chart) { this.reSize = this.reSize.bind(this) this.id = chartId this.lines = [] + this.interval = null this.wrapper = document.createElement('div') this.div = document.createElement('div') this.scale = { @@ -143,6 +144,12 @@ if (!window.Chart) { this.line = this.chart.series.createPriceLine(this.priceLine) } + updateStyle(style) { + this.chart.series.removePriceLine(this.line) + this.priceLine.lineStyle = style + this.line = this.chart.series.createPriceLine(this.priceLine) + } + deleteLine() { this.chart.series.removePriceLine(this.line) this.chart.horizontal_lines.splice(this.chart.horizontal_lines.indexOf(this)) @@ -297,6 +304,7 @@ if (!window.Chart) { window.Legend = Legend } + function syncCharts(childChart, parentChart) { syncCrosshairs(childChart.chart, parentChart.chart) syncRanges(childChart, parentChart) @@ -462,26 +470,28 @@ if (!window.ContextMenu) { let 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) } - elem.addEventListener('mouseover', (event) => { + item.addEventListener('mouseover', (event) => { item.style.backgroundColor = 'rgba(0, 122, 255, 0.3)' if (this.hoverItem && this.hoverItem.closeAction) this.hoverItem.closeAction() this.hoverItem = {elem: elem, action: action, closeAction: hover} }) - elem.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent') - if (!hover) elem.addEventListener('click', (event) => {action(event); this.menu.style.display = 'none'}) + item.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent') + if (!hover) item.addEventListener('click', (event) => {action(event); this.menu.style.display = 'none'}) else { let timeout - elem.addEventListener('mouseover', () => timeout = setTimeout(() => action(item.getBoundingClientRect()), 100)) - elem.addEventListener('mouseout', () => clearTimeout(timeout)) + item.addEventListener('mouseover', () => timeout = setTimeout(() => action(item.getBoundingClientRect()), 100)) + item.addEventListener('mouseout', () => clearTimeout(timeout)) } } separator() { diff --git a/lightweight_charts/js/toolbox.js b/lightweight_charts/js/toolbox.js index 47be952..ea29529 100644 --- a/lightweight_charts/js/toolbox.js +++ b/lightweight_charts/js/toolbox.js @@ -11,7 +11,6 @@ if (!window.ToolBox) { this.chart.cursor = 'default' this.makingDrawing = false - this.interval = 24 * 60 * 60 * 1000 this.activeBackgroundColor = 'rgba(0, 122, 255, 0.7)' this.activeIconColor = 'rgb(240, 240, 240)' this.iconColor = 'lightgrey' @@ -174,13 +173,13 @@ if (!window.ToolBox) { currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x) if (!currentTime) { let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime)) - currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval))) + currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.chart.interval))) } let currentPrice = this.chart.series.coordinateToPrice(param.point.y) if (!currentTime) return this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) - let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart, ray) + let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.chart.interval, this.chart, ray) trendLine.from = [data[0].time, data[0].value] trendLine.to = [data[data.length - 1].time, data[data.length-1].value] @@ -263,14 +262,20 @@ if (!window.ToolBox) { let hoveringOver = null let x, y let colorPicker = new ColorPicker(this.saveDrawings) + let stylePicker = new StylePicker(this.saveDrawings) let onClickDelete = () => this.deleteDrawing(contextMenu.drawing) let onClickColor = (rect) => colorPicker.openMenu(rect, contextMenu.drawing) + let onClickStyle = (rect) => stylePicker.openMenu(rect, contextMenu.drawing) let contextMenu = new ContextMenu() contextMenu.menuItem('Color Picker', onClickColor, () =>{ document.removeEventListener('click', colorPicker.closeMenu) colorPicker.container.style.display = 'none' }) + contextMenu.menuItem('Style', onClickStyle, () => { + document.removeEventListener('click', stylePicker.closeMenu) + stylePicker.container.style.display = 'none' + }) contextMenu.separator() contextMenu.menuItem('Delete Drawing', onClickDelete) @@ -390,10 +395,10 @@ if (!window.ToolBox) { endBar = endBarIndex === -1 ? null : this.chart.candleData[endBarIndex + barsToMove] } - let endDate = endBar ? endBar.time : dateToStamp(new Date(stampToDate(hoveringOver.to[0]).getTime() + (barsToMove * this.interval))) + let endDate = endBar ? endBar.time : dateToStamp(new Date(stampToDate(hoveringOver.to[0]).getTime() + (barsToMove * this.chart.interval))) let startValue = hoveringOver.from[1] + priceDiff let endValue = hoveringOver.to[1] + priceDiff - let data = calculateTrendLine(startDate, startValue, endDate, endValue, this.interval, this.chart, hoveringOver.ray) + let data = calculateTrendLine(startDate, startValue, endDate, endValue, this.chart.interval, this.chart, hoveringOver.ray) let logical = this.chart.chart.timeScale().getVisibleLogicalRange() @@ -439,9 +444,9 @@ if (!window.ToolBox) { let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time if (!currentTime) { let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime)) - currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval))) + currentTime = dateToStamp(new Date(stampToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.chart.interval))) } - let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart) + let data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.chart.interval, this.chart) hoveringOver.line.setData(data) this.chart.chart.timeScale().applyOptions({shiftVisibleRangeOnNewBar: true}) @@ -476,9 +481,9 @@ if (!window.ToolBox) { renderDrawings() { this.drawings.forEach((item) => { if ('price' in item) return - let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.interval) * this.interval)) - let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.interval) * this.interval)) - let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray) + let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.chart.interval) * this.chart.interval)) + let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.chart.interval) * this.chart.interval)) + let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.chart.interval, this.chart, item.ray) item.from = [data[0].time, data[0].value] item.to = [data[data.length - 1].time, data[data.length-1].value] item.line.setData(data) @@ -541,9 +546,9 @@ if (!window.ToolBox) { }, }), }) - let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.interval) * this.interval)) - let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.interval) * this.interval)) - let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.interval, this.chart, item.ray) + let startDate = dateToStamp(new Date(Math.round(stampToDate(item.from[0]).getTime() / this.chart.interval) * this.chart.interval)) + let endDate = dateToStamp(new Date(Math.round(stampToDate(item.to[0]).getTime() / this.chart.interval) * this.chart.interval)) + let data = calculateTrendLine(startDate, item.from[1], endDate, item.to[1], this.chart.interval, this.chart, item.ray) item.from = [data[0].time, data[0].value] item.to = [data[data.length - 1].time, data[data.length-1].value] item.line.setData(data) @@ -554,9 +559,7 @@ if (!window.ToolBox) { } } window.ToolBox = ToolBox -} -if (!window.ColorPicker) { class ColorPicker { constructor(saveDrawings) { this.saveDrawings = saveDrawings @@ -685,4 +688,77 @@ if (!window.ColorPicker) { } } window.ColorPicker = ColorPicker + class StylePicker { + constructor(saveDrawings) { + this.saveDrawings = saveDrawings + + this.container = document.createElement('div') + this.container.style.position = 'absolute' + this.container.style.zIndex = '10000' + this.container.style.background = 'rgb(50, 50, 50)' + this.container.style.color = '#ececed' + this.container.style.display = 'none' + this.container.style.borderRadius = '5px' + this.container.style.padding = '3px 3px' + this.container.style.fontSize = '13px' + this.container.style.cursor = 'default' + + let styles = [ + {name: 'Solid', var: LightweightCharts.LineStyle.Solid}, + {name: 'Dotted', var: LightweightCharts.LineStyle.Dotted}, + {name: 'Dashed', var: LightweightCharts.LineStyle.Dashed}, + {name: 'Large Dashed', var: LightweightCharts.LineStyle.LargeDashed}, + {name: 'Sparse Dotted', var: LightweightCharts.LineStyle.SparseDotted}, + ] + styles.forEach((style) => { + this.container.appendChild(this.makeTextBox(style.name, style.var)) + }) + + document.getElementById('wrapper').appendChild(this.container) + + } + makeTextBox(text, style) { + let item = document.createElement('span') + item.style.display = 'flex' + item.style.alignItems = 'center' + item.style.justifyContent = 'space-between' + item.style.padding = '2px 10px' + item.style.margin = '1px 0px' + item.style.borderRadius = '3px' + item.innerText = text + + item.addEventListener('mouseover', (event) => item.style.backgroundColor = 'rgba(0, 122, 255, 0.3)') + item.addEventListener('mouseout', (event) => item.style.backgroundColor = 'transparent') + + item.addEventListener('click', (event) => { + this.style = style + this.updateStyle() + }) + return item + } + + updateStyle() { + if ('price' in this.drawing) this.drawing.updateStyle(this.style) + else { + this.drawing.line.applyOptions({lineStyle: this.style}) + } + this.saveDrawings() + } + openMenu(rect, drawing) { + this.drawing = drawing + this.container.style.top = (rect.top-30)+'px' + this.container.style.left = rect.right+'px' + this.container.style.display = 'block' + setTimeout(() => document.addEventListener('mousedown', (event) => { + if (!this.container.contains(event.target)) { + this.closeMenu() + } + }), 10) + } + closeMenu(event) { + document.removeEventListener('click', this.closeMenu) + this.container.style.display = 'none' + } + } + window.ColorPicker = ColorPicker }