def render( xs: np.array, ys: np.array, x_min: float, x_max: float, y_min: float, y_max: float, width: int, height: int, ) -> np.array: """ Turn a list of 2D points into a raster matrix. Returns the pixels as 2D array with 1 or 0 integer entries. Note that the row order is optimized for drawing later, so the first row corresponds to the highest line of pixels. """ assert xs.shape == ys.shape assert x_max > x_min assert y_max > y_min assert width > 0 assert height > 0 x_indices = discretize(np.array(xs), x_min, x_max, steps=width) y_indices = discretize(np.array(ys), y_min, y_max, steps=height) # Invert y direction to optimize for plotting later y_indices = (height - 1) - y_indices # Filter out of view pixels xy_indices = np.stack((x_indices, y_indices)).T xy_indices = xy_indices[(xy_indices[:, 0] >= 0) & (xy_indices[:, 0] < width) & (xy_indices[:, 1] >= 0) & (xy_indices[:, 1] < height)] xy_indices = xy_indices.T # Assemble pixel matrix pixels = np.zeros((height, width), dtype=int) pixels[xy_indices[1], xy_indices[0]] = 1 return pixels
def render_vertical_gridline(x: float, options: Options) -> np.array: """ Render the pixel matrix that only consists of a line where the `x` value is. """ pixels = _init_character_matrix(width=options.width, height=options.height) if x < options.x_min or x >= options.x_max: return pixels x_index = discretize( x=x, x_min=options.x_min, x_max=options.x_max, steps=options.width ) pixels[:, x_index] = "│" return pixels
def render_horizontal_gridline(y: float, options: Options) -> np.array: """ Render the pixel matrix that only consists of a line where the `y` value is. Because a character is higher than wide, this is rendered with "super-resolution" Unicode characters. """ pixels = _init_character_matrix(width=options.width, height=options.height) if y < options.y_min or y >= options.y_max: return pixels y_index_superresolution = (3 * options.height - 1 - discretize(x=y, x_min=options.y_min, x_max=options.y_max, steps=3 * options.height)) y_index = int(y_index_superresolution / 3) character = Y_GRIDLINE_CHARACTERS[y_index_superresolution % 3] pixels[y_index, :] = character return pixels
def test_correct_discretization_for_number(): integer = discretize(x=2.1, x_min=1, x_max=3, steps=2) assert integer == 1
def test_correct_discretization_for_array(): vector_float = np.array([0.01, 0.99, 1.01, 1.5, 1.99, 9.99]) vector_integer = discretize(x=vector_float, x_min=0, x_max=10, steps=10) assert (vector_integer == np.array([0, 0, 1, 1, 1, 9])).all()
def _render_and_measure_to_cache(self) -> None: # Break if result is already in cache if self._results_already_in_cache: return str_labels = self._find_shortest_string_representation(self.labels) if self.vertical_direction: # So this is for the y axis case lines: List[str] = [""] * self.available_space for i, label in enumerate(self.labels): str_label = str_labels[i] index = ( self.available_space - 1 - min( max( 0, discretize( label, x_min=self.x_min, x_max=self.x_max, steps=self.available_space, ), ), self.available_space - 1, ) ) if lines[index] != "": # This is bad and leads to wrong offsets self._render_does_overlap = True lines[index] = str_label + self.unit self._rendered_result = lines else: # So this is for the x axis case line = "" for i, label in enumerate(self.labels): str_label = str_labels[i] offset = max( 0, discretize( label, x_min=self.x_min, x_max=self.x_max, steps=self.available_space, ) - int(0.5 * (len(str_label) + len(self.unit))) + LEFT_MARGIN_FOR_HORIZONTAL_AXIS, ) buffer = offset - len(line) if i == 0 and buffer < 0: # This is bad and leads to wrong offsets buffer = 0 self._render_does_overlap = True elif i > 0 and buffer < 1: # This is bad and leads to wrong offsets buffer = 1 self._render_does_overlap = True # Compose string for this line line = line + (" " * buffer) + str_label + self.unit self._rendered_result = [line] self._results_already_in_cache = True
def render( xs: np.array, ys: np.array, x_min: float, x_max: float, y_min: float, y_max: float, width: int, height: int, lines: bool = False, ) -> np.array: """ Turn a list of 2D points into a raster matrix. Returns the pixels as 2D array with 1 or 0 integer entries. Note that the row order is optimized for drawing later, so the first row corresponds to the highest line of pixels. """ xs = np.array(xs) ys = np.array(ys) assert xs.shape == ys.shape assert x_max > x_min assert y_max > y_min assert width > 0 assert height > 0 pixels = np.zeros((height, width), dtype=int) x_indices = discretize(xs, x_min, x_max, steps=width) y_indices = discretize(ys, y_min, y_max, steps=height) # Invert y direction to optimize for plotting later y_indices = (height - 1) - y_indices # Combine lists to get coordinates xy_indices = np.column_stack((x_indices, y_indices)) if lines: xys = np.column_stack((xs, ys)) # Compute all line segments as an array with entries of the shape: # [ # [x_index_start, y_index_start], # [x_start, y_start], # [x_index_stop, y_index_stop], # [x_stop, y_stop] # ] xy_line_endpoints = np.stack( (xy_indices[:-1], xys[:-1], xy_indices[1:], xys[1:]), axis=1).astype(float) # Filter out of view line segments xy_line_endpoints = xy_line_endpoints[ ( # At least one of the x coordinates of start and end need to be >= x_min (xy_line_endpoints[:, 1, 0] >= x_min) | (xy_line_endpoints[:, 3, 0] >= x_min)) & ( # At least one of the x coordinates of start and end need to be < x_max (xy_line_endpoints[:, 1, 0] < x_max) | (xy_line_endpoints[:, 3, 0] < x_max)) & ( # At least one of the y coordinates of start and end need to be >= y_min (xy_line_endpoints[:, 1, 1] >= y_min) | (xy_line_endpoints[:, 3, 1] >= y_min)) & ( # At least one of the y coordinates of start and end need to be < x_max (xy_line_endpoints[:, 1, 1] < y_max) | (xy_line_endpoints[:, 3, 1] < y_max)) & ( # The start and stop indices need to be different by at least 2 in any # direction (np.abs(xy_line_endpoints[:, 0, 0] - xy_line_endpoints[:, 2, 0]) > 1.5) | (np.abs(xy_line_endpoints[:, 0, 1] - xy_line_endpoints[:, 2, 1]) > 1.5))] # TODO This can likely be optimized by assembling all segments and computing the # pixels of all lines together, or at least of each half split by slope # for segment in np.nditer(xy_line_endpoints): # TODO use this for segment in xy_line_endpoints: [ [x_index_start, y_index_start], [x_start, y_start], [x_index_stop, y_index_stop], [x_stop, y_stop], ] = segment # Convert back to integers (not very efficient) x_index_start = int(round(x_index_start)) x_index_stop = int(round(x_index_stop)) y_index_start = int(round(y_index_start)) y_index_stop = int(round(y_index_stop)) # For convenience x_index_smaller = min(x_index_start, x_index_stop) x_index_bigger = max(x_index_start, x_index_stop) y_index_smaller = min(y_index_start, y_index_stop) y_index_bigger = max(y_index_start, y_index_stop) # Slope is inverted because y indices are inverted indices_slope: Optional[float] = None if x_index_start != x_index_stop: indices_slope = (-1 * (y_index_stop - y_index_start) / (x_index_stop - x_index_start)) slope: Optional[float] = None if x_start != x_stop: slope = (y_stop - y_start) / (x_stop - x_start) pixels_already_drawn = False if indices_slope is None: # That means it's a vertical line step = 1 if y_index_stop < y_index_start: step = -1 pixels[y_index_start:y_index_stop:step, x_index_start] = 1 pixels_already_drawn = True elif y_index_start == y_index_stop: # That means it's a horizontal line step = 1 if x_index_stop < x_index_start: step = -1 pixels[y_index_start, x_index_start:x_index_stop:step] = 1 pixels_already_drawn = True elif abs(indices_slope) > 1: # Draw line by iterating vertically # 1. Compute y indices in the middle of bins between the two origins step = 1 if y_index_stop < y_index_start: step = -1 y_indices_of_line = np.arange(y_index_start, y_index_stop + step, step=step) ys_of_line = invert_discretize( height - 1 - y_indices_of_line, minimum=y_min, maximum=y_max, nr_bins=height, ) # 2. Compute corresponding x coordinates # Derivation: # xs_of_line - x_start = (ys_of_line - y_start) / slope # xs_of_line = (ys_of_line - y_start) / slope + x_start xs_of_line = (ys_of_line - y_start) / slope + x_start x_indices_of_line = discretize(xs_of_line, x_min=x_min, x_max=x_max, steps=width) else: # Draw line by iterating horizontically # 1. Compute x indices in the middle of bins between the two origins step = 1 if x_index_stop < x_index_start: step = -1 x_indices_of_line = np.arange(x_index_start, x_index_stop + step, step=step) xs_of_line = invert_discretize(x_indices_of_line, minimum=x_min, maximum=x_max, nr_bins=width) # 2. Compute corresponding y coordinates ys_of_line = y_start + slope * (xs_of_line - x_start) y_indices_of_line = (height - 1 - discretize( ys_of_line, x_min=y_min, x_max=y_max, steps=height)) # Finally, draw pixels (if needed) if not pixels_already_drawn: # Assemble pixels xy_indices_of_line = np.column_stack( (x_indices_of_line, y_indices_of_line)) # Filter out of view pixels xy_indices_of_line = xy_indices_of_line[ (xy_indices_of_line[:, 0] >= max(0, x_index_smaller)) & (xy_indices_of_line[:, 0] <= min(width - 1, x_index_bigger)) & (xy_indices_of_line[:, 1] >= max(0, y_index_smaller)) & (xy_indices_of_line[:, 1] <= min(height - 1, y_index_bigger))] xy_indices_of_line = xy_indices_of_line.T pixels[xy_indices_of_line[1], xy_indices_of_line[0]] = 1 # Filter out of view pixels xy_indices = xy_indices[(xy_indices[:, 0] >= 0) & (xy_indices[:, 0] < width) & (xy_indices[:, 1] >= 0) & (xy_indices[:, 1] < height)] xy_indices = xy_indices.T # Assemble pixel matrix pixels[xy_indices[1], xy_indices[0]] = 1 return pixels