Skip to content

Commit 68051a9

Browse files
authored
Merge pull request #4015 from t20100/line-dash
silx.gui.plot.PlotWidget: Improved line dash rendering for OpenGL backend
2 parents 3c55453 + 4d3c520 commit 68051a9

File tree

2 files changed

+54
-74
lines changed

2 files changed

+54
-74
lines changed

src/silx/gui/plot/backends/BackendOpenGL.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858

5959
class _ShapeItem(dict):
6060
def __init__(
61-
self, x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor
61+
self, x, y, shape, color, fill, overlay, linewidth, dashpattern, gapcolor
6262
):
6363
super(_ShapeItem, self).__init__()
6464

@@ -84,8 +84,8 @@ def __init__(
8484
"fill": "hatch" if fill else None,
8585
"x": x,
8686
"y": y,
87-
"linestyle": linestyle,
8887
"linewidth": linewidth,
88+
"dashpattern": dashpattern,
8989
"gapcolor": gapcolor,
9090
}
9191
)
@@ -99,8 +99,8 @@ def __init__(
9999
text,
100100
color,
101101
symbol,
102-
linestyle,
103102
linewidth,
103+
dashpattern,
104104
constraint,
105105
yaxis,
106106
font,
@@ -124,8 +124,8 @@ def __init__(
124124
"color": colors.rgba(color),
125125
"constraint": constraint if isConstraint else None,
126126
"symbol": symbol,
127-
"linestyle": linestyle,
128127
"linewidth": linewidth,
128+
"dashpattern": dashpattern,
129129
"yaxis": yaxis,
130130
"font": font,
131131
"bgcolor": bgcolor,
@@ -575,7 +575,7 @@ def _renderItems(self, overlay=False):
575575
)
576576

577577
# Draw the stroke
578-
if item["linestyle"] not in ("", " ", None):
578+
if item["dashpattern"] is not None:
579579
if item["shape"] != "polylines":
580580
# close the polyline
581581
points = numpy.append(
@@ -585,10 +585,10 @@ def _renderItems(self, overlay=False):
585585
lines = glutils.GLLines2D(
586586
points[:, 0],
587587
points[:, 1],
588-
style=item["linestyle"],
589588
color=item["color"],
590589
gapColor=item["gapcolor"],
591590
width=item["linewidth"],
591+
dashPattern=item["dashpattern"],
592592
)
593593
context.matrix = self.matScreenProj
594594
lines.render(context)
@@ -636,9 +636,9 @@ def _renderItems(self, overlay=False):
636636
lines = glutils.GLLines2D(
637637
(0, width),
638638
(pixelPos[1], pixelPos[1]),
639-
style=item["linestyle"],
640639
color=color,
641640
width=item["linewidth"],
641+
dashPattern=item["dashpattern"],
642642
)
643643
context.matrix = self.matScreenProj
644644
lines.render(context)
@@ -669,9 +669,9 @@ def _renderItems(self, overlay=False):
669669
lines = glutils.GLLines2D(
670670
(pixelPos[0], pixelPos[0]),
671671
(0, height),
672-
style=item["linestyle"],
673672
color=color,
674673
width=item["linewidth"],
674+
dashPattern=item["dashpattern"],
675675
)
676676
context.matrix = self.matScreenProj
677677
lines.render(context)
@@ -859,6 +859,22 @@ def _castArrayTo(v):
859859
else:
860860
raise ValueError("Unsupported data type")
861861

862+
_DASH_PATTERNS = { # Convert from linestyle to dash pattern
863+
"": None,
864+
" ": None,
865+
"-": (),
866+
"--": (3.7, 1.6, 3.7, 1.6),
867+
"-.": (6.4, 1.6, 1, 1.6),
868+
":": (1, 1.65, 1, 1.65),
869+
None: None,
870+
}
871+
872+
def _lineStyleToDashPattern(
873+
self, style: str | None
874+
) -> tuple[float, float, float, float] | tuple[()] | None:
875+
"""Convert a linestyle to its corresponding dash pattern"""
876+
return self._DASH_PATTERNS[style]
877+
862878
def addCurve(
863879
self,
864880
x,
@@ -977,16 +993,17 @@ def addCurve(
977993
fillColor = None
978994
if fill is True:
979995
fillColor = color
996+
980997
curve = glutils.GLPlotCurve2D(
981998
x,
982999
y,
9831000
colorArray,
9841001
xError=xerror,
9851002
yError=yerror,
986-
lineStyle=linestyle,
9871003
lineColor=color,
9881004
lineGapColor=gapcolor,
9891005
lineWidth=linewidth,
1006+
lineDashPattern=self._lineStyleToDashPattern(linestyle),
9901007
marker=symbol,
9911008
markerColor=color,
9921009
markerSize=symbolsize,
@@ -1091,8 +1108,9 @@ def addShape(
10911108
if self._plotFrame.yAxis.isLog and y.min() <= 0.0:
10921109
raise RuntimeError("Cannot add item with Y <= 0 with Y axis log scale")
10931110

1111+
dashpattern = self._lineStyleToDashPattern(linestyle)
10941112
return _ShapeItem(
1095-
x, y, shape, color, fill, overlay, linestyle, linewidth, gapcolor
1113+
x, y, shape, color, fill, overlay, linewidth, dashpattern, gapcolor
10961114
)
10971115

10981116
def addMarker(
@@ -1110,14 +1128,15 @@ def addMarker(
11101128
bgcolor: RGBAColorType | None,
11111129
):
11121130
font = qt.QApplication.instance().font() if font is None else font
1131+
dashpattern = self._lineStyleToDashPattern(linestyle)
11131132
return _MarkerItem(
11141133
x,
11151134
y,
11161135
text,
11171136
color,
11181137
symbol,
1119-
linestyle,
11201138
linewidth,
1139+
dashpattern,
11211140
constraint,
11221141
yaxis,
11231142
font,
@@ -1209,7 +1228,7 @@ def __pickCurves(self, item, x, y):
12091228
qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
12101229
size = item.markerSize / 72.0 * qtDpi
12111230
offset = max(size / 2.0, offset)
1212-
if item.lineStyle is not None:
1231+
if item.lineDashPattern is not None:
12131232
# Convert line width from points to qt pixels
12141233
qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
12151234
lineWidth = item.lineWidth / 72.0 * qtDpi

src/silx/gui/plot/backends/glutils/GLPlotCurve.py

Lines changed: 23 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,6 @@ def isInitialized(self):
278278

279279
# line ########################################################################
280280

281-
SOLID, DASHED, DASHDOT, DOTTED = "-", "--", "-.", ":"
282-
283281

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

299-
STYLES = SOLID, DASHED, DASHDOT, DOTTED
300-
"""Supported line styles"""
301-
302299
_SOLID_PROGRAM = Program(
303300
vertexShader="""
304301
#version 120
@@ -383,11 +380,10 @@ def __init__(
383380
yVboData=None,
384381
colorVboData=None,
385382
distVboData=None,
386-
style=SOLID,
387383
color=(0.0, 0.0, 0.0, 1.0),
388384
gapColor=None,
389385
width=1,
390-
dashPeriod=10.0,
386+
dashPattern=(),
391387
drawMode=None,
392388
offset=(0.0, 0.0),
393389
):
@@ -419,26 +415,11 @@ def __init__(
419415
self.color = color
420416
self.gapColor = gapColor
421417
self.width = width
422-
self._style = None
423-
self.style = style
424-
self.dashPeriod = dashPeriod
418+
self.dashPattern = dashPattern
425419
self.offset = offset
426420

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

429-
@property
430-
def style(self):
431-
"""Line style (Union[str,None])"""
432-
return self._style
433-
434-
@style.setter
435-
def style(self, style):
436-
if style in _MPL_NONES:
437-
self._style = None
438-
else:
439-
assert style in self.STYLES
440-
self._style = style
441-
442423
@classmethod
443424
def init(cls):
444425
"""OpenGL context initialization"""
@@ -449,39 +430,23 @@ def render(self, context):
449430
450431
:param RenderContext context:
451432
"""
452-
width = self.width / 72.0 * context.dpi
453-
454-
style = self.style
455-
if style is None:
433+
if self.dashPattern is None: # Nothing to display
456434
return
457435

458-
elif style == SOLID:
436+
if self.dashPattern == (): # No dash: solid line
459437
program = self._SOLID_PROGRAM
460438
program.use()
461439

462-
else: # DASHED, DASHDOT, DOTTED
440+
else: # Dashed line defined by 4 control points
463441
program = self._DASH_PROGRAM
464442
program.use()
465443

466-
dashPeriod = self.dashPeriod * width
467-
if self.style == DOTTED:
468-
dash = (
469-
0.2 * dashPeriod,
470-
0.5 * dashPeriod,
471-
0.7 * dashPeriod,
472-
dashPeriod,
473-
)
474-
elif self.style == DASHDOT:
475-
dash = (
476-
0.3 * dashPeriod,
477-
0.5 * dashPeriod,
478-
0.6 * dashPeriod,
479-
dashPeriod,
480-
)
481-
else:
482-
dash = (0.5 * dashPeriod, dashPeriod, dashPeriod, dashPeriod)
483-
484-
gl.glUniform4f(program.uniforms["dash"], *dash)
444+
# Scale pattern by width, convert from lengths in points to offsets in pixels
445+
scale = self.width / 72.0 * context.dpi
446+
dashOffsets = tuple(
447+
offset * scale for offset in numpy.cumsum(self.dashPattern)
448+
)
449+
gl.glUniform4f(program.uniforms["dash"], *dashOffsets)
485450

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

543-
gl.glLineWidth(width)
508+
gl.glLineWidth(self.width / 72.0 * context.dpi)
544509
gl.glDrawArrays(self._drawMode, 0, self.xVboData.size)
545510

546511
gl.glDisable(gl.GL_LINE_SMOOTH)
@@ -1220,11 +1185,10 @@ def __init__(
12201185
colorData=None,
12211186
xError=None,
12221187
yError=None,
1223-
lineStyle=SOLID,
12241188
lineColor=(0.0, 0.0, 0.0, 1.0),
12251189
lineGapColor=None,
12261190
lineWidth=1,
1227-
lineDashPeriod=20,
1191+
lineDashPattern=(),
12281192
marker=SQUARE,
12291193
markerColor=(0.0, 0.0, 0.0, 1.0),
12301194
markerSize=7,
@@ -1311,11 +1275,10 @@ def deduce_baseline(baseline):
13111275
)
13121276

13131277
self.lines = GLLines2D()
1314-
self.lines.style = lineStyle
13151278
self.lines.color = lineColor
13161279
self.lines.gapColor = lineGapColor
13171280
self.lines.width = lineWidth
1318-
self.lines.dashPeriod = lineDashPeriod
1281+
self.lines.dashPattern = lineDashPattern
13191282
self.lines.offset = self.offset
13201283

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

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

1339-
lineStyle = _proxyProperty(("lines", "style"))
1340-
13411302
lineColor = _proxyProperty(("lines", "color"))
13421303

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

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

1347-
lineDashPeriod = _proxyProperty(("lines", "dashPeriod"))
1308+
lineDashPattern = _proxyProperty(("lines", "dashPattern"))
13481309

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

@@ -1362,7 +1323,7 @@ def prepare(self):
13621323
"""Rendering preparation: build indices and bounding box vertices"""
13631324
if self.xVboData is None:
13641325
xAttrib, yAttrib, cAttrib, dAttrib = None, None, None, None
1365-
if self.lineStyle in (DASHED, DASHDOT, DOTTED):
1326+
if self.lineDashPattern:
13661327
dists = distancesFromArrays(self.xData, self.yData, self._ratio)
13671328
if self.colorData is None:
13681329
xAttrib, yAttrib, dAttrib = vertexBuffer(
@@ -1393,7 +1354,7 @@ def render(self, context):
13931354
13941355
:param RenderContext context: Rendering information
13951356
"""
1396-
if self.lineStyle in (DASHED, DASHDOT, DOTTED):
1357+
if self.lineDashPattern:
13971358
visibleRanges = context.plotFrame.transformedDataRanges
13981359
xLimits = visibleRanges.x
13991360
yLimits = visibleRanges.y if self.yaxis == "left" else visibleRanges.y2
@@ -1450,7 +1411,7 @@ def pick(self, xPickMin, yPickMin, xPickMax, yPickMax):
14501411
:rtype: Union[List[int],None]
14511412
"""
14521413
if (
1453-
(self.marker is None and self.lineStyle is None)
1414+
(self.marker is None and self.lineDashPattern is None)
14541415
or self.xMin > xPickMax
14551416
or xPickMin > self.xMax
14561417
or self.yMin > yPickMax
@@ -1464,7 +1425,7 @@ def pick(self, xPickMin, yPickMin, xPickMax, yPickMax):
14641425
yPickMin = yPickMin - self.offset[1]
14651426
yPickMax = yPickMax - self.offset[1]
14661427

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

0 commit comments

Comments
 (0)