Skip to content

Commit

Permalink
Merge pull request #4015 from t20100/line-dash
Browse files Browse the repository at this point in the history
silx.gui.plot.PlotWidget: Improved line dash rendering for OpenGL backend
  • Loading branch information
t20100 authored Dec 19, 2023
2 parents 3c55453 + 4d3c520 commit 68051a9
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 74 deletions.
43 changes: 31 additions & 12 deletions src/silx/gui/plot/backends/BackendOpenGL.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@

class _ShapeItem(dict):
def __init__(
self, x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor
self, x, y, shape, color, fill, overlay, linewidth, dashpattern, gapcolor
):
super(_ShapeItem, self).__init__()

Expand All @@ -84,8 +84,8 @@ def __init__(
"fill": "hatch" if fill else None,
"x": x,
"y": y,
"linestyle": linestyle,
"linewidth": linewidth,
"dashpattern": dashpattern,
"gapcolor": gapcolor,
}
)
Expand All @@ -99,8 +99,8 @@ def __init__(
text,
color,
symbol,
linestyle,
linewidth,
dashpattern,
constraint,
yaxis,
font,
Expand All @@ -124,8 +124,8 @@ def __init__(
"color": colors.rgba(color),
"constraint": constraint if isConstraint else None,
"symbol": symbol,
"linestyle": linestyle,
"linewidth": linewidth,
"dashpattern": dashpattern,
"yaxis": yaxis,
"font": font,
"bgcolor": bgcolor,
Expand Down Expand Up @@ -575,7 +575,7 @@ def _renderItems(self, overlay=False):
)

# Draw the stroke
if item["linestyle"] not in ("", " ", None):
if item["dashpattern"] is not None:
if item["shape"] != "polylines":
# close the polyline
points = numpy.append(
Expand All @@ -585,10 +585,10 @@ def _renderItems(self, overlay=False):
lines = glutils.GLLines2D(
points[:, 0],
points[:, 1],
style=item["linestyle"],
color=item["color"],
gapColor=item["gapcolor"],
width=item["linewidth"],
dashPattern=item["dashpattern"],
)
context.matrix = self.matScreenProj
lines.render(context)
Expand Down Expand Up @@ -636,9 +636,9 @@ def _renderItems(self, overlay=False):
lines = glutils.GLLines2D(
(0, width),
(pixelPos[1], pixelPos[1]),
style=item["linestyle"],
color=color,
width=item["linewidth"],
dashPattern=item["dashpattern"],
)
context.matrix = self.matScreenProj
lines.render(context)
Expand Down Expand Up @@ -669,9 +669,9 @@ def _renderItems(self, overlay=False):
lines = glutils.GLLines2D(
(pixelPos[0], pixelPos[0]),
(0, height),
style=item["linestyle"],
color=color,
width=item["linewidth"],
dashPattern=item["dashpattern"],
)
context.matrix = self.matScreenProj
lines.render(context)
Expand Down Expand Up @@ -859,6 +859,22 @@ def _castArrayTo(v):
else:
raise ValueError("Unsupported data type")

_DASH_PATTERNS = { # Convert from linestyle to dash pattern
"": None,
" ": None,
"-": (),
"--": (3.7, 1.6, 3.7, 1.6),
"-.": (6.4, 1.6, 1, 1.6),
":": (1, 1.65, 1, 1.65),
None: None,
}

def _lineStyleToDashPattern(
self, style: str | None
) -> tuple[float, float, float, float] | tuple[()] | None:
"""Convert a linestyle to its corresponding dash pattern"""
return self._DASH_PATTERNS[style]

def addCurve(
self,
x,
Expand Down Expand Up @@ -977,16 +993,17 @@ def addCurve(
fillColor = None
if fill is True:
fillColor = color

curve = glutils.GLPlotCurve2D(
x,
y,
colorArray,
xError=xerror,
yError=yerror,
lineStyle=linestyle,
lineColor=color,
lineGapColor=gapcolor,
lineWidth=linewidth,
lineDashPattern=self._lineStyleToDashPattern(linestyle),
marker=symbol,
markerColor=color,
markerSize=symbolsize,
Expand Down Expand Up @@ -1091,8 +1108,9 @@ def addShape(
if self._plotFrame.yAxis.isLog and y.min() <= 0.0:
raise RuntimeError("Cannot add item with Y <= 0 with Y axis log scale")

dashpattern = self._lineStyleToDashPattern(linestyle)
return _ShapeItem(
x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor
x, y, shape, color, fill, overlay, linewidth, dashpattern, gapcolor
)

def addMarker(
Expand All @@ -1110,14 +1128,15 @@ def addMarker(
bgcolor: RGBAColorType | None,
):
font = qt.QApplication.instance().font() if font is None else font
dashpattern = self._lineStyleToDashPattern(linestyle)
return _MarkerItem(
x,
y,
text,
color,
symbol,
linestyle,
linewidth,
dashpattern,
constraint,
yaxis,
font,
Expand Down Expand Up @@ -1209,7 +1228,7 @@ def __pickCurves(self, item, x, y):
qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
size = item.markerSize / 72.0 * qtDpi
offset = max(size / 2.0, offset)
if item.lineStyle is not None:
if item.lineDashPattern is not None:
# Convert line width from points to qt pixels
qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
lineWidth = item.lineWidth / 72.0 * qtDpi
Expand Down
85 changes: 23 additions & 62 deletions src/silx/gui/plot/backends/glutils/GLPlotCurve.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,8 +278,6 @@ def isInitialized(self):

# line ########################################################################

SOLID, DASHED, DASHDOT, DOTTED = "-", "--", "-.", ":"


class GLLines2D(object):
"""Object rendering curve as a polyline
Expand All @@ -288,17 +286,16 @@ class GLLines2D(object):
:param yVboData: Y coordinates VBO
:param colorVboData: VBO of colors
:param distVboData: VBO of distance along the polyline
:param str style: Line style in: '-', '--', '-.', ':'
:param List[float] color: RGBA color as 4 float in [0, 1]
:param float width: Line width
:param float dashPeriod: Period of dashes
:param List[float] dashPattern:
"unscaled" dash pattern as 4 lengths in points (dash1, gap1, dash2, gap2).
This pattern is scaled with the line width.
Set to () to draw solid lines (default), and to None to disable rendering.
:param drawMode: OpenGL drawing mode
:param List[float] offset: Translation of coordinates (ox, oy)
"""

STYLES = SOLID, DASHED, DASHDOT, DOTTED
"""Supported line styles"""

_SOLID_PROGRAM = Program(
vertexShader="""
#version 120
Expand Down Expand Up @@ -383,11 +380,10 @@ def __init__(
yVboData=None,
colorVboData=None,
distVboData=None,
style=SOLID,
color=(0.0, 0.0, 0.0, 1.0),
gapColor=None,
width=1,
dashPeriod=10.0,
dashPattern=(),
drawMode=None,
offset=(0.0, 0.0),
):
Expand Down Expand Up @@ -419,26 +415,11 @@ def __init__(
self.color = color
self.gapColor = gapColor
self.width = width
self._style = None
self.style = style
self.dashPeriod = dashPeriod
self.dashPattern = dashPattern
self.offset = offset

self._drawMode = drawMode if drawMode is not None else gl.GL_LINE_STRIP

@property
def style(self):
"""Line style (Union[str,None])"""
return self._style

@style.setter
def style(self, style):
if style in _MPL_NONES:
self._style = None
else:
assert style in self.STYLES
self._style = style

@classmethod
def init(cls):
"""OpenGL context initialization"""
Expand All @@ -449,39 +430,23 @@ def render(self, context):
:param RenderContext context:
"""
width = self.width / 72.0 * context.dpi

style = self.style
if style is None:
if self.dashPattern is None: # Nothing to display
return

elif style == SOLID:
if self.dashPattern == (): # No dash: solid line
program = self._SOLID_PROGRAM
program.use()

else: # DASHED, DASHDOT, DOTTED
else: # Dashed line defined by 4 control points
program = self._DASH_PROGRAM
program.use()

dashPeriod = self.dashPeriod * width
if self.style == DOTTED:
dash = (
0.2 * dashPeriod,
0.5 * dashPeriod,
0.7 * dashPeriod,
dashPeriod,
)
elif self.style == DASHDOT:
dash = (
0.3 * dashPeriod,
0.5 * dashPeriod,
0.6 * dashPeriod,
dashPeriod,
)
else:
dash = (0.5 * dashPeriod, dashPeriod, dashPeriod, dashPeriod)

gl.glUniform4f(program.uniforms["dash"], *dash)
# Scale pattern by width, convert from lengths in points to offsets in pixels
scale = self.width / 72.0 * context.dpi
dashOffsets = tuple(
offset * scale for offset in numpy.cumsum(self.dashPattern)
)
gl.glUniform4f(program.uniforms["dash"], *dashOffsets)

if self.gapColor is None:
# Use fully transparent color which gets discarded in shader
Expand Down Expand Up @@ -540,7 +505,7 @@ def render(self, context):
yPosAttrib, 1, gl.GL_FLOAT, False, 0, self.yVboData
)

gl.glLineWidth(width)
gl.glLineWidth(self.width / 72.0 * context.dpi)
gl.glDrawArrays(self._drawMode, 0, self.xVboData.size)

gl.glDisable(gl.GL_LINE_SMOOTH)
Expand Down Expand Up @@ -1220,11 +1185,10 @@ def __init__(
colorData=None,
xError=None,
yError=None,
lineStyle=SOLID,
lineColor=(0.0, 0.0, 0.0, 1.0),
lineGapColor=None,
lineWidth=1,
lineDashPeriod=20,
lineDashPattern=(),
marker=SQUARE,
markerColor=(0.0, 0.0, 0.0, 1.0),
markerSize=7,
Expand Down Expand Up @@ -1311,11 +1275,10 @@ def deduce_baseline(baseline):
)

self.lines = GLLines2D()
self.lines.style = lineStyle
self.lines.color = lineColor
self.lines.gapColor = lineGapColor
self.lines.width = lineWidth
self.lines.dashPeriod = lineDashPeriod
self.lines.dashPattern = lineDashPattern
self.lines.offset = self.offset

self.points = Points2D()
Expand All @@ -1336,15 +1299,13 @@ def deduce_baseline(baseline):

distVboData = _proxyProperty(("lines", "distVboData"))

lineStyle = _proxyProperty(("lines", "style"))

lineColor = _proxyProperty(("lines", "color"))

lineGapColor = _proxyProperty(("lines", "gapColor"))

lineWidth = _proxyProperty(("lines", "width"))

lineDashPeriod = _proxyProperty(("lines", "dashPeriod"))
lineDashPattern = _proxyProperty(("lines", "dashPattern"))

marker = _proxyProperty(("points", "marker"))

Expand All @@ -1362,7 +1323,7 @@ def prepare(self):
"""Rendering preparation: build indices and bounding box vertices"""
if self.xVboData is None:
xAttrib, yAttrib, cAttrib, dAttrib = None, None, None, None
if self.lineStyle in (DASHED, DASHDOT, DOTTED):
if self.lineDashPattern:
dists = distancesFromArrays(self.xData, self.yData, self._ratio)
if self.colorData is None:
xAttrib, yAttrib, dAttrib = vertexBuffer(
Expand Down Expand Up @@ -1393,7 +1354,7 @@ def render(self, context):
:param RenderContext context: Rendering information
"""
if self.lineStyle in (DASHED, DASHDOT, DOTTED):
if self.lineDashPattern:
visibleRanges = context.plotFrame.transformedDataRanges
xLimits = visibleRanges.x
yLimits = visibleRanges.y if self.yaxis == "left" else visibleRanges.y2
Expand Down Expand Up @@ -1450,7 +1411,7 @@ def pick(self, xPickMin, yPickMin, xPickMax, yPickMax):
:rtype: Union[List[int],None]
"""
if (
(self.marker is None and self.lineStyle is None)
(self.marker is None and self.lineDashPattern is None)
or self.xMin > xPickMax
or xPickMin > self.xMax
or self.yMin > yPickMax
Expand All @@ -1464,7 +1425,7 @@ def pick(self, xPickMin, yPickMin, xPickMax, yPickMax):
yPickMin = yPickMin - self.offset[1]
yPickMax = yPickMax - self.offset[1]

if self.lineStyle is not None:
if self.lineDashPattern is not None:
# Using Cohen-Sutherland algorithm for line clipping
with numpy.errstate(invalid="ignore"): # Ignore NaN comparison warnings
codes = (
Expand Down

0 comments on commit 68051a9

Please sign in to comment.