Enhancements & Bug Fixes

- Updated to Lightweight Charts 4.1
- Topbar menu widgets will now scroll when a large number of items are added to them
- Vertical Spans can now be placed on Line objects

Bugs
- Histograms will now be deleted from the legend
- autoScale is reset to true upon using `set`.
This commit is contained in:
louisnw
2023-10-31 14:19:40 +00:00
parent 5bb3739a40
commit fecfb6531c
6 changed files with 94 additions and 88 deletions

View File

@ -227,16 +227,24 @@ class SeriesCommon(Pane):
df = df.rename(columns={self.name: 'value'}) df = df.rename(columns={self.name: 'value'})
self.data = df.copy() self.data = df.copy()
self._last_bar = df.iloc[-1] self._last_bar = df.iloc[-1]
self.run_script(f'{self.id}.series.setData({js_data(df)})') self.run_script(f'{self.id}.data = {js_data(df)}; {self.id}.series.setData({self.id}.data); ')
def update(self, series: pd.Series): def update(self, series: pd.Series):
series = self._series_datetime_format(series, exclude_lowercase=self.name) series = self._series_datetime_format(series, exclude_lowercase=self.name)
if self.name in series.index: if self.name in series.index:
series.rename({self.name: 'value'}, inplace=True) series.rename({self.name: 'value'}, inplace=True)
if series['time'] != self._last_bar['time']: if self._last_bar and series['time'] != self._last_bar['time']:
self.data.loc[self.data.index[-1]] = self._last_bar self.data.loc[self.data.index[-1]] = self._last_bar
self.data = pd.concat([self.data, series.to_frame().T], ignore_index=True) self.data = pd.concat([self.data, series.to_frame().T], ignore_index=True)
self._last_bar = series self._last_bar = series
bar = js_data(series)
self.run_script(f'''
if (stampToDate(lastBar({self.id}.data).time).getTime() === stampToDate({series['time']}).getTime()) {{
{self.id}.data[{self.id}.data.length-1] = {bar}
}}
else {self.id}.data.push({bar})
{self.id}.series.update({bar})
''')
self.run_script(f'{self.id}.series.update({js_data(series)})') self.run_script(f'{self.id}.series.update({js_data(series)})')
def marker(self, time: datetime = None, position: MARKER_POSITION = 'below', def marker(self, time: datetime = None, position: MARKER_POSITION = 'below',
@ -345,6 +353,15 @@ class SeriesCommon(Pane):
if ('volumeSeries' in {self.id}) {self.id}.volumeSeries.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: TIME = None,
color: str = 'rgba(252, 219, 3, 0.2)'):
"""
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)
class HorizontalLine(Pane): class HorizontalLine(Pane):
def __init__(self, chart, price, color, width, style, text, axis_label_visible, func): def __init__(self, chart, price, color, width, style, text, axis_label_visible, func):
@ -388,13 +405,13 @@ class HorizontalLine(Pane):
class VerticalSpan(Pane): class VerticalSpan(Pane):
def __init__(self, chart: 'AbstractChart', start_time: Union[TIME, tuple, list], end_time: TIME = None, def __init__(self, series: 'SeriesCommon', start_time: Union[TIME, tuple, list], end_time: TIME = None,
color: str = 'rgba(252, 219, 3, 0.2)'): color: str = 'rgba(252, 219, 3, 0.2)'):
super().__init__(chart.win) self._chart = series._chart
self._chart = chart super().__init__(self._chart.win)
start_time, end_time = 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.run_script(f'''
{self.id} = {chart.id}.chart.addHistogramSeries({{ {self.id} = {self._chart.id}.chart.addHistogramSeries({{
color: '{color}', color: '{color}',
priceFormat: {{type: 'volume'}}, priceFormat: {{type: 'volume'}},
priceScaleId: 'vertical_line', priceScaleId: 'vertical_line',
@ -414,7 +431,7 @@ class VerticalSpan(Pane):
else: else:
self.run_script(f''' self.run_script(f'''
{self.id}.setData(calculateTrendLine( {self.id}.setData(calculateTrendLine(
{start_time.timestamp()}, 1, {end_time.timestamp()}, 1, {chart.id})) {start_time.timestamp()}, 1, {end_time.timestamp()}, 1, {series.id}))
''') ''')
def delete(self): def delete(self):
@ -510,6 +527,9 @@ class Histogram(SeriesCommon):
""" """
self.run_script(f''' self.run_script(f'''
{self._chart.id}.chart.removeSeries({self.id}.series) {self._chart.id}.chart.removeSeries({self.id}.series)
{self._chart.id}.legend.lines.forEach(line => {{
if (line.line === {self.id}) {self._chart.id}.legend.div.removeChild(line.row)
}})
delete {self.id} delete {self.id}
''') ''')
@ -545,7 +565,7 @@ class Candlestick(SeriesCommon):
self.candle_data = df.copy() self.candle_data = df.copy()
self._last_bar = df.iloc[-1] self._last_bar = df.iloc[-1]
self.run_script(f'{self.id}.candleData = {js_data(df)}; {self.id}.series.setData({self.id}.candleData)') self.run_script(f'{self.id}.data = {js_data(df)}; {self.id}.series.setData({self.id}.data)')
toolbox_action = 'clearDrawings' if not render_drawings else 'renderDrawings' toolbox_action = 'clearDrawings' if not render_drawings else 'renderDrawings'
self.run_script(f"if ('toolBox' in {self._chart.id}) {self._chart.id}.toolBox.{toolbox_action}()") self.run_script(f"if ('toolBox' in {self._chart.id}) {self._chart.id}.toolBox.{toolbox_action}()")
if 'volume' not in df: if 'volume' not in df:
@ -559,6 +579,11 @@ class Candlestick(SeriesCommon):
if line.name not in df.columns: if line.name not in df.columns:
continue continue
line.set(df[['time', line.name]], format_cols=False) 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}})
''')
def update(self, series: pd.Series, _from_tick=False): def update(self, series: pd.Series, _from_tick=False):
""" """
@ -574,10 +599,10 @@ class Candlestick(SeriesCommon):
self._last_bar = series self._last_bar = series
bar = js_data(series) bar = js_data(series)
self.run_script(f''' self.run_script(f'''
if (stampToDate(lastBar({self.id}.candleData).time).getTime() === stampToDate({series['time']}).getTime()) {{ if (stampToDate(lastBar({self.id}.data).time).getTime() === stampToDate({series['time']}).getTime()) {{
{self.id}.candleData[{self.id}.candleData.length-1] = {bar} {self.id}.data[{self.id}.data.length-1] = {bar}
}} }}
else {self.id}.candleData.push({bar}) else {self.id}.data.push({bar})
{self.id}.series.update({bar}) {self.id}.series.update({bar})
''') ''')
if 'volume' not in series: if 'volume' not in series:
@ -750,15 +775,6 @@ class AbstractChart(Candlestick, Pane):
line._set_trend(start_time, value, start_time, value, ray=True, round=round) line._set_trend(start_time, value, start_time, value, ray=True, round=round)
return line return line
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.\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)
def set_visible_range(self, start_time: TIME, end_time: TIME): def set_visible_range(self, start_time: TIME, end_time: TIME):
self.run_script(f''' self.run_script(f'''
{self.id}.chart.timeScale().setVisibleRange({{ {self.id}.chart.timeScale().setVisibleRange({{

View File

@ -90,6 +90,9 @@ if (!window.TopBar) {
menu.style.border = '2px solid '+pane.borderColor menu.style.border = '2px solid '+pane.borderColor
menu.style.borderTop = 'none' menu.style.borderTop = 'none'
menu.style.alignItems = 'flex-start' menu.style.alignItems = 'flex-start'
menu.style.maxHeight = '80%'
menu.style.overflowY = 'auto'
menu.style.scrollbar
let menuOpen = false let menuOpen = false
items.forEach(text => { items.forEach(text => {

View File

@ -91,7 +91,7 @@ if (!window.Chart) {
makeCandlestickSeries() { makeCandlestickSeries() {
this.markers = [] this.markers = []
this.horizontal_lines = [] this.horizontal_lines = []
this.candleData = [] this.data = []
this.precision = 2 this.precision = 2
let up = 'rgba(39, 157, 130, 100)' let up = 'rgba(39, 157, 130, 100)'
let down = 'rgba(200, 97, 100, 100)' let down = 'rgba(200, 97, 100, 100)'
@ -336,59 +336,46 @@ if (!window.Chart) {
} }
function syncCharts(childChart, parentChart) { function syncCharts(childChart, parentChart) {
syncCrosshairs(childChart.chart, parentChart.chart)
syncRanges(childChart, parentChart)
}
function syncCrosshairs(childChart, parentChart) {
function crosshairHandler (e, thisChart, otherChart, otherHandler) {
thisChart.applyOptions({crosshair: { horzLine: {
visible: true,
labelVisible: true,
}}})
otherChart.applyOptions({crosshair: { horzLine: {
visible: false,
labelVisible: false,
}}})
otherChart.unsubscribeCrosshairMove(otherHandler) function crosshairHandler(chart, series, point) {
if (e.time !== undefined) { if (!point) {
let xx = otherChart.timeScale().timeToCoordinate(e.time); chart.clearCrosshairPosition()
otherChart.setCrosshairXY(xx,300,true); return
} else if (e.point !== undefined){
otherChart.setCrosshairXY(e.point.x,300,false);
} }
otherChart.subscribeCrosshairMove(otherHandler) chart.setCrosshairPosition(point.value || point.close, point.time, series);
} }
let parent = 0
let child = 0 function getPoint(series, param) {
let parentCrosshairHandler = (e) => { if (!param.time) return null;
parent ++ return param.seriesData.get(series) || null;
if (parent < 10) return
child = 0
crosshairHandler(e, parentChart, childChart, childCrosshairHandler)
} }
let childCrosshairHandler = (e) => {
child ++
if (child < 10) return
parent = 0
crosshairHandler(e, childChart, parentChart, parentCrosshairHandler)
}
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
childChart.subscribeCrosshairMove(childCrosshairHandler)
}
function syncRanges(childChart, parentChart) {
let setChildRange = (timeRange) => childChart.chart.timeScale().setVisibleLogicalRange(timeRange) let setChildRange = (timeRange) => childChart.chart.timeScale().setVisibleLogicalRange(timeRange)
let setParentRange = (timeRange) => parentChart.chart.timeScale().setVisibleLogicalRange(timeRange) let setParentRange = (timeRange) => parentChart.chart.timeScale().setVisibleLogicalRange(timeRange)
parentChart.wrapper.addEventListener('mouseover', (event) => { let setParentCrosshair = (param) => {
childChart.chart.timeScale().unsubscribeVisibleLogicalRangeChange(setParentRange) crosshairHandler(parentChart.chart, parentChart.series, getPoint(childChart.series, param))
parentChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setChildRange) }
}) let setChildCrosshair = (param) => {
childChart.wrapper.addEventListener('mouseover', (event) => { crosshairHandler(childChart.chart, childChart.series, getPoint(parentChart.series, param))
parentChart.chart.timeScale().unsubscribeVisibleLogicalRangeChange(setChildRange) }
childChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setParentRange)
}) let selected = parentChart
function addMouseOverListener(thisChart, otherChart, thisCrosshair, otherCrosshair, thisRange, otherRange) {
thisChart.wrapper.addEventListener('mouseover', (event) => {
if (selected === thisChart) return
selected = thisChart
otherChart.chart.timeScale().unsubscribeVisibleLogicalRangeChange(thisRange)
otherChart.chart.unsubscribeCrosshairMove(thisCrosshair)
thisChart.chart.timeScale().subscribeVisibleLogicalRangeChange(otherRange)
thisChart.chart.subscribeCrosshairMove(otherCrosshair)
})
}
addMouseOverListener(parentChart, childChart, setParentCrosshair, setChildCrosshair, setParentRange, setChildRange)
addMouseOverListener(childChart, parentChart, setChildCrosshair, setParentCrosshair, setChildRange, setParentRange)
parentChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setChildRange) parentChart.chart.timeScale().subscribeVisibleLogicalRangeChange(setChildRange)
parentChart.chart.subscribeCrosshairMove(setChildCrosshair)
} }
function stampToDate(stampOrBusiness) { function stampToDate(stampOrBusiness) {
@ -409,11 +396,11 @@ function calculateTrendLine(startDate, startValue, endDate, endValue, chart, ray
[startDate, endDate] = [endDate, startDate]; [startDate, endDate] = [endDate, startDate];
} }
let startIndex let startIndex
if (stampToDate(startDate).getTime() < stampToDate(chart.candleData[0].time).getTime()) { if (stampToDate(startDate).getTime() < stampToDate(chart.data[0].time).getTime()) {
startIndex = 0 startIndex = 0
} }
else { else {
startIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(startDate).getTime()) startIndex = chart.data.findIndex(item => stampToDate(item.time).getTime() === stampToDate(startDate).getTime())
} }
if (startIndex === -1) { if (startIndex === -1) {
@ -421,14 +408,14 @@ function calculateTrendLine(startDate, startValue, endDate, endValue, chart, ray
} }
let endIndex let endIndex
if (ray) { if (ray) {
endIndex = chart.candleData.length+1000 endIndex = chart.data.length+1000
startValue = endValue startValue = endValue
} }
else { else {
endIndex = chart.candleData.findIndex(item => stampToDate(item.time).getTime() === stampToDate(endDate).getTime()) endIndex = chart.data.findIndex(item => stampToDate(item.time).getTime() === stampToDate(endDate).getTime())
if (endIndex === -1) { if (endIndex === -1) {
let barsBetween = (endDate-lastBar(chart.candleData).time)/chart.interval let barsBetween = (endDate-lastBar(chart.data).time)/chart.interval
endIndex = chart.candleData.length-1+barsBetween endIndex = chart.data.length-1+barsBetween
} }
} }
@ -438,12 +425,12 @@ function calculateTrendLine(startDate, startValue, endDate, endValue, chart, ray
let currentDate = null let currentDate = null
let iPastData = 0 let iPastData = 0
for (let i = 0; i <= numBars; i++) { for (let i = 0; i <= numBars; i++) {
if (chart.candleData[startIndex+i]) { if (chart.data[startIndex+i]) {
currentDate = chart.candleData[startIndex+i].time currentDate = chart.data[startIndex+i].time
} }
else { else {
iPastData ++ iPastData ++
currentDate = lastBar(chart.candleData).time+(iPastData*chart.interval) currentDate = lastBar(chart.data).time+(iPastData*chart.interval)
} }
const currentValue = reversed ? startValue + rate_of_change * (numBars - i) : startValue + rate_of_change * i; const currentValue = reversed ? startValue + rate_of_change * (numBars - i) : startValue + rate_of_change * i;

File diff suppressed because one or more lines are too long

View File

@ -161,8 +161,8 @@ if (!window.ToolBox) {
currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x) currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x)
if (!currentTime) { if (!currentTime) {
let barsToMove = param.logical - this.chart.candleData.length-1 let barsToMove = param.logical - this.chart.data.length-1
currentTime = lastBar(this.chart.candleData).time+(barsToMove*this.chart.interval) currentTime = lastBar(this.chart.data).time+(barsToMove*this.chart.interval)
} }
let currentPrice = this.chart.series.coordinateToPrice(param.point.y) let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
@ -179,7 +179,7 @@ if (!window.ToolBox) {
this.makingDrawing = true this.makingDrawing = true
trendLine = new TrendLine(this.chart, 'rgb(15, 139, 237)', ray) trendLine = new TrendLine(this.chart, 'rgb(15, 139, 237)', ray)
firstPrice = this.chart.series.coordinateToPrice(param.point.y) firstPrice = this.chart.series.coordinateToPrice(param.point.y)
firstTime = !ray ? this.chart.chart.timeScale().coordinateToTime(param.point.x) : lastBar(this.chart.candleData).time firstTime = !ray ? this.chart.chart.timeScale().coordinateToTime(param.point.x) : lastBar(this.chart.data).time
this.chart.chart.applyOptions({handleScroll: false}) this.chart.chart.applyOptions({handleScroll: false})
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend) this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
} }
@ -337,17 +337,17 @@ if (!window.ToolBox) {
let priceDiff = priceAtCursor - originalPrice let priceDiff = priceAtCursor - originalPrice
let barsToMove = param.logical - originalIndex let barsToMove = param.logical - originalIndex
let startBarIndex = this.chart.candleData.findIndex(item => item.time === hoveringOver.from[0]) let startBarIndex = this.chart.data.findIndex(item => item.time === hoveringOver.from[0])
let endBarIndex = this.chart.candleData.findIndex(item => item.time === hoveringOver.to[0]) let endBarIndex = this.chart.data.findIndex(item => item.time === hoveringOver.to[0])
let startDate let startDate
let endBar let endBar
if (hoveringOver.ray) { if (hoveringOver.ray) {
endBar = this.chart.candleData[startBarIndex + barsToMove] endBar = this.chart.data[startBarIndex + barsToMove]
startDate = hoveringOver.to[0] startDate = hoveringOver.to[0]
} else { } else {
startDate = this.chart.candleData[startBarIndex + barsToMove].time startDate = this.chart.data[startBarIndex + barsToMove].time
endBar = endBarIndex === -1 ? null : this.chart.candleData[endBarIndex + barsToMove] endBar = endBarIndex === -1 ? null : this.chart.data[endBarIndex + barsToMove]
} }
let endDate = endBar ? endBar.time : hoveringOver.to[0] + (barsToMove * this.chart.interval) let endDate = endBar ? endBar.time : hoveringOver.to[0] + (barsToMove * this.chart.interval)
@ -378,8 +378,8 @@ if (!window.ToolBox) {
} }
if (!currentTime) { if (!currentTime) {
let barsToMove = param.logical - this.chart.candleData.length-1 let barsToMove = param.logical - this.chart.data.length-1
currentTime = lastBar(this.chart.candleData).time + (barsToMove*this.chart.interval) currentTime = lastBar(this.chart.data).time + (barsToMove*this.chart.interval)
} }
hoveringOver.calculateAndSet(firstTime, firstPrice, currentTime, currentPrice) hoveringOver.calculateAndSet(firstTime, firstPrice, currentTime, currentPrice)

View File

@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
setup( setup(
name='lightweight_charts', name='lightweight_charts',
version='1.0.18.2', version='1.0.18.3',
packages=find_packages(), packages=find_packages(),
python_requires='>=3.8', python_requires='>=3.8',
install_requires=[ install_requires=[