804 lines
22 KiB
Python
804 lines
22 KiB
Python
# piker: trading gear for hackers
|
|
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
"""
|
|
Lines for orders, alerts, L2.
|
|
|
|
"""
|
|
from functools import partial
|
|
from math import floor
|
|
from typing import Optional, Callable
|
|
|
|
import pyqtgraph as pg
|
|
from pyqtgraph import Point, functions as fn
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
from PyQt5.QtCore import QPointF
|
|
|
|
from ._annotate import qgo_draw_markers, LevelMarker
|
|
from ._anchors import (
|
|
vbr_left,
|
|
right_axis,
|
|
gpath_pin,
|
|
)
|
|
from ..calc import humanize
|
|
from ._label import Label
|
|
from ._style import hcolor, _font
|
|
|
|
|
|
# TODO: probably worth investigating if we can
|
|
# make .boundingRect() faster:
|
|
# https://stackoverflow.com/questions/26156486/determine-bounding-rect-of-line-in-qt
|
|
class LevelLine(pg.InfiniteLine):
|
|
|
|
def __init__(
|
|
self,
|
|
chart: 'ChartPlotWidget', # type: ignore # noqa
|
|
|
|
# style
|
|
color: str = 'default',
|
|
highlight_color: str = 'default_light',
|
|
dotted: bool = False,
|
|
|
|
# UX look and feel opts
|
|
always_show_labels: bool = False,
|
|
highlight_on_hover: bool = True,
|
|
hide_xhair_on_hover: bool = True,
|
|
only_show_markers_on_hover: bool = True,
|
|
use_marker_margin: bool = False,
|
|
movable: bool = True,
|
|
|
|
) -> None:
|
|
|
|
# TODO: at this point it's probably not worth the inheritance
|
|
# any more since we've reimplemented ``.pain()`` among other
|
|
# things..
|
|
super().__init__(
|
|
movable=movable,
|
|
angle=0,
|
|
|
|
# don't use the shitty ``InfLineLabel``
|
|
label=None,
|
|
)
|
|
|
|
self._chart = chart
|
|
self.highlight_on_hover = highlight_on_hover
|
|
self._dotted = dotted
|
|
self._hide_xhair_on_hover = hide_xhair_on_hover
|
|
|
|
# callback that can be assigned by user code
|
|
# to get updates from each level change
|
|
self._on_level_change: Callable[[float], None] = lambda y: None
|
|
|
|
self._marker = None
|
|
self.only_show_markers_on_hover = only_show_markers_on_hover
|
|
self.show_markers: bool = True # presuming the line is hovered at init
|
|
|
|
# should line go all the way to far end or leave a "margin"
|
|
# space for other graphics (eg. L1 book)
|
|
self.use_marker_margin: bool = use_marker_margin
|
|
|
|
if dotted:
|
|
self._style = QtCore.Qt.DashLine
|
|
else:
|
|
self._style = QtCore.Qt.SolidLine
|
|
|
|
self._hcolor: str = None
|
|
|
|
# the float y-value in the view coords
|
|
self.level: float = 0
|
|
|
|
# list of labels anchored at one of the 2 line endpoints
|
|
# inside the viewbox
|
|
self._labels: list[Label] = []
|
|
self._markers: list[(int, Label)] = []
|
|
|
|
# whenever this line is moved trigger label updates
|
|
self.sigPositionChanged.connect(self.on_pos_change)
|
|
|
|
# sets color to value triggering pen creation
|
|
self._hl_color = highlight_color
|
|
self.color = color
|
|
|
|
# TODO: for when we want to move groups of lines?
|
|
self._track_cursor: bool = False
|
|
self.always_show_labels = always_show_labels
|
|
|
|
self._on_drag_start = lambda l: None
|
|
self._on_drag_end = lambda l: None
|
|
|
|
self._y_incr_mult = 1 / chart.linked.symbol.tick_size
|
|
self._right_end_sc: float = 0
|
|
|
|
def txt_offsets(self) -> tuple[int, int]:
|
|
return 0, 0
|
|
|
|
@property
|
|
def color(self):
|
|
return self._hcolor
|
|
|
|
@color.setter
|
|
def color(self, color: str) -> None:
|
|
# set pens to new color
|
|
self._hcolor = color
|
|
pen = pg.mkPen(hcolor(color))
|
|
hoverpen = pg.mkPen(hcolor(self._hl_color))
|
|
|
|
pen.setStyle(self._style)
|
|
hoverpen.setStyle(self._style)
|
|
|
|
# set regular pen
|
|
self.setPen(pen)
|
|
|
|
# use slightly thicker highlight for hover pen
|
|
hoverpen.setWidth(2)
|
|
self.hoverPen = hoverpen
|
|
|
|
def on_pos_change(
|
|
self,
|
|
line: 'LevelLine', # noqa
|
|
) -> None:
|
|
"""Position changed handler.
|
|
|
|
"""
|
|
level = self.value()
|
|
self.update_labels({'level': level})
|
|
self.set_level(level, called_from_on_pos_change=True)
|
|
|
|
def update_labels(
|
|
self,
|
|
fields_data: dict,
|
|
|
|
) -> None:
|
|
|
|
for label in self._labels:
|
|
|
|
label.color = self.color
|
|
label.fields.update(fields_data)
|
|
label.render()
|
|
|
|
level = fields_data.get('level')
|
|
if level:
|
|
label.set_view_pos(y=level)
|
|
|
|
self.update()
|
|
|
|
def hide_labels(self) -> None:
|
|
for label in self._labels:
|
|
label.hide()
|
|
|
|
def show_labels(self) -> None:
|
|
for label in self._labels:
|
|
label.show()
|
|
|
|
def set_level(
|
|
self,
|
|
level: float,
|
|
called_from_on_pos_change: bool = False,
|
|
|
|
) -> None:
|
|
|
|
if not called_from_on_pos_change:
|
|
last = self.value()
|
|
|
|
# if the position hasn't changed then ``.update_labels()``
|
|
# will not be called by a non-triggered `.on_pos_change()`,
|
|
# so we need to call it manually to avoid mismatching
|
|
# label-to-line color when the line is updated but not
|
|
# from a "moved" event.
|
|
if level == last:
|
|
self.update_labels({'level': level})
|
|
|
|
self.setPos(level)
|
|
|
|
self.level = self.value()
|
|
self.update()
|
|
|
|
# invoke any user code
|
|
self._on_level_change(level)
|
|
|
|
def on_tracked_source(
|
|
self,
|
|
|
|
x: int,
|
|
y: float
|
|
|
|
) -> None:
|
|
'''Chart coordinates cursor tracking callback.
|
|
|
|
this is called by our ``Cursor`` type once this line is set to
|
|
track the cursor: for every movement this callback is invoked to
|
|
reposition the line with the current view coordinates.
|
|
'''
|
|
self.movable = True
|
|
self.set_level(y) # implictly calls reposition handler
|
|
|
|
def mouseDragEvent(self, ev):
|
|
"""Override the ``InfiniteLine`` handler since we need more
|
|
detailed control and start end signalling.
|
|
|
|
"""
|
|
cursor = self._chart.linked.cursor
|
|
|
|
# hide y-crosshair
|
|
cursor.hide_xhair()
|
|
|
|
# highlight
|
|
self.currentPen = self.hoverPen
|
|
self.show_labels()
|
|
|
|
# XXX: normal tracking behavior pulled out from parent type
|
|
if self.movable and ev.button() == QtCore.Qt.LeftButton:
|
|
ev.accept()
|
|
|
|
if ev.isStart():
|
|
self.moving = True
|
|
down_pos = ev.buttonDownPos()
|
|
self.cursorOffset = self.pos() - self.mapToParent(down_pos)
|
|
self.startPosition = self.pos()
|
|
|
|
self._on_drag_start(self)
|
|
|
|
if not self.moving:
|
|
return
|
|
|
|
pos = self.cursorOffset + self.mapToParent(ev.pos())
|
|
|
|
# TODO: we should probably figure out a std api
|
|
# for this kind of thing given we already have
|
|
# it on the cursor system...
|
|
|
|
# round to nearest symbol tick
|
|
m = self._y_incr_mult
|
|
self.setPos(
|
|
QPointF(
|
|
self.pos().x(), # don't allow shifting horizontally
|
|
round(pos.y() * m) / m
|
|
)
|
|
)
|
|
|
|
self.sigDragged.emit(self)
|
|
|
|
if ev.isFinish():
|
|
self.moving = False
|
|
self.sigPositionChangeFinished.emit(self)
|
|
self._on_drag_end(self)
|
|
|
|
# This is the final position in the drag
|
|
if ev.isFinish():
|
|
# show y-crosshair again
|
|
cursor.show_xhair()
|
|
|
|
def delete(self) -> None:
|
|
"""Remove this line from containing chart/view/scene.
|
|
|
|
"""
|
|
scene = self.scene()
|
|
if scene:
|
|
for label in self._labels:
|
|
label.delete()
|
|
|
|
# gc managed labels?
|
|
self._labels.clear()
|
|
|
|
if self._marker:
|
|
self.scene().removeItem(self._marker)
|
|
|
|
# remove from chart/cursor states
|
|
chart = self._chart
|
|
cur = chart.linked.cursor
|
|
|
|
if self in cur._hovered:
|
|
cur._hovered.remove(self)
|
|
|
|
chart.plotItem.removeItem(self)
|
|
|
|
def mouseDoubleClickEvent(
|
|
self,
|
|
ev: QtGui.QMouseEvent,
|
|
) -> None:
|
|
|
|
# TODO: enter labels edit mode
|
|
print(f'double click {ev}')
|
|
|
|
def paint(
|
|
self,
|
|
|
|
p: QtGui.QPainter,
|
|
opt: QtWidgets.QStyleOptionGraphicsItem,
|
|
w: QtWidgets.QWidget
|
|
|
|
) -> None:
|
|
'''
|
|
Core paint which we override (yet again)
|
|
from pg..
|
|
|
|
'''
|
|
p.setRenderHint(p.Antialiasing)
|
|
|
|
# these are in viewbox coords
|
|
vb_left, vb_right = self._endPoints
|
|
vb = self.getViewBox()
|
|
|
|
line_end, marker_right, r_axis_x = self._chart.marker_right_points()
|
|
|
|
if self.show_markers and self.markers:
|
|
|
|
p.setPen(self.pen)
|
|
qgo_draw_markers(
|
|
self.markers,
|
|
self.pen.color(),
|
|
p,
|
|
vb_left,
|
|
vb_right,
|
|
marker_right,
|
|
)
|
|
# marker_size = self.markers[0][2]
|
|
self._maxMarkerSize = max([m[2] / 2. for m in self.markers])
|
|
|
|
# this seems slower when moving around
|
|
# order lines.. not sure wtf is up with that.
|
|
# for now we're just using it on the position line.
|
|
elif self._marker:
|
|
|
|
# TODO: make this label update part of a scene-aware-marker
|
|
# composed annotation
|
|
self._marker.setPos(
|
|
QPointF(marker_right, self.scene_y())
|
|
)
|
|
if hasattr(self._marker, 'label'):
|
|
self._marker.label.update()
|
|
|
|
elif not self.use_marker_margin:
|
|
# basically means **don't** shorten the line with normally
|
|
# reserved space for a direction marker but, leave small
|
|
# blank gap for style
|
|
line_end = r_axis_x - 10
|
|
|
|
line_end_view = vb.mapToView(Point(line_end, 0)).x()
|
|
|
|
# self.currentPen.setJoinStyle(QtCore.Qt.MiterJoin)
|
|
p.setPen(self.currentPen)
|
|
p.drawLine(
|
|
Point(vb_left, 0),
|
|
Point(line_end_view, 0)
|
|
)
|
|
self._right_end_sc = line_end
|
|
|
|
def hide(self) -> None:
|
|
super().hide()
|
|
if self._marker:
|
|
self._marker.hide()
|
|
# needed for ``order_line()`` lines currently
|
|
self._marker.label.hide()
|
|
|
|
def show(self) -> None:
|
|
super().show()
|
|
if self._marker:
|
|
self._marker.show()
|
|
# self._marker.label.show()
|
|
|
|
def scene_y(self) -> float:
|
|
return self.getViewBox().mapFromView(
|
|
Point(0, self.value())
|
|
).y()
|
|
|
|
def scene_endpoint(self) -> QPointF:
|
|
|
|
if not self._right_end_sc:
|
|
line_end, _, _ = self._chart.marker_right_points()
|
|
self._right_end_sc = line_end - 10
|
|
|
|
return QPointF(self._right_end_sc, self.scene_y())
|
|
|
|
def add_marker(
|
|
self,
|
|
path: QtWidgets.QGraphicsPathItem,
|
|
|
|
) -> QtWidgets.QGraphicsPathItem:
|
|
|
|
self._marker = path
|
|
self._marker.setPen(self.currentPen)
|
|
self._marker.setBrush(fn.mkBrush(self.currentPen.color()))
|
|
# add path to scene
|
|
self.getViewBox().scene().addItem(path)
|
|
|
|
# place to just-left of L1 labels
|
|
rsc = self._chart.pre_l1_xs()[0]
|
|
path.setPos(QPointF(rsc, self.scene_y()))
|
|
|
|
return path
|
|
|
|
def hoverEvent(self, ev):
|
|
'''
|
|
Mouse hover callback.
|
|
|
|
'''
|
|
cur = self._chart.linked.cursor
|
|
|
|
# hovered
|
|
if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
|
|
|
|
# if already hovered we don't need to run again
|
|
if self.mouseHovering is True:
|
|
return
|
|
|
|
if self.only_show_markers_on_hover:
|
|
self.show_markers = True
|
|
|
|
if self._marker:
|
|
self._marker.show()
|
|
|
|
# highlight if so configured
|
|
if self.highlight_on_hover:
|
|
|
|
self.currentPen = self.hoverPen
|
|
|
|
if self not in cur._trackers:
|
|
# only disable cursor y-label updates
|
|
# if we're highlighting a line
|
|
cur._y_label_update = False
|
|
|
|
# add us to cursor state
|
|
cur.add_hovered(self)
|
|
|
|
if self._hide_xhair_on_hover:
|
|
cur.hide_xhair(
|
|
# set y-label to current value
|
|
y_label_level=self.value(),
|
|
just_vertical=True,
|
|
|
|
# fg_color=self._hcolor,
|
|
# bg_color=self._hcolor,
|
|
)
|
|
|
|
# if we want highlighting of labels
|
|
# it should be delegated into this method
|
|
self.show_labels()
|
|
|
|
self.mouseHovering = True
|
|
|
|
# un-hovered
|
|
else:
|
|
if self.mouseHovering is False:
|
|
return
|
|
|
|
cur._y_label_update = True
|
|
|
|
self.currentPen = self.pen
|
|
|
|
cur._hovered.remove(self)
|
|
|
|
if self.only_show_markers_on_hover:
|
|
self.show_markers = False
|
|
|
|
if self._marker:
|
|
self._marker.hide()
|
|
self._marker.label.hide()
|
|
|
|
if self not in cur._trackers:
|
|
cur.show_xhair(y_label_level=self.value())
|
|
|
|
if not self.always_show_labels:
|
|
self.hide_labels()
|
|
|
|
self.mouseHovering = False
|
|
|
|
self.update()
|
|
|
|
|
|
def level_line(
|
|
|
|
chart: 'ChartPlotWidget', # noqa
|
|
level: float,
|
|
|
|
# line style
|
|
dotted: bool = False,
|
|
color: str = 'default',
|
|
|
|
# ux
|
|
highlight_on_hover: bool = True,
|
|
|
|
# label fields and options
|
|
always_show_labels: bool = False,
|
|
add_label: bool = True,
|
|
orient_v: str = 'bottom',
|
|
**kwargs,
|
|
|
|
) -> LevelLine:
|
|
"""Convenience routine to add a styled horizontal line to a plot.
|
|
|
|
"""
|
|
hl_color = color + '_light' if highlight_on_hover else color
|
|
|
|
line = LevelLine(
|
|
chart,
|
|
color=color,
|
|
|
|
# lookup "highlight" equivalent
|
|
highlight_color=hl_color,
|
|
|
|
dotted=dotted,
|
|
|
|
# UX related options
|
|
highlight_on_hover=highlight_on_hover,
|
|
|
|
# when set to True the label is always shown instead of just on
|
|
# highlight (which is a privacy thing for orders)
|
|
always_show_labels=always_show_labels,
|
|
|
|
**kwargs,
|
|
)
|
|
|
|
chart.plotItem.addItem(line)
|
|
|
|
if add_label:
|
|
|
|
label = Label(
|
|
|
|
view=line.getViewBox(),
|
|
|
|
# by default we only display the line's level value
|
|
# in the label
|
|
fmt_str=('{level:,.{level_digits}f}'),
|
|
color=color,
|
|
)
|
|
|
|
# anchor to right side (of view ) label
|
|
label.set_x_anchor_func(
|
|
right_axis(
|
|
chart,
|
|
label,
|
|
side='left', # side of axis
|
|
offset=0,
|
|
avoid_book=False,
|
|
)
|
|
)
|
|
|
|
# add to label set which will be updated on level changes
|
|
line._labels.append(label)
|
|
|
|
label.orient_v = orient_v
|
|
line.update_labels({'level': level, 'level_digits': 2})
|
|
label.render()
|
|
|
|
# keep pp label details private until
|
|
# the user edge triggers "order mode"
|
|
line.hide_labels()
|
|
|
|
# activate/draw label
|
|
line.set_level(level)
|
|
|
|
return line
|
|
|
|
|
|
def order_line(
|
|
|
|
chart,
|
|
level: float,
|
|
action: Optional[str] = 'buy', # buy or sell
|
|
|
|
marker_style: Optional[str] = None,
|
|
level_digits: Optional[float] = 3,
|
|
size: Optional[int] = 1,
|
|
size_digits: int = 1,
|
|
show_markers: bool = False,
|
|
submit_price: float = None,
|
|
orient_v: str = 'bottom',
|
|
|
|
**line_kwargs,
|
|
|
|
) -> LevelLine:
|
|
'''
|
|
Convenience routine to add a line graphic representing an order
|
|
execution submitted to the EMS via the chart's "order mode".
|
|
|
|
'''
|
|
line = level_line(
|
|
chart,
|
|
level,
|
|
add_label=False,
|
|
use_marker_margin=True,
|
|
**line_kwargs
|
|
)
|
|
|
|
font_size = _font.font.pixelSize()
|
|
|
|
# scale marker size with dpi-aware font size
|
|
marker_size = floor(1.375 * font_size)
|
|
|
|
orient_v = 'top' if action == 'sell' else 'bottom'
|
|
|
|
if action == 'alert':
|
|
|
|
label = Label(
|
|
|
|
view=line.getViewBox(),
|
|
color=line.color,
|
|
|
|
# completely different labelling for alerts
|
|
fmt_str='alert => {level}',
|
|
)
|
|
|
|
# for now, we're just duplicating the label contents i guess..
|
|
line._labels.append(label)
|
|
|
|
# anchor to left side of view / line
|
|
label.set_x_anchor_func(vbr_left(label))
|
|
|
|
label.fields = {
|
|
'level': level,
|
|
'level_digits': level_digits,
|
|
}
|
|
|
|
marker_size = marker_size * 0.666
|
|
|
|
else:
|
|
view = line.getViewBox()
|
|
|
|
# far-side label
|
|
label = Label(
|
|
view=view,
|
|
# display the order pos size, which is some multiple
|
|
# of the user defined base unit size
|
|
fmt_str=(
|
|
'{account_text}{size:.{size_digits}f}u{fiat_text}'
|
|
),
|
|
color=line.color,
|
|
)
|
|
|
|
label.set_x_anchor_func(vbr_left(label))
|
|
|
|
line._labels.append(label)
|
|
|
|
def maybe_show_fiat_text(fields: dict) -> str:
|
|
fiat_size = fields.get('fiat_size')
|
|
if not fiat_size:
|
|
return ''
|
|
|
|
return f' ~ ${humanize(fiat_size)}'
|
|
|
|
def maybe_show_account_name(fields: dict) -> str:
|
|
account = fields.get('account')
|
|
if not account:
|
|
return ''
|
|
|
|
return f'{account}: '
|
|
|
|
label.fields = {
|
|
'size': size,
|
|
'size_digits': 0,
|
|
'fiat_size': None,
|
|
'fiat_text': maybe_show_fiat_text,
|
|
'account': None,
|
|
'account_text': maybe_show_account_name,
|
|
}
|
|
|
|
label.orient_v = orient_v
|
|
label.render()
|
|
label.show()
|
|
|
|
if show_markers:
|
|
# add arrow marker on end of line nearest y-axis
|
|
marker_style = marker_style or {
|
|
'buy': '|<',
|
|
'sell': '>|',
|
|
'alert': 'v',
|
|
}[action]
|
|
|
|
# the old way which is still somehow faster?
|
|
marker = LevelMarker(
|
|
chart=chart,
|
|
style=marker_style,
|
|
get_level=line.value,
|
|
size=marker_size,
|
|
keep_in_view=False,
|
|
)
|
|
|
|
# XXX: this is our new approach but seems slower?
|
|
marker = line.add_marker(marker)
|
|
|
|
# XXX: DON'T COMMENT THIS!
|
|
# this fixes it the artifact issue! .. of course, bounding rect stuff
|
|
line._maxMarkerSize = marker_size
|
|
|
|
assert line._marker is marker
|
|
assert not line.markers
|
|
|
|
# above we use ``QPathGraphicsItem``s directly to draw markers
|
|
# in scene coords instead of the way ``InfiniteLine`` does it
|
|
# internally: by resetting the graphics item transform
|
|
# intermittently inside ``.paint()`` which we've copied and
|
|
# seperated as ``qgo_draw_markers()`` if we ever want to go back
|
|
# to it; likely we can remove this.
|
|
|
|
# manually append for later ``InfiniteLine.paint()`` drawing
|
|
# XXX: this was manually tested as faster then using the
|
|
# QGraphicsItem around a painter path.. probably needs further
|
|
# testing to figure out why tf that's true.
|
|
# line.markers.append((marker, 0, marker_size))
|
|
|
|
if action != 'alert':
|
|
|
|
# add a partial position label if we also added a level marker
|
|
pp_size_label = Label(
|
|
view=view,
|
|
color=line.color,
|
|
|
|
# this is "static" label
|
|
# update_on_range_change=False,
|
|
fmt_str='\n'.join((
|
|
'{slots_used:.1f}x',
|
|
)),
|
|
|
|
fields={
|
|
'slots_used': 0,
|
|
},
|
|
)
|
|
pp_size_label.render()
|
|
pp_size_label.show()
|
|
|
|
line._labels.append(pp_size_label)
|
|
|
|
# TODO: pretty sure one of the reasons these "label
|
|
# updatess" are a bit "jittery" is because we aren't
|
|
# leveraging the "scene coordinates hierarchy" stuff:
|
|
# i.e. using some parent object as the coord "origin"
|
|
# which i presume would result in better pixel caching
|
|
# results? def something to dig into..
|
|
pp_size_label.scene_anchor = partial(
|
|
gpath_pin,
|
|
gpath=marker,
|
|
label=pp_size_label,
|
|
)
|
|
# XXX: without this the pp proportion label next the marker
|
|
# seems to lag? this is the same issue we had with position
|
|
# lines which we handle with ``.update_graphcis()``.
|
|
# marker._on_paint=lambda marker: pp_size_label.update()
|
|
marker._on_paint = lambda marker: pp_size_label.update()
|
|
|
|
marker.label = label
|
|
|
|
# sanity check
|
|
line.update_labels({'level': level})
|
|
|
|
return line
|
|
|
|
|
|
# TODO: should probably consider making this a more general
|
|
# purpose class method on the type?
|
|
def copy_from_order_line(
|
|
|
|
chart: 'ChartPlotWidget', # noqa
|
|
line: LevelLine
|
|
|
|
) -> LevelLine:
|
|
|
|
return order_line(
|
|
|
|
chart,
|
|
|
|
# label fields default values
|
|
level=line.value(),
|
|
marker_style=line._marker.style,
|
|
|
|
# LevelLine kwargs
|
|
color=line.color,
|
|
dotted=line._dotted,
|
|
|
|
show_markers=line.show_markers,
|
|
only_show_markers_on_hover=line.only_show_markers_on_hover,
|
|
)
|