Add configurable grid line width (#265)

pull/357/head
nyanpasu64 2019-04-08 23:54:00 -07:00 zatwierdzone przez GitHub
commit 452bf29d5e
3 zmienionych plików z 120 dodań i 62 usunięć

Wyświetl plik

@ -169,6 +169,16 @@ class MainWindow(QWidget):
): ):
pass pass
with add_row(
s,
tr("Grid Line Width"),
BoundDoubleSpinBox,
name="render.grid_line_width",
minimum=0.5,
singleStep=0.5,
):
pass
with append_widget(s, QGroupBox, title=tr("Labels"), layout=QFormLayout): with append_widget(s, QGroupBox, title=tr("Labels"), layout=QFormLayout):
with add_row( with add_row(
s, tr("Font"), BoundFontButton, name="render__label_qfont" s, tr("Font"), BoundFontButton, name="render__label_qfont"

Wyświetl plik

@ -47,6 +47,7 @@ if TYPE_CHECKING:
from matplotlib.artist import Artist from matplotlib.artist import Artist
from matplotlib.axes import Axes from matplotlib.axes import Axes
from matplotlib.lines import Line2D from matplotlib.lines import Line2D
from matplotlib.spines import Spine
from matplotlib.text import Text, Annotation from matplotlib.text import Text, Annotation
from corrscope.channel import ChannelConfig from corrscope.channel import ChannelConfig
@ -114,6 +115,7 @@ class RendererConfig(DumpableAttrs, always_dump="*"):
width: int width: int
height: int height: int
line_width: float = with_units("px", default=1.5) line_width: float = with_units("px", default=1.5)
grid_line_width: float = with_units("px", default=1.0)
@property @property
def divided_width(self): def divided_width(self):
@ -230,12 +232,19 @@ class Renderer(ABC):
Point = float Point = float
PX_INCH = 96 Pixel = float
POINT_INCH = 72
# Matplotlib multiplies all widths by (inch/72 units) (AKA "matplotlib points").
# To simplify code, render output at (72 px/inch), so 1 unit = 1 px.
# For font sizes, convert from font-pt to pixels.
# (Font sizes are used far less than pixel measurements.)
PX_INCH = 72
PIXELS_PER_PT = 96 / 72
def pixels(px: float) -> Point: def px_from_points(pt: Point) -> Pixel:
return px / PX_INCH * POINT_INCH return pt * PIXELS_PER_PT
class MatplotlibRenderer(Renderer): class MatplotlibRenderer(Renderer):
@ -360,7 +369,7 @@ class MatplotlibRenderer(Renderer):
# Setup midlines (depends on max_x and wave_data) # Setup midlines (depends on max_x and wave_data)
midline_color = cfg.midline_color midline_color = cfg.midline_color
midline_width = pixels(1) midline_width = cfg.grid_line_width
# Not quite sure if midlines or gridlines draw on top # Not quite sure if midlines or gridlines draw on top
kw = dict(color=midline_color, linewidth=midline_width) kw = dict(color=midline_color, linewidth=midline_width)
@ -377,7 +386,7 @@ class MatplotlibRenderer(Renderer):
# satisfies RegionFactory # satisfies RegionFactory
def _axes_factory(self, r: RegionSpec, label: str = "") -> "Axes": def _axes_factory(self, r: RegionSpec, label: str = "") -> "Axes":
grid_color = self.cfg.grid_color cfg = self.cfg
width = 1 / r.ncol width = 1 / r.ncol
left = r.col / r.ncol left = r.col / r.ncol
@ -392,6 +401,7 @@ class MatplotlibRenderer(Renderer):
[left, bottom, width, height], xticks=[], yticks=[], label=label [left, bottom, width, height], xticks=[], yticks=[], label=label
) )
grid_color = cfg.grid_color
if grid_color: if grid_color:
# Initialize borders # Initialize borders
# Hide Axises # Hide Axises
@ -406,7 +416,8 @@ class MatplotlibRenderer(Renderer):
ax.set_facecolor(self.transparent) ax.set_facecolor(self.transparent)
# Set border colors # Set border colors
for spine in ax.spines.values(): for spine in ax.spines.values(): # type: Spine
spine.set_linewidth(cfg.grid_line_width)
spine.set_color(grid_color) spine.set_color(grid_color)
def hide(key: str): def hide(key: str):
@ -423,9 +434,9 @@ class MatplotlibRenderer(Renderer):
hide("right") hide("right")
# Dim stereo gridlines # Dim stereo gridlines
if self.cfg.stereo_grid_opacity > 0: if cfg.stereo_grid_opacity > 0:
dim_color = matplotlib.colors.to_rgba_array(grid_color)[0] dim_color = matplotlib.colors.to_rgba_array(grid_color)[0]
dim_color[-1] = self.cfg.stereo_grid_opacity dim_color[-1] = cfg.stereo_grid_opacity
def dim(key: str): def dim(key: str):
ax.spines[key].set_color(dim_color) ax.spines[key].set_color(dim_color)
@ -449,7 +460,7 @@ class MatplotlibRenderer(Renderer):
cfg = self.cfg cfg = self.cfg
# Plot lines over background # Plot lines over background
line_width = pixels(cfg.line_width) line_width = cfg.line_width
# Foreach wave, plot dummy data. # Foreach wave, plot dummy data.
lines2d = [] lines2d = []
@ -531,7 +542,7 @@ class MatplotlibRenderer(Renderer):
) )
pos_axes = (xpos.pos_axes, ypos.pos_axes) pos_axes = (xpos.pos_axes, ypos.pos_axes)
offset_pt = (pixels(xpos.offset_px), pixels(ypos.offset_px)) offset_pt = (xpos.offset_px, ypos.offset_px)
out: List["Text"] = [] out: List["Text"] = []
for label_text, ax in zip(labels, self._axes_mono): for label_text, ax in zip(labels, self._axes_mono):
@ -548,7 +559,7 @@ class MatplotlibRenderer(Renderer):
verticalalignment=ypos.align, verticalalignment=ypos.align,
# Cosmetics # Cosmetics
color=color, color=color,
fontsize=size_pt, fontsize=px_from_points(size_pt),
fontfamily=cfg.label_font.family, fontfamily=cfg.label_font.family,
fontweight=("bold" if cfg.label_font.bold else "normal"), fontweight=("bold" if cfg.label_font.bold else "normal"),
fontstyle=("italic" if cfg.label_font.italic else "normal"), fontstyle=("italic" if cfg.label_font.italic else "normal"),

