def test_add_stop_color(self): lg = LinearGradient() lg.add_stop_color(offset=0.5, color='red', opacity=1.0) self.assertEqual( lg.tostring(), '<linearGradient><stop offset="0.5" stop-color="red" stop-opacity="1.0" /></linearGradient>' )
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()))
def test_add_stop_color(self): lg = LinearGradient() lg.add_stop_color(offset=0.5, color='red', opacity=1.0) self.assertEqual(lg.tostring(), '<linearGradient><stop offset="0.5" stop-color="red" stop-opacity="1.0" /></linearGradient>')
def grid(settings={}): # Check arguments for setting_name, setting_default in DEFAULT_SETTINGS_GRID.items(): if setting_name not in settings.keys(): settings[setting_name] = setting_default # Useful calculations # Bounds defined as (x0, y0, x1, y1) sizes = (min(settings['canvas_size']) // 50, min(settings['canvas_size']) // 40) margin = 2 * sizes[0] bar_width = 15 * sizes[0] tickbox_1_bounds = (margin, margin, settings['canvas_size'][0] * 0.45, 15 * sizes[0]) tickbox_2_bounds = (settings['canvas_size'][0] * 0.55, margin, settings['canvas_size'][0] - margin, 15 * sizes[0]) divider_line_y = 22.5 * sizes[0] bar_charts_bounds = (margin + 3 * sizes[0], 28 * sizes[0], settings['canvas_size'][0] - margin, settings['canvas_size'][1] - 10 * sizes[0]) bar_1_x = bar_charts_bounds[0] + (bar_charts_bounds[2] - bar_charts_bounds[0]) * 1 / 4 bar_2_x = bar_charts_bounds[0] + (bar_charts_bounds[2] - bar_charts_bounds[0]) * 3 / 4 # Initialise drawing dwg = svgwrite.Drawing(profile='full') # Set viewbox attribute for scaling dwg.attribs['viewBox'] = '0 0 ' + ' '.join( [str(x) for x in settings['canvas_size']]) dwg.attribs['width'] = '95%' dwg.attribs['height'] = '95%' # Set HTML attributes for interaction # If the user doesn't set an ID for the chart, then it shouldn't have an ID defined in the XML. But, the svg_id variable still needs to be # defined for later, to make sure there's something to prepend to the IDs of any child elements that *do* need IDs. if settings['svg_id'] is None: settings['svg_id'] = 'grid' else: dwg.attribs['id'] = settings['svg_id'] if settings['svg_hidden']: dwg.attribs['style'] = 'display: none;' # Checkboxes dwg.add( dwg.polygon(points=[(tickbox_1_bounds[0], tickbox_1_bounds[1]), (tickbox_1_bounds[2], tickbox_1_bounds[1]), (tickbox_1_bounds[2], tickbox_1_bounds[3]), (tickbox_1_bounds[0], tickbox_1_bounds[3])], fill=COLORS['VL_GREY'], fill_opacity=0.8, stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=1, id='checkbox-1')) dwg.add( dwg.text('', insert=((tickbox_1_bounds[0] + tickbox_1_bounds[2]) / 2, (tickbox_1_bounds[1] + tickbox_1_bounds[3]) / 2), font_size=2 * sizes[1], font_family='Arial', font_weight='bold', fill=COLORS['BLACK'], fill_opacity=1, text_anchor='middle', alignment_baseline='central', id='checkbox-1-text')) dwg.add( dwg.text(settings['box_1_label'], insert=((tickbox_1_bounds[0] + tickbox_1_bounds[2]) / 2, tickbox_1_bounds[3] + 25), font_size=1.8 * sizes[1], font_family='Arial', fill=COLORS['BLACK'], fill_opacity=1, text_anchor='middle', alignment_baseline='central')) dwg.add( dwg.polygon(points=[(tickbox_2_bounds[0], tickbox_2_bounds[1]), (tickbox_2_bounds[2], tickbox_2_bounds[1]), (tickbox_2_bounds[2], tickbox_2_bounds[3]), (tickbox_2_bounds[0], tickbox_2_bounds[3])], fill=COLORS['VL_GREY'], fill_opacity=0.8, stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=1, id='checkbox-2')) dwg.add( dwg.text('', insert=((tickbox_2_bounds[0] + tickbox_2_bounds[2]) / 2, (tickbox_2_bounds[1] + tickbox_2_bounds[3]) / 2), font_size=2 * sizes[1], font_family='Arial', font_weight='bold', fill=COLORS['BLACK'], fill_opacity=1, text_anchor='middle', alignment_baseline='central', id='checkbox-2-text')) dwg.add( dwg.text(settings['box_2_label'], insert=((tickbox_2_bounds[0] + tickbox_2_bounds[2]) / 2, tickbox_2_bounds[3] + 25), font_size=1.8 * sizes[1], font_family='Arial', fill=COLORS['BLACK'], fill_opacity=1, text_anchor='middle', alignment_baseline='central')) # Divider line dwg.add( dwg.line((margin, divider_line_y), (settings['canvas_size'][0] - margin, divider_line_y), stroke=COLORS['L_GREY'], stroke_width=2)) # Bars dwg.add( dwg.polygon(points=[(bar_charts_bounds[0], bar_charts_bounds[1]), (bar_charts_bounds[2], bar_charts_bounds[1]), (bar_charts_bounds[2], bar_charts_bounds[3]), (bar_charts_bounds[0], bar_charts_bounds[3])], fill=COLORS['WHITE'], fill_opacity=0, stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=1, id='bar-charts-container')) dwg.add( dwg.text(settings['bar_label'], insert=(settings['canvas_size'][0] / 2, bar_charts_bounds[3] + 6 * sizes[0]), font_size=1.8 * sizes[1], font_family='Arial', fill=COLORS['BLACK'], fill_opacity=1, text_anchor='middle', alignment_baseline='central')) dwg.add( dwg.text(settings['bar_1_label'], insert=(bar_1_x, bar_charts_bounds[3] + 20), font_size=1.8 * sizes[1], font_family='Arial', fill=COLORS['BLACK'], fill_opacity=1, text_anchor='middle', alignment_baseline='central')) dwg.add( dwg.text(settings['bar_2_label'], insert=(bar_2_x, bar_charts_bounds[3] + 20), font_size=1.8 * sizes[1], font_family='Arial', fill=COLORS['BLACK'], fill_opacity=1, text_anchor='middle', alignment_baseline='central')) # Bar axis for i in range(10 + 1): height = bar_charts_bounds[1] + i * (bar_charts_bounds[3] - bar_charts_bounds[1]) / 10 dwg.add( dwg.line((bar_charts_bounds[0] - 5, height), (bar_charts_bounds[0] + 5, height), stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=1)) dwg.add( dwg.text(str(100 - i * 10), insert=(bar_charts_bounds[0] - sizes[0], height), font_size=1.5 * sizes[1], font_family='Arial', fill=COLORS['BLACK'], fill_opacity=1, text_anchor='end', alignment_baseline='central')) # Bar 1 dwg.add( dwg.polygon(points=[(bar_1_x - bar_width // 2, bar_charts_bounds[3]), (bar_1_x - bar_width // 2, bar_charts_bounds[1]), (bar_1_x + bar_width // 2, bar_charts_bounds[1]), (bar_1_x + bar_width // 2, bar_charts_bounds[3])], fill=COLORS['VL_GREY'], fill_opacity=0.5, stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=1)) # Box plot 1 box_plot_group_1 = dwg.g(id='boxplot-1', opacity=0) box_plot_group_1.add( dwg.polygon(points=[(bar_1_x - bar_width // 2, bar_charts_bounds[3]), (bar_1_x - bar_width // 2, bar_charts_bounds[1]), (bar_1_x + bar_width // 2, bar_charts_bounds[1]), (bar_1_x + bar_width // 2, bar_charts_bounds[3])], fill=COLORS['WHITE'], fill_opacity=1, stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=1)) box_plot_group_1.add( dwg.polygon(points=[ (bar_1_x - bar_width // 2, bar_charts_bounds[1] + 80), (bar_1_x - bar_width // 2, bar_charts_bounds[3] - 80), (bar_1_x + bar_width // 2, bar_charts_bounds[3] - 80), (bar_1_x + bar_width // 2, bar_charts_bounds[1] + 80) ], fill='url(#gradient-1)', fill_opacity=0.8, stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=0.5, id='boxplot-1-box')) gradient = LinearGradient(start=(0, 0), end=(0, 1), id='gradient-1') gradient.add_stop_color(offset='0%', color=COLORS['BAR_GREEN']) gradient.add_stop_color(offset='50%', color=COLORS['BAR_ORANGE']) gradient.add_stop_color(offset='100%', color=COLORS['BAR_RED']) dwg.defs.add(gradient) box_plot_group_1.add( dwg.line((bar_1_x - bar_width // 2, bar_charts_bounds[1] + 200), (bar_1_x + bar_width // 2, bar_charts_bounds[1] + 200), stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=0.5, stroke_dasharray=2, id='boxplot-1-line-high')) box_plot_group_1.add( dwg.line((bar_1_x - bar_width // 2, (bar_charts_bounds[1] + bar_charts_bounds[3]) // 2), (bar_1_x + bar_width // 2, (bar_charts_bounds[1] + bar_charts_bounds[3]) // 2), stroke=COLORS['BLACK'], stroke_width=3, stroke_opacity=0.8, stroke_dasharray=5, id='boxplot-1-line-mid')) box_plot_group_1.add( dwg.line((bar_1_x - bar_width // 2, bar_charts_bounds[3] - 200), (bar_1_x + bar_width // 2, bar_charts_bounds[3] - 200), stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=0.5, stroke_dasharray=2, id='boxplot-1-line-low')) box_plot_group_1.add( dwg.line((bar_1_x - bar_width // 2, bar_charts_bounds[3]), (bar_1_x + bar_width // 2, bar_charts_bounds[3]), fill_opacity=0, stroke=COLORS['BLACK'], stroke_width=4, stroke_opacity=1, id='bar-1-mainline')) box_plot_group_1.add( dwg.text('', insert=(bar_1_x, bar_charts_bounds[3]), font_size=2 * sizes[1], font_family='Arial', font_weight='bold', fill=COLORS['BLACK'], fill_opacity=1, text_anchor='middle', alignment_baseline='central', id='bar-1-label')) dwg.add(box_plot_group_1) # Bar 2 dwg.add( dwg.polygon(points=[(bar_2_x - bar_width // 2, bar_charts_bounds[3]), (bar_2_x - bar_width // 2, bar_charts_bounds[1]), (bar_2_x + bar_width // 2, bar_charts_bounds[1]), (bar_2_x + bar_width // 2, bar_charts_bounds[3])], fill=COLORS['VL_GREY'], fill_opacity=0.5, stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=1)) # Box plot 2 box_plot_group_2 = dwg.g(id='boxplot-2', opacity=0) box_plot_group_2.add( dwg.polygon(points=[(bar_2_x - bar_width // 2, bar_charts_bounds[3]), (bar_2_x - bar_width // 2, bar_charts_bounds[1]), (bar_2_x + bar_width // 2, bar_charts_bounds[1]), (bar_2_x + bar_width // 2, bar_charts_bounds[3])], fill=COLORS['WHITE'], fill_opacity=1, stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=1)) box_plot_group_2.add( dwg.polygon(points=[ (bar_2_x - bar_width // 2, bar_charts_bounds[1] + 80), (bar_2_x - bar_width // 2, bar_charts_bounds[3] - 80), (bar_2_x + bar_width // 2, bar_charts_bounds[3] - 80), (bar_2_x + bar_width // 2, bar_charts_bounds[1] + 80) ], fill='url(#gradient-2)', fill_opacity=0.8, stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=0.5, id='boxplot-2-box')) gradient = LinearGradient(start=(0, 0), end=(0, 1), id='gradient-2') gradient.add_stop_color(offset='0%', color=COLORS['BAR_GREEN']) gradient.add_stop_color(offset='50%', color=COLORS['BAR_ORANGE']) gradient.add_stop_color(offset='100%', color=COLORS['BAR_RED']) dwg.defs.add(gradient) box_plot_group_2.add( dwg.line((bar_2_x - bar_width // 2, bar_charts_bounds[1] + 200), (bar_2_x + bar_width // 2, bar_charts_bounds[1] + 200), stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=0.5, stroke_dasharray=2, id='boxplot-2-line-high')) box_plot_group_2.add( dwg.line((bar_2_x - bar_width // 2, (bar_charts_bounds[1] + bar_charts_bounds[3]) // 2), (bar_2_x + bar_width // 2, (bar_charts_bounds[1] + bar_charts_bounds[3]) // 2), stroke=COLORS['BLACK'], stroke_width=3, stroke_opacity=0.8, stroke_dasharray=5, id='boxplot-2-line-mid')) box_plot_group_2.add( dwg.line((bar_2_x - bar_width // 2, bar_charts_bounds[3] - 200), (bar_2_x + bar_width // 2, bar_charts_bounds[3] - 200), stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=0.5, stroke_dasharray=2, id='boxplot-2-line-low')) box_plot_group_2.add( dwg.line((bar_2_x - bar_width // 2, bar_charts_bounds[3]), (bar_2_x + bar_width // 2, bar_charts_bounds[3]), fill_opacity=0, stroke=COLORS['BLACK'], stroke_width=4, stroke_opacity=1, id='bar-2-mainline')) box_plot_group_2.add( dwg.text('', insert=(bar_2_x, bar_charts_bounds[3]), font_size=2 * sizes[1], font_family='Arial', font_weight='bold', fill=COLORS['BLACK'], fill_opacity=1, text_anchor='middle', alignment_baseline='central', id='bar-2-label')) dwg.add(box_plot_group_2) return dwg.tostring()