Source code for storylines.fatband

# Copyright (C) 2016-2024 Jan Berges
# This program is free software under the terms of the BSD Zero Clause License.

"""Plot weights along line."""

from __future__ import division

import math

[docs] def fatband(points, width, weights, shifts, nib=None): """Represent weighted data points via varying linewidth. Parameters ---------- points : list of 2-tuple Vertices of linear spline. width : float Overall linewidth scaling factor. weights : list of float Weights of `points`. shifts : list of float Displacements in weight direction. nib : float Angle of broad pen nib. If ``None``, the nib is held perpendicular to the direction of the line. The direction is always the average of the directions of adjacent line segments. Returns ------- list of 2-tuple Fatband outline. See Also -------- miter_butt : Equivalent routine with miter line join. """ N = len(points) x, y = tuple(zip(*points)) if nib is not None: alpha = [nib] * (N - 1) else: alpha = [(math.pi / 2 + math.atan2(y[n + 1] - y[n], x[n + 1] - x[n])) % (2 * math.pi) for n in range(N - 1)] phi = alpha[:1] for n in range(N - 2): phi.append(sum(alpha[n:n + 2]) / 2) if max(alpha[n:n + 2]) - min(alpha[n:n + 2]) > math.pi: phi[-1] += math.pi phi.append(alpha[-1]) X = [] Y = [] for sgn in 1, -1: for n in range(N) if sgn == 1 else reversed(range(N)): X.append(x[n] + math.cos(phi[n]) * width * (shifts[n] + sgn * weights[n] / 2)) Y.append(y[n] + math.sin(phi[n]) * width * (shifts[n] + sgn * weights[n] / 2)) return list(zip(X, Y))
[docs] def miter_butt(points, width, weights, shifts, nib=None): """Represent weighted data points via varying linewidth. Parameters ---------- points : list of 2-tuple Vertices of linear spline. width : float Overall linewidth scaling factor. weights : list of float Weights of `points`. shifts : list of float Displacements in weight direction. nib : float Angle of broad pen nib. If ``None``, the nib is held perpendicular to the direction of the current line segment. Line segments are connected using the miter joint. Returns ------- list of 2-tuple Fatband outline. See Also -------- fatband : Equivalent routine without miter line join. """ N = len(points) x, y = tuple(zip(*points)) upper = [] lower = [] for n in range(N - 1): if nib is not None: alpha = nib else: alpha = math.atan2(y[n + 1] - y[n], x[n + 1] - x[n]) + math.pi / 2 dx = 0.5 * width * math.cos(alpha) dy = 0.5 * width * math.sin(alpha) lower.append((x[n] - dx, y[n] - dy, x[n + 1] - dx, y[n + 1] - dy)) upper.append((x[n] + dx, y[n] + dy, x[n + 1] + dx, y[n + 1] + dy)) X = [] Y = [] for segs in upper, lower: X.append([segs[0][0]]) Y.append([segs[0][1]]) for n in range(1, N - 1): x1a, y1a, x1b, y1b = segs[n - 1] x2a, y2a, x2b, y2b = segs[n] dx1 = x1b - x1a dy1 = y1b - y1a dx2 = x2b - x2a dy2 = y2b - y2a det = dy1 * dx2 - dx1 * dy2 if det: X[-1].append((x1a * dy1 * dx2 - y1a * dx1 * dx2 - x2a * dx1 * dy2 + y2a * dx1 * dx2) / det) Y[-1].append((x1a * dy1 * dy2 - y1a * dx1 * dy2 - x2a * dy1 * dy2 + y2a * dy1 * dx2) / det) else: X[-1].append(x2a) Y[-1].append(y2a) X[-1].append(segs[-1][2]) Y[-1].append(segs[-1][3]) XA = [] XB = [] YA = [] YB = [] for n in range(N): a1 = 0.5 + shifts[n] - 0.5 * weights[n] a2 = 0.5 - shifts[n] + 0.5 * weights[n] b1 = 0.5 + shifts[n] + 0.5 * weights[n] b2 = 0.5 - shifts[n] - 0.5 * weights[n] XA.append(a1 * X[0][n] + a2 * X[1][n]) XB.append(b1 * X[0][n] + b2 * X[1][n]) YA.append(a1 * Y[0][n] + a2 * Y[1][n]) YB.append(b1 * Y[0][n] + b2 * Y[1][n]) return list(zip(XA + XB[::-1], YA + YB[::-1]))