Exemple #1
0
 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)
Exemple #2
0
 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)
Exemple #3
0
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()
Exemple #4
0
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
Exemple #5
0
    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()
Exemple #6
0
    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()
Exemple #7
0
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()
Exemple #8
0
 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)
Exemple #9
0
 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)
Exemple #10
0
    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
Exemple #11
0
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
Exemple #12
0
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
Exemple #13
0
    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" />')
Exemple #14
0
    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
Exemple #15
0
    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))
Exemple #16
0
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')
Exemple #17
0
 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)
Exemple #18
0
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
Exemple #19
0
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)
Exemple #20
0
    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()))