From 4ce0bdb2809723d86539f3949abfb398c74154a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Mon, 2 Dec 2019 18:46:12 +0100 Subject: [PATCH] feat: add colorbar (close #1) --- CHANGELOG | 3 +- shapeout2/gui/analysis/ana_plot.ui | 26 ++++---- shapeout2/gui/colorbar_widget.py | 96 ++++++++++++++++++++++++++++++ shapeout2/gui/pipeline_plot.py | 45 +++++++++++--- shapeout2/pipeline/plot.py | 7 ++- 5 files changed, 151 insertions(+), 26 deletions(-) create mode 100644 shapeout2/gui/colorbar_widget.py diff --git a/CHANGELOG b/CHANGELOG index e85a0b8..c3acf17 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,7 +4,8 @@ - feat: implement loading of (polygon) filters from file - feat: implement session saving and opening - feat: added perceptive colormaps and set viridis as default - - feat: allow to set hue range for feature-colored scatter plots + - feat: allow to set hue range for feature-colored scatter plots (#1) + - feat: add colorbar (#1) - fix: hide confusing "Advances Export" plot menu in developer mode - fix: only show scalar features in QuickView feature selection - fix: QuickView -> Event -> Features labels in table were drawn diff --git a/shapeout2/gui/analysis/ana_plot.ui b/shapeout2/gui/analysis/ana_plot.ui index 6b741b6..50b79d6 100644 --- a/shapeout2/gui/analysis/ana_plot.ui +++ b/shapeout2/gui/analysis/ana_plot.ui @@ -654,22 +654,22 @@ - - - - Qt::Horizontal - - - - 0 - 20 - - - - + + + + Qt::Horizontal + + + + 0 + 20 + + + + diff --git a/shapeout2/gui/colorbar_widget.py b/shapeout2/gui/colorbar_widget.py new file mode 100644 index 0000000..aae5dfa --- /dev/null +++ b/shapeout2/gui/colorbar_widget.py @@ -0,0 +1,96 @@ +import numpy as np +from PyQt5 import QtGui +import pyqtgraph as pg +from pyqtgraph.graphicsItems.GradientEditorItem import Gradients + + +class ColorBarWidget(pg.GraphicsWidget): + def __init__(self, cmap, width, height, vmin=0.0, vmax=1.0, label=""): + """Colorbar widget that can be added to a layout + + This widget can be added to a GraphicsLayout. It is designed to + work well with the Shape-Out plot layout. + + Parameters + ---------- + cmap: str + Name of the colormap + width: int + Width of the colorbar + height: + Height of the colorbar + vmin: float + Lower value of the colorbar + vmax: float + Upper value of the colorbar + label: str + Label placed next to the colorbar + + Notes + ----- + Inspired by https://gist.github.com/maedoc/b61090021d2a5161c5b9 + """ + pg.GraphicsWidget.__init__(self) + self.setSizePolicy(QtGui.QSizePolicy.Fixed, + QtGui.QSizePolicy.Preferred) + + # arguments + pcmap = pg.ColorMap(*zip(*Gradients[cmap]["ticks"])) + w = width + h = height + stops, colors = pcmap.getStops('float') + smn, spp = stops.min(), stops.ptp() + stops = (stops - stops.min())/stops.ptp() + ticks = np.r_[0.0:1.0:5j, 1.0] * spp + smn + tick_labels = ["%0.2g" % (t,) for t in np.linspace(vmin, vmax, 5)] + + # setup picture + self.pic = pg.QtGui.QPicture() + p = pg.QtGui.QPainter(self.pic) + + # draw bar with gradient following colormap + p.setPen(pg.mkPen('k')) + grad = pg.QtGui.QLinearGradient(w/2.0, 0.0, w/2.0, h*1.0) + for stop, color in zip(stops, colors): + grad.setColorAt(1.0 - stop, pg.QtGui.QColor(*[c for c in color])) + p.setBrush(pg.QtGui.QBrush(grad)) + p.drawRect(pg.QtCore.QRectF(0, 0, w, h)) + + # draw ticks & tick labels + mintx = 0.0 + maxwidth = 0.0 + for tick, tick_label in zip(ticks, tick_labels): + y_ = (1.0 - (tick - smn)/spp) * h + p.drawLine(w, y_, w+5.0, y_) + br = p.boundingRect( + 0, 0, 0, 0, pg.QtCore.Qt.AlignRight, tick_label) + if br.x() < mintx: + mintx = br.x() + if br.width() > maxwidth: + maxwidth = br.width() + p.drawText(br.x() + 10.0 + w + br.width(), + y_ + br.height() / 4.0, tick_label) + + # draw label + br = p.boundingRect(0, 0, 0, 0, pg.QtCore.Qt.AlignBottom, label) + p.rotate(90) + p.drawText(h/2 - br.width()/2, -w-maxwidth-15, label) + + # done + p.end() + + # set minimum sizes (how do you get the actual bounding rect?) + br = self.pic.boundingRect() + self.setMinimumWidth(br.width() + maxwidth + 20) + self.setMinimumHeight(h) + + # alognment with other Shape-Out plots (kind of a workaround) + self.translate(0, 40) + + def paint(self, p, *args): + # paint underlying mask + p.setPen(pg.QtGui.QColor(255, 255, 255, 0)) + p.setBrush(pg.QtGui.QColor(255, 255, 255, 200)) + + # paint colorbar + p.drawPicture(0, 0, self.pic) diff --git a/shapeout2/gui/pipeline_plot.py b/shapeout2/gui/pipeline_plot.py index 3e7551a..32f7f4f 100644 --- a/shapeout2/gui/pipeline_plot.py +++ b/shapeout2/gui/pipeline_plot.py @@ -10,6 +10,7 @@ from ..pipeline import Plot from .. import plot_cache +from .colorbar_widget import ColorBarWidget from .simple_plot_widget import SimplePlotItem @@ -50,13 +51,11 @@ def update_content(self): # font size for plot title (default size + 2) size = "{}pt".format(QtGui.QFont().pointSize() + 2) - self.plot_layout.addLabel(lay["name"], colspan=2, size=size) + self.plot_layout.addLabel(lay["name"], colspan=3, size=size) self.plot_layout.nextRow() self.plot_layout.addLabel(labely, angle=-90) linner = self.plot_layout.addLayout() - self.plot_layout.nextRow() - self.plot_layout.addLabel(labelx, col=1) # limits in case of scatter plot and feature hue if lay["division"] == "merge": @@ -107,6 +106,34 @@ def update_content(self): colspan=1) pp.redraw(dslist, slot_states, plot_state_contour) + sca = plot_state["scatter"] + colorbar_kwds = {} + + if sca["marker hue"] == "kde": + colorbar_kwds["vmin"] = 0 + colorbar_kwds["vmax"] = 1 + colorbar_kwds["label"] = "density" + elif sca["marker hue"] == "feature": + colorbar_kwds["vmin"] = sca["hue min"] + colorbar_kwds["vmax"] = sca["hue max"] + feat = sca["hue feature"] + colorbar_kwds["label"] = dclab.dfn.feature_name2label[feat] + + if colorbar_kwds: + # add colorbar + sca = plot_state["scatter"] + colorbar = ColorBarWidget( + cmap=sca["colormap"], + width=15, + height=min(300, lay["size y"]//2), + **colorbar_kwds + ) + self.plot_layout.addItem(colorbar) + + self.plot_layout.nextRow() + self.plot_layout.addLabel(labelx, col=1) + self.update() + class PipelinePlotItem(SimplePlotItem): def __init__(self, *args, **kwargs): @@ -329,14 +356,14 @@ def add_scatter(plot_item, plot_state, rtdc_ds, slot_state): brush.append(cmap.mapToQColor(f)) elif sca["marker hue"] == "dataset": alpha = int(sca["marker alpha"] * 255) - color = pg.mkColor(slot_state["color"]) - color.setAlpha(alpha) - brush = pg.mkBrush(color) + colord = pg.mkColor(slot_state["color"]) + colord.setAlpha(alpha) + brush = pg.mkBrush(colord) else: alpha = int(sca["marker alpha"] * 255) - color = pg.mkColor("k") - color.setAlpha(alpha) - brush = pg.mkBrush(color) + colork = pg.mkColor("#000000") + colork.setAlpha(alpha) + brush = pg.mkBrush(colork) # convert to log-scale if applicable if gen["scale x"] == "log": diff --git a/shapeout2/pipeline/plot.py b/shapeout2/pipeline/plot.py index b7614ea..c564159 100644 --- a/shapeout2/pipeline/plot.py +++ b/shapeout2/pipeline/plot.py @@ -7,11 +7,11 @@ DEFAULT_STATE = { "identifier": "no default", "layout": { - "column count": 3, + "column count": 2, "division": "multiscatter+contour", "label plots": True, "name": "no default", # overridden by __init__ - "size x": 500, + "size x": 600, "size y": 400, }, "general": { @@ -72,7 +72,8 @@ "scale y": ["linear", "log"], }, "scatter": { - "colormap": ["inferno", "jet", "magma", "plasma", "viridis"], + "colormap": ["flame", "inferno", "magma", "plasma", "thermal", + "viridis"], "downsampling": bool, "downsampling value": int, "enabled": bool,