def draw(self, svg_file, max_lines=12): if self.intensity < self.filter_ratio: return count = int((max_lines * self.intensity) + 0.5) path = None for _ in range(count): start_side = random.choice((self.left, self.top)) offset = random.random() * self.block_size if start_side == self.left: x1 = self.left x2 = self.right y1 = self.top + offset y2 = self.bottom - offset else: x1 = self.left + offset x2 = self.right - offset y1 = self.top y2 = self.bottom if path is None: path = Path(("M", x1, y1), stroke="black", stroke_width="0.3", fill="none") else: path.push("M", x1, y1) path.push("L", x2, y2) svg_file.add(path)
def draw(self, svg_file): if self.intensity < self.filter_ratio: return lines = int(max(min(30 * self.intensity, 40), 1)) y_disp = float(self.block_size) / lines start = (self.left, self.top) end = (self.left, self.top) path = Path(("M", self.left, self.top), stroke="black", stroke_width="0.3", fill="none") # Stop when we hit the bottom while start[1] < self.bottom: # Determine which edge we are on # Left if start[0] == self.left: end = (self.right, start[1] + y_disp) # right elif start[0] == self.right: end = (self.left, start[1] + y_disp) if end[1] < self.bottom: # Recalc line to terminate at bottom path.push('L', *end) start = end svg_file.add(path)
def plot(*auts, filename='plot.svg', diagonal=True, endpoints=False, display_scale=1): assert len({aut.signature for aut in auts}) == 1 from svgwrite.path import Path from svgwrite.shapes import Line, Polyline from svgwrite.container import Group dwg, canvas = new_drawing(filename, display_scale) include_markers(dwg, endpoints) draw_grid(canvas, auts[0].signature) x_axis = Polyline([(0, 0), (SCALE, 0)], class_="axis") y_axis = Polyline([(0, 0), (0, SCALE)], class_="axis") canvas.add(x_axis) canvas.add(y_axis) if diagonal: diag = Line((0,0), (SCALE, SCALE), class_="grid depth0") canvas.add(diag) for i, aut in enumerate(auts): group = Group(class_="graph", id='graph_' + str(i)) canvas.add(group) last = (None, None) for (x0, y0, x1, y1) in graph_segments(aut): if last != (x0, y0): graph = Path(class_='graph_segment') group.add(graph) graph.push('M', SCALE * x0, SCALE * y0) graph.push('L', SCALE * x1, SCALE * y1) last = (x1, y1) dwg.save()
def arc(center): center = center arc_start = 150 arc_end = 30 radius = center[2] #p= Path(d=f"M {center[0]} {center[1]}") p = Path(d=[]) current_a = (-(radius * math.cos(math.pi * (arc_start / 180.0))) + center[0], -(radius * math.sin(math.pi * (arc_start / 180.0))) + center[1]) #p.push(f"M {(radius * math.cos(math.pi *arc_start/180.0 )) +center[0] } {(radius * math.sin(math.pi * arc_start/180.0)) +center[1] } ") #p.push(f"M 0 0 L 0 0 {center[0]} {center[1]} ") p.push(f"M {current_a[0]} {current_a[1]} ") target = (-(radius * math.cos(math.pi * (arc_end / 180.1))) + center[0], -(radius * math.sin(math.pi * (arc_end / 180.1))) + center[1]) p.push_arc(target, rotation=0, r=radius, large_arc=False, angle_dir='-', absolute=True) #p.push(f" L {target[0]} {target[1]} ") #p.push(f"L {target[0]} {target[1]} {center[0]} {center[1]}") svg_entity = svgwrite.Drawing().path(d=p.commands, stroke="blue", stroke_width="1", fill="none") ergebnis = svg_entity._repr_svg_() return ergebnis
def save(cls, image, filename, mosaic=False): # Use debug=False everywhere to turn off SVG validation, # which turns out to be obscenely expensive in this # library. DEBUG = False svg = svgwrite.Drawing(filename=filename, style='background-color: black;', size=(("%dpx" % (2 * image.r_outer), "%dpx" % (2 * image.r_outer))), debug=DEBUG) group = Group(debug=DEBUG) group.translate(image.r_outer, image.r_outer) for y, row in enumerate(image.pixels): ring = image.rings[y] theta = 2 * math.pi / len(row) r1 = ring.center + image.r_ring / 2 r2 = ring.center - image.r_ring / 2 for x, c in enumerate(row): if mosaic: path = Path(stroke='black', stroke_width=1, fill=cls.color_hex(c), debug=DEBUG) path.push((('M', 0, r2), ('L', 0, r1), ('A', r1, r1, 0, '0,0', (r1 * sin(theta), r1 * cos(theta))), ('L', r2 * sin(theta), r2 * cos(theta)), ('A', r2, r2, 0, '0,1', (0, r2)))) else: path = Path(stroke=cls.color_hex(c), stroke_width=image.r_pixel, fill='none', debug=DEBUG) path.push((('M', 0, ring.center), ('A', ring.center, ring.center, 0, '0,0', (ring.center * sin(theta), ring.center * cos(theta))))) path.rotate(180 - degrees(theta * (x + 1)), center=(0, 0)) group.add(path) svg.add(group) svg.save()
def save(cls, image, filename, mosaic=False): # Use debug=False everywhere to turn off SVG validation, # which turns out to be obscenely expensive in this # library. DEBUG = False svg = svgwrite.Drawing(filename=filename, style='background-color: black;', size=(("%dpx" % (2 * image.r_outer), "%dpx" % (2 * image.r_outer))), debug=DEBUG) group = Group(debug=DEBUG) group.translate(image.r_outer, image.r_outer) for y, row in enumerate(image.pixels): ring = image.rings[y] theta = 2 * math.pi / len(row) r1 = ring.center + image.r_ring / 2 r2 = ring.center - image.r_ring / 2 for x, c in enumerate(row): if mosaic: path = Path(stroke='black', stroke_width=1, fill=cls.color_hex(c), debug=DEBUG) path.push( (('M', 0, r2), ('L', 0, r1), ('A', r1, r1, 0, '0,0', (r1 * sin(theta), r1 * cos(theta))), ('L', r2 * sin(theta), r2 * cos(theta)), ('A', r2, r2, 0, '0,1', (0, r2)))) else: path = Path(stroke=cls.color_hex(c), stroke_width=image.r_pixel, fill='none', debug=DEBUG) path.push((('M', 0, ring.center), ('A', ring.center, ring.center, 0, '0,0', (ring.center * sin(theta), ring.center * cos(theta))))) path.rotate(180 - degrees(theta * (x + 1)), center=(0, 0)) group.add(path) svg.add(group) svg.save()
class SVG(object): ''' SVG ''' def __init__(self, id, WIDTH, HEIGHT): self.id = id self.WIDTH = WIDTH self.HEIGHT = HEIGHT x = random.randint(0, self.WIDTH) y = random.randint(0, self.HEIGHT) self.path = Path(d=('M', x, y)) self.elements = [] def addElement(self, newELement): self.elements.append(newELement) self.path.push(newELement) def getElements(self): return self.elements def getPointOfLastElement(self): point = [] #print(self.elements[-1]) point.append(self.elements[-1][-2]) point.append(self.elements[-1][-1]) return point def getPreviousPoints(self): points = [] for i in range(len(self.elements)): points.append([self.elements[i][-2], self.elements[i][-1]]) print("previous points: " + str(points)) return points def saveToFile(self): OUTPUT_DIR = "output" #Check if folder exists, if not then it will be created. path = "." + os.sep + OUTPUT_DIR + os.sep try: os.makedirs(path) except OSError: if os.path.exists(path): # We are nearly safe pass else: # There was an error on creation, so make sure we know about it raise dwg = svgwrite.Drawing(path + str(self.id) + '.svg', profile='tiny', size=(self.WIDTH, self.HEIGHT)) dwg.add(self.path) #print(dwg.tostring()) dwg.save()
def draw(self, svg_file, max_waves=20): if self.intensity < self.filter_ratio: return waves = int(max(min(max_waves * self.intensity, max_waves*1.5), 1)) peak_offset = self.block_size * 0.8 step_width = (self.block_size / (waves * 4.0)) path = Path(("M", self.left, self.mid_y), stroke="black", stroke_width="0.3", fill="none") for _ in range(waves): path.push("q", step_width, -peak_offset, 2 * step_width, 0) path.push("q", step_width, peak_offset, 2 * step_width, 0) svg_file.add(path)
def draw(self, svg_file): if self.intensity < self.filter_ratio: return segments = int(max(min(20 * self.intensity, 30), 1)) def rand_point(): return random.uniform(self.left, self.right), random.uniform(self.top, self.bottom) start = rand_point() path = Path(("M",) + start, stroke="black", stroke_width="0.3", fill="none") for _ in range(segments): path.push("T", *rand_point()) svg_file.add(path)
def get_svg(self, unit=mm): ''' Generate an SVG Drawing based of the generated gear profile. :param unit: None or a unit within the 'svgwrite' module, such as svgwrite.mm, svgwrite.cm :return: An svgwrite.Drawing object populated only with the gear path. ''' points = self.get_point_list() width, height = np.ptp(points, axis=0) left, top = np.min(points, axis=0) size = (width * unit, height * unit) if unit is not None else (width, height) dwg = Drawing(size=size, viewBox='{} {} {} {}'.format(left, top, width, height)) p = Path('M') p.push(points) p.push('Z') dwg.add(p) return dwg
def draw_slice(center, radius, start_angle, stop_angle, **kwargs): p_a = Path(**kwargs) angle = math.radians(stop_angle - start_angle) / 2.0 p_a.push(f"""M {center[0]} {center[1]} l {cos(-1*angle)*radius} {sin(-1*angle)*radius}""") p_a.push_arc( target=(cos(angle) * radius + center[0], sin(angle) * radius + center[1]), rotation=0, r=radius, large_arc=True if stop_angle - start_angle > 180 else False, angle_dir="+", absolute=True, ) p_a.push("Z") p_a.rotate( angle=(min([start_angle, stop_angle]) + (stop_angle - start_angle) / 2.0), center=center, ) return p_a
def trans_arc(dxf_entity): radius= dxf_entity.dxf.radius #stroke = dxf_entity.dxf.color center = slice_l2(dxf_entity.dxf.center) arc_start = dxf_entity.dxf.start_angle arc_end = dxf_entity.dxf.end_angle p=Path(d=[]) radius = dxf_entity.dxf.radius current_a = (-(radius * math.cos(math.pi * (arc_start/180.0 ))) +center[0], -(radius * math.sin(math.pi * (arc_start/180.0))) +center[1]) #p.push(f"M {current_a[0]} {current_a[1]} L {center[0]} {center[1]} {current_a[0]} {current_a[1]} ") p.push(f"M {current_a[0]} {current_a[1]} ") target=( -(radius * math.cos(math.pi * (arc_end/180.1) )) +center[0], -(radius * math.sin(math.pi * (arc_end/180.1)))+center[1] ) p.push_arc(target, rotation=0, r=radius, large_arc=False , angle_dir='-', absolute=True) #p.push(f" L {target[0]} {target[1]} ") #p.push(f"L {target[0]} {target[1]} {center[0]} {center[1]}") #print(f"trans_arc: dxf_entity.dxf.start_angle= {dxf_entity.dxf.start_angle} dxf_entity.dxf.end_angle={dxf_entity.dxf.end_angle} circle_center={circle_center},dxf_entity.dxf.center= {dxf_entity.dxf.center}") #print(f"trans_arc: dxf_entity.dxf.radius= {dxf_entity.dxf.radius}") #svg_entity = svgwrite.Drawing().circle(center=circle_center, r=0, stroke =stroke , fill="none", stroke_width = thickness)# !!! #svg_entity = svgwrite.Drawing().arc(center=circle_center, r=circle_radius, stroke =stroke , fill="none", stroke_width = thickness) svg_entity = svgwrite.Drawing().path(d=p.commands,stroke=stroke, stroke_width=thickness ,fill="none") # ->src/python/svgwrite/svgwrite/path.py print(f"p.commands= {svg_entity._repr_svg_()}") #svg_entity = svgwrite.Drawing().arc(center=circle_center, r=circle_radius, stroke =stroke , fill="none", stroke_width = thickness) svg_entity.scale(SCALE,-SCALE) return svg_entity
def test_flat_commands(self): p = Path(d="M 0 0") self.assertEqual(p.tostring(), '<path d="M 0 0" />') # push separated commands and values p.push(100, 100, 100, 200) self.assertEqual(p.tostring(), '<path d="M 0 0 100 100 100 200" />') # push commands strings p = Path() p.push('M 100 100 100 200') self.assertEqual(p.tostring(), '<path d="M 100 100 100 200" />') p = Path(d=('M 10', 7)) p.push('l', 100., 100.) p.push('v 100.7 200.1') self.assertEqual(p.tostring(), '<path d="M 10 7 l 100.0 100.0 v 100.7 200.1" />')
def render(self, canvas, level, x=0, y=0, origin=None, height=None, side=1, init=True, level_index=0): """ Render node set to canvas :param canvas: SVG object :param list level: List of nodes to render :param int x: X coordinate of top left of level block :param int y: Y coordinate of top left of level block :param tuple origin: Coordinates to draw 'connecting' line to :param float height: Block height budget :param int side: What direction to move into: 1 for rightwards, -1 for leftwards :param bool init: Whether the draw the top level of nodes. Only has an effect if side == self.SIDE_LEFT :return: Updated canvas """ if not level: return canvas # this eliminates a small misalignment where the left side of the # graph starts slightly too far to the left if init and side == self.SIDE_LEFT: x += self.step # determine how many nodes we'll need to fit on top of each other # within this block required_space_level = sum([self.max_breadth(node) for node in level]) # draw each node and the tree below it for node in level: # determine how high this block will be based on the available # height and the nodes we'll need to fit in it required_space_node = self.max_breadth(node) block_height = (required_space_node / required_space_level) * height # determine how much we want to enlarge the text occurrence_ratio = node.occurrences / self.max_occurrences[ level_index] if occurrence_ratio >= 0.75: embiggen = 3 elif occurrence_ratio > 0.5: embiggen = 2 elif occurrence_ratio > 0.25: embiggen = 1.75 elif occurrence_ratio > 0.15: embiggen = 1.5 else: embiggen = 1 # determine how large the text block will be (this is why we use a # monospace font) characters = len(node.name) text_width = characters * self.step text_width *= (embiggen * 1) text_offset_y = self.fontsize if self.align == "top" else ( (block_height) / 2) # determine where in the block to draw the text and where on the # canvas the block appears block_position = (x, y) block_offset_x = -(text_width + self.step) if side == self.SIDE_LEFT else 0 self.x_min = min(self.x_min, block_position[0] + block_offset_x) self.x_max = max(self.x_max, block_position[0] + block_offset_x + text_width) # the first node on the left side of the graph does not need to be # drawn if the right side is also being drawn because in that case # it's already going to be included through that part of the graph if not (init and side == self.SIDE_LEFT): container = SVG(x=block_position[0] + block_offset_x, y=block_position[1], width=text_width, height=block_height, overflow="visible") container.add( Text(text=node.name, insert=(0, text_offset_y), alignment_baseline="middle", style="font-size:" + str(embiggen) + "em")) canvas.add(container) else: # adjust position to make left side connect to right side x += text_width block_position = (block_position[0] + text_width, block_position[1]) # draw the line connecting this node to the parent node if origin: destination = (x - self.step, y + text_offset_y) # for the left side of the graph, draw a curve leftwards # instead of rightwards if side == self.SIDE_RIGHT: bezier_origin = origin bezier_destination = destination else: bezier_origin = (destination[0] + self.step, destination[1]) bezier_destination = (origin[0] - self.step, origin[1]) # bezier curve control points control_x = bezier_destination[0] - ( (bezier_destination[0] - bezier_origin[0]) / 2) control_left = (control_x, bezier_origin[1]) control_right = (control_x, bezier_destination[1]) # draw curve flow = Path(stroke="#000", fill_opacity=0, stroke_width=1.5) flow.push("M %f %f" % bezier_origin) flow.push("C %f %f %f %f %f %f" % tuple( [*control_left, *control_right, *bezier_destination])) canvas.add(flow) # bezier curves for the next set of nodes will start at these # coordinates new_origin = (block_position[0] + ((text_width + self.step) * side), block_position[1] + text_offset_y) # draw this node's children canvas = self.render(canvas, node.children, x=x + ((text_width + self.gap) * side), y=y, origin=new_origin, height=int(block_height), side=side, init=False, level_index=level_index + 1) y += block_height return canvas
def process(self): graphs = {} intervals = [] smooth = self.parameters.get("smooth") normalise_values = self.parameters.get("normalise") completeness = convert_to_int(self.parameters.get("complete"), 0) graph_label = self.parameters.get("label") top = convert_to_int(self.parameters.get("top"), 10) # first gather graph data: each distinct item gets its own graph and # for each graph we have a sequence of intervals, each interval with # its own value first_date = "9999-99-99" last_date = "0000-00-00" for row in self.iterate_items(self.source_file): if row["item"] not in graphs: graphs[row["item"]] = {} # make sure the months and days are zero-padded interval = row.get("date", "") interval = "-".join([ str(bit).zfill(2 if len(bit) != 4 else 4) for bit in interval.split("-") ]) first_date = min(first_date, interval) last_date = max(last_date, interval) if interval not in intervals: intervals.append(interval) if interval not in graphs[row["item"]]: graphs[row["item"]][interval] = 0 graphs[row["item"]][interval] += float(row.get("value", 0)) # first make sure we actually have something to render intervals = sorted(intervals) if len(intervals) <= 1: self.dataset.update_status( "Not enough data for a side-by-side over-time visualisation.") self.dataset.finish(0) return # only retain most-occurring series - sort by sum of all frequencies if len(graphs) > top: selected_graphs = { graph: graphs[graph] for graph in sorted( graphs, key=lambda x: sum( [graphs[x][interval] for interval in graphs[x]]), reverse=True)[0:top] } graphs = selected_graphs # there may be items that do not have values for all intervals # this will distort the graph, so the next step is to make sure all # graphs consist of the same continuous interval list missing = {graph: 0 for graph in graphs} for graph in graphs: missing[graph], graphs[graph] = pad_interval( graphs[graph], first_interval=first_date, last_interval=last_date) # now that's done, make sure the graph datapoints are in order intervals = sorted(list(graphs[list(graphs)[0]].keys())) # delete graphs that do not have the required amount of intervals # this is useful to get rid of outliers and items that only occur # very few times over the full interval if completeness > 0: intervals_required = len(intervals) * (completeness / 100) disqualified = [] for graph in graphs: if len(intervals) - missing[graph] < intervals_required: disqualified.append(graph) graphs = { graph: graphs[graph] for graph in graphs if graph not in disqualified } # determine max value per item, so we can normalize them later limits = {} max_limit = 0 for graph in graphs: for interval in graphs[graph]: limits[graph] = max(limits.get(graph, 0), abs(graphs[graph][interval])) max_limit = max(max_limit, abs(graphs[graph][interval])) # order graphs by highest (or lowest) value) limits = { limit: limits[limit] for limit in sorted(limits, key=lambda l: limits[l]) } graphs = {graph: graphs[graph] for graph in limits} if not graphs: # maybe nothing is actually there to be graphed self.dataset.update_status( "No items match the selection criteria - nothing to visualise." ) self.dataset.finish(0) return None # how many vertical grid lines (and labels) are to be included at most # 12 is a sensible default because it allows one label per month for a full # year's data max_gridlines = 12 # If True, label is put at the lower left bottom of the graph rather than # outside it. Automatically set to True if one of the labels is long, as # else the label would fall off the screen label_in_graph = max([len(item) for item in graphs]) > 30 # determine how wide each interval should be # the graph has a minimum width - but the graph's width will be # extended if at this minimum width each item does not have the # minimum per-item width min_full_width = 600 min_item_width = 50 item_width = max(min_item_width, min_full_width / len(intervals)) # determine how much space each graph should get # same trade-off as for the interval width min_full_height = 300 min_item_height = 100 item_height = max(min_item_height, min_full_height / len(graphs)) # margin - this should be enough for the text labels to fit in margin_base = 50 margin_right = margin_base * 4 margin_top = margin_base * 3 # this determines the "flatness" of the isometric projection and an be # tweaked for different looks - basically corresponds to how far the # camera is above the horizon plane_angle = 120 # don't change these plane_obverse = radians((180 - plane_angle) / 2) plane_angle = radians(plane_angle) # okay, now determine the full graphic size with these dimensions projected # semi-isometrically. We can also use these values later for drawing for # drawing grid lines, et cetera. The axis widths and heights here are the # dimensions of the bounding box wrapping the isometrically projected axes. x_axis_length = (item_width * (len(intervals) - 1)) y_axis_length = (item_height * len(graphs)) x_axis_width = (sin(plane_angle / 2) * x_axis_length) y_axis_width = (sin(plane_angle / 2) * y_axis_length) canvas_width = x_axis_width + y_axis_width # leave room for graph header if graph_label: margin_top += (2 * (canvas_width / 50)) x_axis_height = (cos(plane_angle / 2) * x_axis_length) y_axis_height = (cos(plane_angle / 2) * y_axis_length) canvas_height = x_axis_height + y_axis_height # now we have the dimensions, the canvas can be instantiated canvas = get_4cat_canvas( self.dataset.get_results_path(), width=(canvas_width + margin_base + margin_right), height=(canvas_height + margin_base + margin_top), header=graph_label) # draw gridlines - vertical gridline_x = y_axis_width + margin_base gridline_y = margin_top + canvas_height step_x_horizontal = sin(plane_angle / 2) * item_width step_y_horizontal = cos(plane_angle / 2) * item_width step_x_vertical = sin(plane_angle / 2) * item_height step_y_vertical = cos(plane_angle / 2) * item_height # labels for x axis # month and week both follow the same pattern # it's not always possible to distinguish between them but we will try # by looking for months greater than 12 in which case we are dealing # with weeks # we need to know this because for months there is an extra row in the # label with the full month is_week = False for i in range(0, len(intervals)): if re.match(r"^[0-9]{4}-[0-9]{2}", intervals[i]) and int(intervals[i].split("-")[1]) > 12: is_week = True break skip = max(1, int(len(intervals) / max_gridlines)) for i in range(0, len(intervals)): if i % skip == 0: canvas.add( Line(start=(gridline_x, gridline_y), end=(gridline_x - y_axis_width, gridline_y - y_axis_height), stroke="grey", stroke_width=0.25)) # to properly position the rotated and skewed text a container # element is needed label1 = str(intervals[i])[0:4] center = (gridline_x, gridline_y) container = SVG(x=center[0] - 25, y=center[1], width="50", height="1.5em", overflow="visible", style="font-size:0.8em;") container.add( Text(insert=("25%", "100%"), text=label1, transform="rotate(%f) skewX(%f)" % (-degrees(plane_obverse), degrees(plane_obverse)), text_anchor="middle", baseline_shift="-0.5em", style="font-weight:bold;")) if re.match(r"^[0-9]{4}-[0-9]{2}", intervals[i]) and not is_week: label2 = month_abbr[int(str(intervals[i])[5:7])] if re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}", intervals[i]): label2 += " %i" % int(intervals[i][8:10]) container.add( Text(insert=("25%", "150%"), text=label2, transform="rotate(%f) skewX(%f)" % (-degrees(plane_obverse), degrees(plane_obverse)), text_anchor="middle", baseline_shift="-0.5em")) canvas.add(container) gridline_x += step_x_horizontal gridline_y -= step_y_horizontal # draw graphs as filled beziers top = step_y_vertical * 1.5 graph_start_x = y_axis_width + margin_base graph_start_y = margin_top + canvas_height # draw graphs in reverse order, so the bottom one is most in the # foreground (in case of overlap) for graph in reversed(list(graphs)): self.dataset.update_status("Rendering graph for '%s'" % graph) # path starting at lower left corner of graph area_graph = Path(fill=self.colours[self.colour_index]) area_graph.push("M %f %f" % (graph_start_x, graph_start_y)) previous_value = None graph_x = graph_start_x graph_y = graph_start_y for interval in graphs[graph]: # normalise value value = graphs[graph][interval] try: limit = limits[graph] if normalise_values else max_limit value = top * copysign(abs(value) / limit, value) except ZeroDivisionError: value = 0 if previous_value is None: # vertical line upwards to starting value of graph area_graph.push("L %f %f" % (graph_start_x, graph_start_y - value)) elif not smooth: area_graph.push("L %f %f" % (graph_x, graph_y - value)) else: # quadratic bezier from previous value to current value control_left = (graph_x - (step_x_horizontal / 2), graph_y + step_y_horizontal - previous_value - (step_y_horizontal / 2)) control_right = (graph_x - (step_x_horizontal / 2), graph_y - value + (step_y_horizontal / 2)) area_graph.push("C %f %f %f %f %f %f" % (*control_left, *control_right, graph_x, graph_y - value)) previous_value = value graph_x += step_x_horizontal graph_y -= step_y_horizontal # line to the bottom of the graph at the current Y position area_graph.push( "L %f %f" % (graph_x - step_x_horizontal, graph_y + step_y_horizontal)) area_graph.push("Z") # then close the Path canvas.add(area_graph) # add text labels - skewing is a bit complicated and we need a # "center" to translate the origins properly. if label_in_graph: insert = (graph_start_x + 5, graph_start_y - 10) else: insert = (graph_x - (step_x_horizontal) + 5, graph_y + step_y_horizontal - 10) # we need to take the skewing into account for the translation offset_y = tan(plane_obverse) * insert[0] canvas.add( Text(insert=(0, 0), text=graph, transform="skewY(%f) translate(%f %f)" % (-degrees(plane_obverse), insert[0], insert[1] + offset_y))) # cycle colours, back to the beginning if all have been used self.colour_index += 1 if self.colour_index >= len(self.colours): self.colour_index = 0 graph_start_x -= step_x_vertical graph_start_y -= step_y_vertical # draw gridlines - horizontal gridline_x = margin_base gridline_y = margin_top + canvas_height - y_axis_height for graph in graphs: gridline_x += step_x_vertical gridline_y += step_y_vertical canvas.add( Line(start=(gridline_x, gridline_y), end=(gridline_x + x_axis_width, gridline_y - x_axis_height), stroke="black", stroke_width=1)) # x axis canvas.add( Line(start=(margin_base + y_axis_width, margin_top + canvas_height), end=(margin_base + canvas_width, margin_top + canvas_height - x_axis_height), stroke="black", stroke_width=2)) # and finally save the SVG canvas.save(pretty=True) self.dataset.finish(len(graphs))
def species_marker(request, genus_name='-', species_name='-'): """ Generate a SVG marker for a given species Args: request: genus_name: species_name: Returns: """ if species_name == '-': color = 'bbbbbb' species_name = '?' else: color = species_to_color(genus_name, species_name) marker_width = 60 marker_height = 100 marker_border = 5 stroke_width = 1 line_color = 'black' marker_color = '#' + color bezier_length = marker_width / 3 width = marker_width + marker_border * 2 height = marker_height + marker_border * 2 font_size = marker_height / 4 arc_centre_drop = (marker_height / 3.5 ) # Distance from top of marker to rotation centre arc_radius_vertical = arc_centre_drop image = Drawing(size=('%dpx' % width, '%dpx' % height)) marker = Path(stroke=line_color, stroke_width=stroke_width, fill=marker_color) marker.push(f'M {marker_border} {arc_centre_drop + marker_border} ' ) # Left arc edge marker.push( f'C {marker_border} {arc_centre_drop + marker_border + bezier_length} ' f'{width / 2 - bezier_length / 3} {height - marker_border - bezier_length} ' f'{width / 2} {height - marker_border}' # Point ) marker.push( f'C {width / 2 + bezier_length / 3} {height - marker_border - bezier_length} ' f'{width - marker_border} {arc_centre_drop + marker_border + bezier_length} ' f'{width - marker_border} {arc_centre_drop + marker_border} ' # Right edge ) # Right arc edge marker.push_arc(target=(marker_border, arc_centre_drop + marker_border), rotation=180, r=(marker_width / 2, arc_radius_vertical), absolute=True, angle_dir='-') marker.push('z') image.add(marker) image.add( Text(species_name, (width / 2, marker_border + arc_centre_drop + marker_height / 20), font_family='Arial', font_size=font_size, dominant_baseline="middle", text_anchor="middle")) return HttpResponse(image.tostring(), content_type='image/svg+xml')
def draw(self, svg_file, max_lines=12): if self.intensity < self.filter_ratio: return path = Path(("M", self.left, self.top), stroke="black", stroke_width="0.3", fill="none") count = int((max_lines * self.intensity)) left_third = self.left + (self.block_size / 4.0) right_third = self.left + (3 * (self.block_size / 4.0)) top_third = self.top + (self.block_size / 4.0) bottom_third = self.top + (3 * (self.block_size / 4.0)) for m in range(count): if m == 0: path.push("L", self.right, self.bottom) path.push("M", self.right, self.top) elif m == 1: path.push("L", self.left, self.bottom) path.push("M", self.mid_x, self.top) elif m == 2: path.push("L", self.mid_x, self.bottom) path.push("M", self.left, self.mid_y) elif m == 3: path.push("L", self.right, self.mid_y) path.push("M", left_third, self.top) elif m == 4: path.push("L", left_third, self.bottom) path.push("M", right_third, self.top) elif m == 5: path.push("L", right_third, self.bottom) path.push("M", self.left, top_third) elif m == 6: path.push("L", self.right, top_third) path.push("M", self.left, bottom_third) elif m == 7: path.push("L", self.right, bottom_third) path.push("M", left_third, self.top) elif m == 8: path.push("L", self.right, bottom_third) path.push("M", right_third, self.top) elif m == 9: path.push("L", self.left, bottom_third) path.push("M", left_third, self.bottom) elif m == 10: path.push("L", self.right, top_third) path.push("M", right_third, self.bottom) elif m == 11: path.push("L", self.left, top_third) svg_file.add(path)
def create_walls(walls: List[Coord], start): path = Path() path.push('M', start.x, start.y) for coords in walls: path.push('L', coords.x, coords.y) return path
class Draw(): def __init__(self, cfg, d=None, svg=None): self.cfg = cfg self.d = d self.svg = svg self.path = None self.materialPath = None self.enable = True self.reverse = False self.last = (0.0, 0.0) self.offset = 0.0 self.pScale = 25.4 * 2 self.xOffset = 50 self.yOffset = 350 self.layerIndex = 0 self.lBorder = BORDER self.lPath = PATH self.lHole = HOLE self.lText = TEXT self.lDebug = DEBUG self.lCount = 0 self.definedLayers = {} self.color = Color.WHITE.value def open(self, inFile, drawDxf=True, drawSvg=True): if drawSvg and self.svg is None: svgFile = inFile + ".svg" try: self.svg = Drawing(svgFile, profile='full', fill='black') self.path = Path(stroke_width=.5, stroke='black', fill='none') except IOError: self.svg = None self.path = None ePrint("svg file open error %s" % (svgFile)) if drawDxf and self.d is None: dxfFile = inFile + "_ngc.dxf" try: self.d = dxf.drawing(dxfFile) self.layerIndex = 0 self.d.add_layer('0', color=self.color, lineweight=0) self.setupLayers() except IOError: self.d = None ePrint("dxf file open error %s" % (dxfFile)) def nextLayer(self): self.layerIndex += 1 self.setupLayers() def setupLayers(self): i = str(self.layerIndex) self.layers = [['lBorder', i + BORDER], \ ['lPath', i + PATH], \ ['lHole', i + HOLE], \ ['lText', i + TEXT], \ ['lDebug', i + DEBUG]] for (var, l) in self.layers: self.definedLayers[l] = True self.d.add_layer(l, color=self.color, lineweight=0) exec("self." + var + "='" + l + "'") def close(self): if self.d is not None: dprt("save drawing file") self.d.save() self.d = None if self.svg is not None: self.svg.add(self.lPath) if self.materialPath is not None: self.svg.add(self.materialPath) self.svg.save() self.svg = None def scaleOffset(self, point): if self.offset == 0.0: point = ((self.xOffset + point[0]) * self.pScale, \ (self.yOffset - point[1]) * self.pScale) else: point = ((self.xOffset + point[0]) * self.pScale, \ (self.yOffset - point[1]) * self.pScale) return point def scale(self, point): point = (point[0] * self.pScale, point[1] * self.pScale) return point def material(self, xSize, ySize): if self.svg is not None: self.offset = 0.0 path = self.materialPath if path is None: self.materialPath = Path(stroke_width=.5, stroke='red', \ fill='none') path = self.materialPath path.push('M', (self.scaleOffset((0, 0)))) path.push('L', (self.scaleOffset((xSize, 0)))) path.push('L', (self.scaleOffset((xSize, ySize)))) path.push('L', (self.scaleOffset((0, ySize)))) path.push('L', (self.scaleOffset((0, 0)))) self.path.push('M', (self.scaleOffset((0, 0)))) # dwg = svgwrite.Drawing(name, (svg_size_width, svg_size_height), \ # debug=True) cfg = self.cfg if self.d is not None: orientation = cfg.orientation if orientation == O_UPPER_LEFT: p0 = (0.0, 0.0) p1 = (xSize, 0.0) p2 = (xSize, -ySize) p3 = (0.0, -ySize) elif orientation == O_LOWER_LEFT: p0 = (0.0, 0.0) p1 = (xSize, 0.0) p2 = (xSize, ySize) p3 = (0.0, ySize) elif orientation == O_UPPER_RIGHT: p0 = (0.0, 0.0) p1 = (-xSize, 0.0) p2 = (-xSize, -ySize) p3 = (0.0, -ySize) elif orientation == O_LOWER_RIGHT: p0 = (0.0, 0.0) p1 = (-xSize, 0.0) p2 = (-xSize, ySize) p3 = (0.0, ySize) elif orientation == O_CENTER: p0 = (-xSize / 2, -ySize / 2) p1 = (xSize / 2, -ySize / 2) p2 = (xSize / 2, ySize / 2) p3 = (-xSize / 2, ySize / 2) elif orientation == O_POINT: dxfInput = cfg.dxfInput p0 = (dxfInput.xMin, dxfInput.yMin) p1 = (dxfInput.xMin, dxfInput.yMax) p2 = (dxfInput.xMax, dxfInput.yMax) p3 = (dxfInput.xMax, dxfInput.yMin) else: ePrint("invalid orientation") self.d.add(dxf.line(p0, p1, layer=self.lBorder)) self.d.add(dxf.line(p1, p2, layer=self.lBorder)) self.d.add(dxf.line(p2, p3, layer=self.lBorder)) self.d.add(dxf.line(p3, p0, layer=self.lBorder)) def materialOutline(self, lines, layer=None): cfg = self.cfg if self.svg is not None: self.xOffset = 0.0 self.yOffset = cfg.dxfInput.ySize self.svg.add(Rect((0, 0), (cfg.dxfInput.xSize * self.pScale, \ cfg.dxfInput.ySize * self.pScale), \ fill='rgb(255, 255, 255)')) path = self.materialPath if path is None: self.materialPath = Path(stroke_width=.5, stroke='red', \ fill='none') path = self.materialPath for l in lines: (start, end) = l path.push('M', (self.scaleOffset(start))) path.push('L', (self.scaleOffset(end))) if self.d is not None: if layer is None: layer = self.lBorder for l in lines: (start, end) = l self.d.add(dxf.line(cfg.dxfInput.fix(start), \ cfg.dxfInput.fix(end), layer=layer)) def move(self, end): if self.enable: if self.svg is not None: self.path.push('M', self.scaleOffset(end)) # dprt("svg move %7.4f %7.4f" % self.scaleOffset(end)) # dprt(" move %7.4f %7.4f" % end) self.last = end def line(self, end, layer=None): if self.enable: if self.svg is not None: self.path.push('L', self.scaleOffset(end)) # dprt("svg line %7.4f %7.4f" % self.scaleOffset(end)) if self.d is not None: if layer is None: layer = self.lPath else: if not layer in self.definedLayers: self.definedLayers[layer] = True self.d.add_layer(layer, color=self.color, lineweight=0) self.d.add(dxf.line(self.last, end, layer=layer)) # dprt(" line %7.4f %7.4f" % end) self.last = end def arc(self, end, center, layer=None): if self.enable: r = xyDist(end, center) if self.svg is not None: self.path.push_arc(self.scaleOffset(end), 0, r, \ large_arc=True, angle_dir='+', \ absolute=True) if self.d is not None: if layer is None: layer = self.lPath else: if not layer in self.definedLayers: self.definedLayers[layer] = True self.d.add_layer(layer, color=self.color, lineweight=0) p0 = self.last p1 = end if xyDist(p0, p1) < MIN_DIST: self.d.add(dxf.circle(r, center, layer=layer)) else: # dprt("p0 (%7.4f, %7.4f) p1 (%7.4f, %7.4f)" % \ # (p0[0], p0[1], p1[0], p1[1])) # if orientation(p0, center, p1) == CCW: # (p0, p1) = (p1, p0) a0 = degrees(calcAngle(center, p0)) a1 = degrees(calcAngle(center, p1)) if a1 == 0.0: a1 = 360.0 # dprt("a0 %5.1f a1 %5.1f" % (a0, a1)) self.d.add(dxf.arc(r, center, a0, a1, layer=layer)) self.last = end def circle(self, end, r, layer=None): if self.enable: if self.d is not None: if layer is None: layer = self.lHole else: if not layer in self.definedLayers: self.definedLayers[layer] = True self.d.add_layer(layer, color=self.color, lineweight=0) self.d.add(dxf.circle(r, end, layer=layer)) self.last = end def hole(self, end, drillSize): if self.enable: if self.svg is not None: self.path.push('L', self.scaleOffset(end)) # dprt("svg line %7.4f %7.4f" % self.scaleOffset(end)) self.svg.add(Circle(self.scaleOffset(end), \ (drillSize / 2) * self.pScale, \ stroke='black', stroke_width=.5, \ fill="none")) if self.d is not None: self.d.add(dxf.line(self.last, end, layer=self.lPath)) self.d.add(dxf.circle(drillSize / 2, end, layer=self.lHole)) self.last = end def text(self, txt, p0, height, layer=None): if self.enable: if self.d is not None: if layer is None: layer = self.lText else: if not layer in self.definedLayers: self.definedLayers[layer] = True self.d.add_layer(layer, color=self.color, lineweight=0) self.d.add(dxf.text(txt, p0, height, layer=layer)) def add(self, entity): if self.enable: if self.d is not None: self.d.add(entity) def drawCross(self, p, layer=None): if layer is None: layer = self.lDebug else: if not layer in self.definedLayers: self.definedLayers[layer] = True self.d.add_layer(layer, color=self.color, lineweight=0) (x, y) = p dprt("cross %2d %7.4f, %7.4f" % (self.lCount, x, y)) labelP(p, "%d" % (self.lCount)) last = self.last self.move((x - 0.02, y)) self.line((x + 0.02, y), layer) self.move((x, y - 0.02)) self.line((x, y + 0.02), layer) self.lCount += 1 self.move(last) def drawX(self, p, txt=None, swap=False, layer=None, h=0.010): if layer is None: layer = self.lDebug else: if not layer in self.definedLayers: self.definedLayers[layer] = True self.d.add_layer(layer, color=self.color, lineweight=0) (x, y) = p xOfs = 0.020 yOfs = 0.010 if swap: (xOfs, yOfs) = (yOfs, xOfs) last = self.last self.move((x - xOfs, y - yOfs)) self.line((x + xOfs, y + yOfs), layer) self.move((x - xOfs, y + yOfs)) self.line((x + xOfs, y - yOfs), layer) self.move(p) if txt is not None: self.text('%s' % (txt), (x + xOfs, y - yOfs), h, layer) self.move(last) def drawCircle(self, p, d=0.010, layer=None, txt=None): if layer is None: layer = self.lDebug else: if not layer in self.definedLayers: self.definedLayers[layer] = True if self.d is not None: self.d.add_layer(layer, color=self.color, lineweight=0) last = self.last self.circle(p, d / 2.0, layer) if txt is not None: self.add(dxf.text(txt, p, 0.010, \ alignpoint=p, halign=CENTER, valign=MIDDLE, \ layer=layer)) self.move(last) def drawLine(self, p, m, b, x): self.move(self.offset((0, b), p)) self.move(self.offset((x, m * x + b), p)) def drawLineCircle(self, m, b, r, index): p = (index * 1, 3) self.drawLine(p, m, b, 2 * r) self.hole(offset((0, 0), p), 2 * r)
def process(self): items = {} max_weight = 1 colour_property = self.parameters.get( "colour_property", self.options["colour_property"]["default"]) size_property = self.parameters.get( "size_property", self.options["size_property"]["default"]) include_value = self.parameters.get("show_value", False) # first create a map with the ranks for each period weighted = False for row in self.iterate_items(self.source_file): if row["date"] not in items: items[row["date"]] = {} try: weight = float(row["value"]) weighted = True except (KeyError, ValueError): weight = 1 # Handle collocations a bit differently if "word_1" in row: # Trigrams if "word_3" in row: label = row["word_1"] + " " + row["word_2"] + " " + row[ "word_3"] # Bigrams else: label = row["word_1"] + " " + row["word_2"] else: label = row["item"] items[row["date"]][label] = weight max_weight = max(max_weight, weight) # determine per-period changes # this is used for determining what colour to give to nodes, and # visualise outlying items in the data changes = {} max_change = 1 max_item_length = 0 for period in items: changes[period] = {} for item in items[period]: max_item_length = max(len(item), max_item_length) now = items[period][item] then = -1 for previous_period in items: if previous_period == period: break for previous_item in items[previous_period]: if previous_item == item: then = items[previous_period][item] if then >= 0: change = abs(now - then) max_change = max(max_change, change) changes[period][item] = change else: changes[period][item] = 1 # some sizing parameters for the chart - experiment with those fontsize_normal = 12 fontsize_small = 8 box_width = fontsize_normal box_height = fontsize_normal * 1.25 # boxes will never be smaller than this box_max_height = box_height * 10 box_gap_x = max_item_length * fontsize_normal * 0.75 box_gap_y = 5 margin = 25 # don't change this - initial X value for top left box box_start_x = margin # we use this to know if and where to draw the flow curve between a box # and its previous counterpart previous_boxes = {} previous = [] # we need to store the svg elements before drawing them to the canvas # because we need to know what elements to draw before we can set the # canvas up for drawing to boxes = [] labels = [] flows = [] definitions = [] # this is the default colour for items (it's blue-ish) # we're using HSV, so we can increase the hue for more prominent items base_colour = [.55, .95, .95] max_y = 0 # go through all periods and draw boxes and flows for period in items: # reset Y coordinate, i.e. start at top box_start_y = margin for item in items[period]: # determine weight (and thereby height) of this particular item weight = items[period][item] weight_factor = weight / max_weight height = int(max(box_height, box_max_height * weight_factor) ) if size_property and weighted else box_height # colour ranges from blue to red change = changes[period][item] change_factor = 0 if not weighted or change <= 0 else ( changes[period][item] / max_change) colour = base_colour.copy() colour[0] += (1 - base_colour[0]) * ( weight_factor if colour_property == "weight" else change_factor) # first draw the box box_fill = "rgb(%i, %i, %i)" % tuple( [int(v * 255) for v in colorsys.hsv_to_rgb(*colour)]) box = Rect(insert=(box_start_x, box_start_y), size=(box_width, height), fill=box_fill) boxes.append(box) # then the text label label_y = (box_start_y + (height / 2)) + 3 label_value = "" if not include_value else ( " (%s)" % weight if weight != 1 else "") label = Text(text=(item + label_value), insert=(box_start_x + box_width + box_gap_y, label_y)) labels.append(label) # store the max y coordinate, which marks the SVG overall height max_y = max(max_y, (box["y"] + box["height"])) # then draw the flow curve, if the box was ranked in an earlier # period as well if item in previous: previous_box = previous_boxes[item] # create a gradient from the colour of the previous box for # this item to this box's colour colour_from = previous_box["fill"] colour_to = box["fill"] gradient = LinearGradient(start=(0, 0), end=(1, 0)) gradient.add_stop_color(offset="0%", color=colour_from) gradient.add_stop_color(offset="100%", color=colour_to) definitions.append(gradient) # the addition of ' none' in the auto-generated fill colour # messes up some viewers/browsers, so get rid of it gradient_key = gradient.get_paint_server().replace( " none", "") # calculate control points for the connecting bezier bar # the top_offset determines the 'steepness' of the curve, # experiment with the "/ 2" part to make it less or more # steep top_offset = (box["x"] - previous_box["x"] + previous_box["width"]) / 2 control_top_left = (previous_box["x"] + previous_box["width"] + top_offset, previous_box["y"]) control_top_right = (box["x"] - top_offset, box["y"]) bottom_offset = top_offset # mirroring looks best control_bottom_left = (previous_box["x"] + previous_box["width"] + bottom_offset, previous_box["y"] + previous_box["height"]) control_bottom_right = (box["x"] - bottom_offset, box["y"] + box["height"]) # now add the bezier curves - svgwrite has no convenience # function for beziers unfortunately. we're using cubic # beziers though quadratic could work as well since our # control points are, in principle, mirrored flow_start = (previous_box["x"] + previous_box["width"], previous_box["y"]) flow = Path(fill=gradient_key, opacity="0.35") flow.push("M %f %f" % flow_start) # go to start flow.push("C %f %f %f %f %f %f" % (*control_top_left, *control_top_right, box["x"], box["y"])) # top bezier flow.push( "L %f %f" % (box["x"], box["y"] + box["height"])) # right boundary flow.push("C %f %f %f %f %f %f" % (*control_bottom_right, *control_bottom_left, previous_box["x"] + previous_box["width"], previous_box["y"] + previous_box["height"])) # bottom bezier flow.push("L %f %f" % flow_start) # back to start flow.push("Z") # close path flows.append(flow) # mark this item as having appeared previously previous.append(item) previous_boxes[item] = box box_start_y += height + box_gap_y box_start_x += (box_gap_x + box_width) # generate SVG canvas to add elements to canvas = get_4cat_canvas(self.dataset.get_results_path(), width=(margin * 2) + (len(items) * (box_width + box_gap_x)), height=max_y + (margin * 2), fontsize_normal=fontsize_normal, fontsize_small=fontsize_small) # now add the various shapes and paths. We only do this here rather than # as we go because only at this point can the canvas be instantiated, as # before we don't know the dimensions of the SVG drawing. # add our gradients so they can be referenced for definition in definitions: canvas.defs.add(definition) # add flows (which should go beyond the boxes) for flow in flows: canvas.add(flow) # add boxes and labels: for item in (*boxes, *labels): canvas.add(item) # finally, save the svg file canvas.saveas(pretty=True, filename=str(self.dataset.get_results_path())) self.dataset.finish(len(items) * len(list(items.items()).pop()))