Wyświetl plik

@ -1,5 +1,6 @@
from typing import Optional, TYPE_CHECKING, List from typing import Optional, TYPE_CHECKING, List
import attr
import hypothesis.strategies as hs import hypothesis.strategies as hs
import matplotlib.colors import matplotlib.colors
import numpy as np import numpy as np
@ -23,87 +24,115 @@ parametrize = pytest.mark.parametrize
WIDTH = 64 WIDTH = 64
HEIGHT = 64 HEIGHT = 64
RENDER_Y_ZEROS = np.zeros((2, 1)) RENDER_Y_ZEROS = np.full((2, 1), 0.5)
RENDER_Y_STEREO = np.zeros((2, 2)) RENDER_Y_STEREO = np.full((2, 2), 0.5)
OPACITY = 2 / 3 OPACITY = 2 / 3
def behead(string: str, header: str) -> str:
if not string.startswith(header):
raise ValueError(f"{string} does not start with {header}")
return string[len(header) :]
def appearance_to_str(val):
"""Called once for each `appear` and `data`."""
if isinstance(val, Appearance):
# Remove class name.
return behead(str(val), Appearance.__name__)
if isinstance(val, np.ndarray):
return "stereo" if val.shape[1] > 1 else "mono"
return None
@attr.dataclass
class Appearance:
bg_str: str
fg_str: str
grid_str: Optional[str]
grid_line_width: float
all_colors = pytest.mark.parametrize( all_colors = pytest.mark.parametrize(
"bg_str,fg_str,grid_str,data", "appear, data",
[ [
("#000000", "#ffffff", None, RENDER_Y_ZEROS), (Appearance("#000000", "#ffffff", None, 1), RENDER_Y_ZEROS),
("#ffffff", "#000000", None, RENDER_Y_ZEROS), (Appearance("#ffffff", "#000000", None, 2), RENDER_Y_ZEROS),
("#0000aa", "#aaaa00", None, RENDER_Y_ZEROS), (Appearance("#0000aa", "#aaaa00", None, 1), RENDER_Y_ZEROS),
("#aaaa00", "#0000aa", None, RENDER_Y_ZEROS), (Appearance("#aaaa00", "#0000aa", None, 2), RENDER_Y_ZEROS),
# Enabling ~~beautiful magenta~~ gridlines enables Axes rectangles. (Appearance("#0000aa", "#aaaa00", "#ff00ff", 1), RENDER_Y_ZEROS),
# Make sure bg is disabled, so they don't overwrite global figure background. (Appearance("#aaaa00", "#0000aa", "#ff00ff", 1), RENDER_Y_ZEROS),
("#0000aa", "#aaaa00", "#ff00ff", RENDER_Y_ZEROS), (Appearance("#aaaa00", "#0000aa", "#ff00ff", 0), RENDER_Y_ZEROS),
("#aaaa00", "#0000aa", "#ff00ff", RENDER_Y_ZEROS), (Appearance("#0000aa", "#aaaa00", "#ff00ff", 1), RENDER_Y_STEREO),
("#0000aa", "#aaaa00", "#ff00ff", RENDER_Y_STEREO), (Appearance("#aaaa00", "#0000aa", "#ff00ff", 1), RENDER_Y_STEREO),
("#aaaa00", "#0000aa", "#ff00ff", RENDER_Y_STEREO),
], ],
ids=appearance_to_str,
) )
NPLOTS = 2
def get_renderer_config(appear: Appearance) -> RendererConfig:
@all_colors
def test_default_colors(bg_str, fg_str, grid_str, data):
""" Test the default background/foreground colors. """
cfg = RendererConfig( cfg = RendererConfig(
WIDTH, WIDTH,
HEIGHT, HEIGHT,
bg_color=bg_str, bg_color=appear.bg_str,
init_line_color=fg_str, init_line_color=appear.fg_str,
grid_color=grid_str, grid_color=appear.grid_str,
grid_line_width=appear.grid_line_width,
stereo_grid_opacity=OPACITY, stereo_grid_opacity=OPACITY,
line_width=2.0, line_width=2.0,
antialiasing=False, antialiasing=False,
) )
lcfg = LayoutConfig() return cfg
NPLOTS = 2
ORIENTATION = "h"
GRID_NPIXEL = WIDTH
@all_colors
def test_default_colors(appear: Appearance, data):
""" Test the default background/foreground colors. """
cfg = get_renderer_config(appear)
lcfg = LayoutConfig(orientation=ORIENTATION)
datas = [data] * NPLOTS datas = [data] * NPLOTS
r = MatplotlibRenderer(cfg, lcfg, datas, None) r = MatplotlibRenderer(cfg, lcfg, datas, None)
verify(r, bg_str, fg_str, grid_str, datas) verify(r, appear, datas)
# Ensure default ChannelConfig(line_color=None) does not override line color # Ensure default ChannelConfig(line_color=None) does not override line color
chan = ChannelConfig(wav_path="") chan = ChannelConfig(wav_path="")
channels = [chan] * NPLOTS channels = [chan] * NPLOTS
r = MatplotlibRenderer(cfg, lcfg, datas, channels) r = MatplotlibRenderer(cfg, lcfg, datas, channels)
verify(r, bg_str, fg_str, grid_str, datas) verify(r, appear, datas)
@all_colors @all_colors
def test_line_colors(bg_str, fg_str, grid_str, data): def test_line_colors(appear: Appearance, data):
""" Test channel-specific line color overrides """ """ Test channel-specific line color overrides """
cfg = RendererConfig( cfg = get_renderer_config(appear)
WIDTH, lcfg = LayoutConfig(orientation=ORIENTATION)
HEIGHT,
bg_color=bg_str,
init_line_color="#888888",
grid_color=grid_str,
stereo_grid_opacity=OPACITY,
line_width=2.0,
antialiasing=False,
)
lcfg = LayoutConfig()
datas = [data] * NPLOTS datas = [data] * NPLOTS
chan = ChannelConfig(wav_path="", line_color=fg_str) # Move line color (appear.fg_str) from renderer cfg to individual channel.
chan = ChannelConfig(wav_path="", line_color=appear.fg_str)
channels = [chan] * NPLOTS channels = [chan] * NPLOTS
cfg.init_line_color = "#888888"
chan.line_color = appear.fg_str
r = MatplotlibRenderer(cfg, lcfg, datas, channels) r = MatplotlibRenderer(cfg, lcfg, datas, channels)
verify(r, bg_str, fg_str, grid_str, datas) verify(r, appear, datas)
TOLERANCE = 3 TOLERANCE = 3
def verify( def verify(r: MatplotlibRenderer, appear: Appearance, datas: List[np.ndarray]):
r: MatplotlibRenderer, bg_str = appear.bg_str
bg_str, fg_str = appear.fg_str
fg_str, grid_str = appear.grid_str
grid_str: Optional[str], grid_line_width = appear.grid_line_width
datas: List[np.ndarray],
):
r.update_main_lines(datas) r.update_main_lines(datas)
frame_colors: np.ndarray = np.frombuffer(r.get_frame(), dtype=np.uint8).reshape( frame_colors: np.ndarray = np.frombuffer(r.get_frame(), dtype=np.uint8).reshape(
(-1, BYTES_PER_PIXEL) (-1, BYTES_PER_PIXEL)
@ -113,15 +142,17 @@ def verify(
fg_u8 = to_rgb(fg_str) fg_u8 = to_rgb(fg_str)
all_colors = [bg_u8, fg_u8] all_colors = [bg_u8, fg_u8]
if grid_str: is_grid = bool(grid_str and grid_line_width >= 1)
if is_grid:
grid_u8 = to_rgb(grid_str) grid_u8 = to_rgb(grid_str)
all_colors.append(grid_u8) all_colors.append(grid_u8)
else: else:
grid_u8 = bg_u8 grid_u8 = np.array([1000] * BYTES_PER_PIXEL)
data = datas[0] data = datas[0]
assert (data.shape[1] > 1) == (data is RENDER_Y_STEREO) assert (data.shape[1] > 1) == (data is RENDER_Y_STEREO)
is_stereo = data.shape[1] > 1 is_stereo = is_grid and data.shape[1] > 1
if is_stereo: if is_stereo:
stereo_grid_u8 = (grid_u8 * OPACITY + bg_u8 * (1 - OPACITY)).astype(int) stereo_grid_u8 = (grid_u8 * OPACITY + bg_u8 * (1 - OPACITY)).astype(int)
all_colors.append(stereo_grid_u8) all_colors.append(stereo_grid_u8)
@ -138,8 +169,14 @@ def verify(
).any(), "incorrect foreground, it might be 136 = #888888" ).any(), "incorrect foreground, it might be 136 = #888888"
# Ensure grid color is present # Ensure grid color is present
if grid_str: does_grid_appear_here = np.prod(frame_colors == grid_u8, axis=-1)
assert np.prod(frame_colors == grid_u8, axis=-1).any(), "Missing grid_str" does_grid_appear = does_grid_appear_here.any()
assert does_grid_appear == is_grid, f"{does_grid_appear} != {is_grid}"
if is_grid:
assert np.sum(does_grid_appear_here) == pytest.approx(
GRID_NPIXEL * grid_line_width, abs=GRID_NPIXEL * 0.1
)
# Ensure stereo grid color is present # Ensure stereo grid color is present
if is_stereo: if is_stereo: