Source code for gewel.contrib.chart

from dataclasses import dataclass
from typing import Optional, Iterable

import cairocffi as cairo
import numpy as np

import tvx
from gewel.color import ColorMap, DARK_GRAY, TRANSPARENT, BaseColor
from gewel.draw import XYDrawable, BoundedMixin, _xform_context, walk_text, TextJustification
from tvx import float_at_time


[docs]@dataclass(init=False) class TvfChartDrawable(XYDrawable, BoundedMixin): width: tvx.FloatOrTVF = 0.0 height: tvx.FloatOrTVF = 0.0 min_time: float = 0.0 max_time: float = 1.0 min_value: Optional[float] = None max_value: Optional[float] = None time_ticks: Optional[Iterable[float]] = None value_ticks: Optional[Iterable[float]] = None line_width: tvx.FloatOrTVF = 1.0 line_color: ColorMap = DARK_GRAY fill_color: ColorMap = TRANSPARENT border_width: tvx.FloatOrTVF = 1.0 border_color: BaseColor = TRANSPARENT title: Optional[str] = None steps: int = 100 def __init__( self, tvf: tvx.Tvf, x: tvx.FloatOrTVF, y: tvx.FloatOrTVF, width: tvx.FloatOrTVF, height: tvx.FloatOrTVF, min_time: float = 0.0, max_time: float = 1.0, min_value: Optional[float] = None, max_value: Optional[float] = None, time_ticks: Optional[Iterable[float]] = None, value_ticks: Optional[Iterable[float]] = None, line_width: tvx.FloatOrTVF = 1.0, line_color: ColorMap = DARK_GRAY, fill_color: ColorMap = TRANSPARENT, border_width: tvx.FloatOrTVF = 1.0, border_color: BaseColor = TRANSPARENT, title: Optional[str] = None, steps: int = 100, theta: tvx.FloatOrTVF = 0.0, z: tvx.FloatOrTVF = 0.0, alpha: tvx.FloatOrTVF = 1.0, ): super().__init__(x=x, y=y, theta=theta, z=z, alpha=alpha) self._tvf = tvf self.width = width self.height = height self.min_time = min_time self.max_time = max_time self.min_value = min_value self.max_value = max_value if time_ticks is not None: self.time_ticks = list(time_ticks) if value_ticks is not None: self.value_ticks = list(value_ticks) self.line_width = line_width self.line_color = line_color self.fill_color = fill_color self.border_width = border_width self.border_color = border_color self.title = title self.steps = steps self.centered = False
[docs] def draw(self, ctx: cairo.Context, t: float) -> None: alpha = float_at_time(self.alpha, t) if alpha == 0.0: return width = float_at_time(self.width, t) if width == 0.0: return height = float_at_time(self.height, t) if height == 0.0: return x0 = 0.0 y0 = 0.0 x1 = x0 + width y1 = y0 + height with _xform_context(ctx, self.local_xform(t)): if not self.fill_color.is_transparent(): ctx.set_line_width(float_at_time(self.border_width, t)) ctx.move_to(x0, y0) ctx.line_to(x0, y1) ctx.line_to(x1, y1) ctx.line_to(x1, y0) ctx.close_path() ctx.set_source_rgba(*self.fill_color.tuple(t, alpha_multiplier=alpha)) ctx.clip() ctx.paint() ctx.reset_clip() if not self.line_color.is_transparent(): sample_times = np.linspace(self.min_time, self.max_time, self.steps + 1) sample_values = [self._tvf(time) for time in sample_times] if self.min_value is None or self.max_value is None: m0 = min(sample_values) m1 = max(sample_values) if self.min_value is None: min_value = m0 - 0.25 * (m1 - m0) else: min_value = self.min_value if self.max_value is None: max_value = m1 + 0.25 * (m1 - m0) else: max_value = self.max_value ctx.set_line_width(float_at_time(self.border_width, t)) ctx.set_source_rgba(*self.line_color.tuple(t, alpha_multiplier=alpha)) for time, value in zip(sample_times, sample_values): point_x = x0 + width * ( (time - self.min_time) / (self.max_time - self.min_time) ) point_y = y0 + height - height * ( (value - min_value) / (max_value - min_value) ) ctx.line_to(point_x, point_y) ctx.stroke() if not self.border_color.is_transparent(): ctx.set_line_width(float_at_time(self.border_width, t)) ctx.move_to(x0, y0) ctx.line_to(x0, y1) ctx.line_to(x1, y1) ctx.line_to(x1, y0) ctx.close_path() ctx.set_source_rgba(*self.border_color.tuple(t, alpha_multiplier=alpha)) ctx.stroke() if self.time_ticks is not None: for time_tick in self.time_ticks: tick_x = x0 + width * ( (time_tick - self.min_time) / (self.max_time - self.min_time) ) ctx.move_to(tick_x, y1) ctx.line_to(tick_x, y1 + 10) ctx.stroke() tick_label = "{:.1f}".format(time_tick) lines = walk_text( ctx, tick_label, tick_x - 20, y1 + 15, 40, 12, 1.2, TextJustification.CENTER ) for line_x, line_y, line in lines: if line is not None: ctx.move_to(line_x, line_y) ctx.show_text(line) if self.value_ticks is not None: for value_tick in self.value_ticks: tick_y = y0 + height - height * ( (value_tick - min_value) / (max_value - min_value) ) ctx.move_to(x0, tick_y) ctx.line_to(x0 - 10, tick_y) ctx.stroke() tick_label = "{:.1f}".format(value_tick) lines = walk_text( ctx, tick_label, x0 - 100, tick_y - 10.5, 85, 12, 1.2, TextJustification.RIGHT ) for line_x, line_y, line in lines: if line is not None: ctx.move_to(line_x, line_y) ctx.show_text(line) if self.title is not None: title_lines = walk_text(ctx, self.title, x0, y0 - 24, width, 14, 1.2, TextJustification.CENTER) for line_x, line_y, line in title_lines: if line is not None: ctx.move_to(line_x, line_y) ctx.show_text(line)