def test_set_value(self): a = Animate(debug=True) a.set_value('0;1;2', 'linear', '0', '0 0 0 0', 0, 0, 0) self.assertEqual(a.tostring(), '<animate by="0" calcMode="linear" from="0" ' \ 'keySplines="0 0 0 0" keyTimes="0" to="0" '\ 'values="0;1;2" />')
def test_values_int(self): s = Animate(values=(3,), debug=True) self.assertEqual(s.tostring(), '<animate values="3" />')
def test_values_list(self): s = Animate(values=[1, 2, 3], debug=True) self.assertEqual(s.tostring(), '<animate values="1;2;3" />')
def test_freeze(self): a = Animate(debug=True) a.freeze() self.assertEqual(a.tostring(), '<animate fill="freeze" />')
def test_constructor(self): a = Animate('x', debug=True) self.assertEqual(a.tostring(), '<animate attributeName="x" />')
def test_values_int(self): s = Animate(values=(3, ), debug=True) self.assertEqual(s.tostring(), '<animate values="3" />')
def test_values_colors(self): s = Animate(values='#000000;#0000ff;#00ff00;#ff0000', debug=True) self.assertEqual(s.tostring(), '<animate values="#000000;#0000ff;#00ff00;#ff0000" />')
def concentric(datapoints, settings={}): # Check arguments try: assert len(datapoints) > 0 for dp_set in datapoints: for dp in dp_set: if dp is None: continue if len(set(['continuous', 'discrete']) - set(dp.keys())) > 0: print( 'ERROR: every datapoint must have at least "continuous" and "discrete" key values defined' ) return '' except: print( 'ERROR: datapoints argument should be a two-dimensional iterable of dictionaries' ) return '' num_segments = len(datapoints) num_versions = len(datapoints[0]) num_rings = None for i in range(num_segments): if datapoints[i][0] is not None: num_rings = len(datapoints[i][0]['continuous']) break for setting_name, setting_default in DEFAULT_SETTINGS_CONCENTRIC.items(): if setting_name not in settings.keys(): settings[setting_name] = setting_default if settings['ring_names'] is None: settings['ring_names'] = ['' for _ in range(num_rings)] if len(settings['ring_names']) != num_rings: print( 'ERROR: specified number of ring names is inconsistent with the data' ) return '' if settings['ring_types'] is None: settings['ring_types'] = ['C' for _ in range(num_rings)] if len(settings['ring_types']) != num_rings: print( 'ERROR: specified number of ring types is inconsistent with the data' ) return '' if settings['ring_polarities'] is None: settings['ring_polarities'] = [+1 for _ in range(num_rings)] if len(settings['ring_polarities']) != num_rings: print( 'ERROR: specified number of ring polarities is inconsistent with the data' ) return '' # Useful calculations gap = float(settings['top_gap']) sizes = (min(settings['canvas_size']) // 100, min(settings['canvas_size']) // 60) center = (settings['canvas_size'][0] // 2, settings['canvas_size'][1] // 2) full_radius = min(settings['canvas_size']) // 2 - 10 division_size = full_radius // (num_rings + 2) angle_delta = (2 * pi - gap) / float(num_segments) # 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'] = 'concentric' else: dwg.attribs['id'] = settings['svg_id'] if settings['svg_hidden']: dwg.attribs['style'] = 'display: none;' # Draw axes for ring_id, ring_name in enumerate(settings['ring_names']): ring_color = settings['ring_color_sequence'][ring_id] if settings[ 'ring_types'][ring_id] == 'C' else settings['discrete_ring_color'] dwg.add( dwg.circle(r=(ring_id + 2) * division_size, center=center, fill_opacity=0, stroke=ring_color, stroke_width=1, stroke_opacity=1)) dwg.add( dwg.polyline([ _coords_from_angle(center, (gap / float(25)) * (i - (20 - 1) / float(2)), (ring_id + 2) * division_size) for i in range(20) ], stroke=ring_color, stroke_width=3, stroke_opacity=1, fill_opacity=0)) dwg.add( dwg.text(text=ring_name, insert=_coords_from_angle( center, 0, (ring_id + 2) * division_size + sizes[1]), font_size=sizes[1], font_family='Arial', text_anchor='middle', alignment_baseline='central')) # Get means for continuous metrics; get ranges for discrete metrics ring_averages = [] ring_categories = [] for ring_id in range(num_rings): ring_values_cont = [] ring_values_disc = set() for dp_set in datapoints: for dp in dp_set: if dp is None: continue if dp['continuous'] is not None and dp['continuous'][ ring_id] is not None: dp['continuous'][ring_id] *= settings['ring_polarities'][ ring_id] ring_values_cont.append(dp['continuous'][ring_id]) if dp['discrete'] is not None and dp['discrete'][ ring_id] is not None: ring_values_disc.add(dp['discrete'][ring_id]) ring_avg = None if len(ring_values_cont) > 0: ring_avg = sum(ring_values_cont) / len(ring_values_cont) ring_averages.append(ring_avg) ring_ordered_set = tuple(sorted(ring_values_disc)) ring_categories.append(ring_ordered_set) # Calculate magnitudes for every point and use them to find the min/max for each metric magnitudes_by_rsv = [] # RSV = ring, segment, version plot_magnitudes_by_rsv = [] for ring_id in range(num_rings): # Calculate average negative delta in the latest dataset latest_negative_deltas = [] for dp_set in datapoints: if dp_set[-1] is None or dp_set[-1]['continuous'][ring_id] is None: continue delta = dp_set[-1]['continuous'][ring_id] - ring_averages[ring_id] if delta < 0: latest_negative_deltas.append(delta) avg_negative_delta = 0 if len(latest_negative_deltas) > 0: avg_negative_delta = sum(latest_negative_deltas) / len( latest_negative_deltas) # Subtract the average negative delta from all deltas to calculate 'magnitudes' ring_magnitudes = [] for dp_set in datapoints: magnitudes_set = [] for dp in dp_set: magnitude = None if dp is None or dp['continuous'] is None or dp['continuous'][ ring_id] is None: magnitudes_set.append(magnitude) continue delta = dp['continuous'][ring_id] - ring_averages[ring_id] magnitude = delta - avg_negative_delta magnitudes_set.append(magnitude) ring_magnitudes.append(magnitudes_set) magnitudes_by_rsv.append(ring_magnitudes) # Calculate 'plot magnitudes' all_ring_magnitudes = [ magnitude for magnitudes_set in ring_magnitudes for magnitude in magnitudes_set if magnitude is not None ] ring_magnitude_minmax = (0, 0) if len(all_ring_magnitudes) > 0: ring_magnitude_minmax = (min(all_ring_magnitudes), max(all_ring_magnitudes)) ring_plot_magnitudes = [] for dp_id, dp_set in enumerate(datapoints): plot_magnitudes_set = [] for version_id, dp in enumerate(dp_set): magnitude = ring_magnitudes[dp_id][version_id] if magnitude is None: plot_magnitude = None else: plot_magnitude = 0 if magnitude > 0 and ring_magnitude_minmax[1] != 0: plot_magnitude = magnitude / ring_magnitude_minmax[ 1] * settings['axis_max'] elif magnitude < 0 and ring_magnitude_minmax[0] != 0: plot_magnitude = magnitude / ring_magnitude_minmax[ 0] * settings['axis_min'] plot_magnitudes_set.append(plot_magnitude) ring_plot_magnitudes.append(plot_magnitudes_set) plot_magnitudes_by_rsv.append(ring_plot_magnitudes) # Calculate plot point coordinates line_points_by_rvs = [] # RVS = ring, version, segment discrete_segs_by_rvs = [] for ring_id in range(num_rings): ring_base_radius = (ring_id + 2) * division_size # Continuous lines if settings['ring_types'][ring_id] == 'C': line_points = [[] for _ in range(num_versions)] # Add zero-point for version_id in range(num_versions): point = _coords_from_angle(center, angle_delta * 0.5, ring_base_radius, gap=gap) line_points[version_id].append(point) for segment_id in range(num_segments): base_angle = angle_delta * (segment_id + 0.5) for version_id, dp in enumerate(dp_set): plot_magnitude = plot_magnitudes_by_rsv[ring_id][ segment_id][version_id] if plot_magnitude is None: point = _coords_from_angle(center, base_angle, ring_base_radius, gap=gap) else: plot_radius = ring_base_radius + division_size * plot_magnitude point = _coords_from_angle(center, base_angle, plot_radius, gap=gap) line_points[version_id].append(point) line_points_by_rvs.append(line_points) discrete_segs_by_rvs.append(None) # Discrete segments elif settings['ring_types'][ring_id] == 'D': ring_discrete_segs = [[] for _ in range(num_versions)] for segment_id, dp_set in enumerate(datapoints): base_angle = angle_delta * (segment_id + 0.5) for version_id, dp in enumerate(dp_set): segment_length = sizes[0] segment_color = settings['discrete_color_sequence'][-1] segment_opacity = 1 if dp is not None and dp['discrete'] is not None and dp[ 'discrete'][ring_id] is not None: category_id = dp['discrete'][ring_id] segment_color = settings['discrete_color_sequence'][ category_id] if segment_color == settings['discrete_color_sequence'][ -1]: segment_opacity = 0.5 segment_points = (_coords_from_angle( center, angle_delta * (segment_id), ring_base_radius - segment_length, gap=gap), _coords_from_angle( center, angle_delta * (segment_id), ring_base_radius + segment_length, gap=gap), _coords_from_angle( center, angle_delta * (segment_id + 1), ring_base_radius + segment_length, gap=gap), _coords_from_angle( center, angle_delta * (segment_id + 1), ring_base_radius - segment_length, gap=gap)) ring_discrete_segs[version_id].append( (segment_points, segment_color, segment_opacity)) line_points_by_rvs.append(None) discrete_segs_by_rvs.append(ring_discrete_segs) # Draw plot points for ring_id in range(num_rings): baseline_circle_points = [] for i in range(settings['baseline_point_resolution'] + 1): point_angle = (settings['baseline_point_resolution'] - i) * ( 2 * pi - gap) / float(settings['baseline_point_resolution']) point_radius = (ring_id + 2) * division_size baseline_circle_points.append( _coords_from_angle(center, point_angle, point_radius, gap=gap)) # Continuous lines if settings['ring_types'][ring_id] == 'C': line_points = line_points_by_rvs[ring_id][ -1] + baseline_circle_points ring_line = dwg.polyline( line_points, stroke=settings['ring_color_sequence'][ring_id], stroke_width=2, stroke_opacity=1, fill=settings['ring_color_sequence'][ring_id], fill_opacity=0.2, id=settings['svg_id'] + '-contline-' + str(ring_id)) for version_id in range(num_versions): line_points = line_points_by_rvs[ring_id][ version_id] + baseline_circle_points points_string = ' '.join([ ','.join([str(c) for c in point]) for point in line_points ]) animation = Animate( values=None, dur=str(settings['animation_length']) + 'ms', begin='indefinite', fill='freeze', attributeName='points', to=points_string, id=settings['svg_id'] + '-animation-' + str(ring_id) + '-' + str(version_id)) ring_line.add(animation) dwg.add(ring_line) # Discrete segments elif settings['ring_types'][ring_id] == 'D': for version_id in range(num_versions): group_opacity = 1 if version_id == num_versions - 1 else 0 segment_group = dwg.g(id=settings['svg_id'] + '-discrete-' + str(ring_id) + '-' + str(version_id), opacity=group_opacity) segment_point_groups = discrete_segs_by_rvs[ring_id][ version_id] for segment_points, segment_color, segment_opacity in segment_point_groups: segment_group.add( dwg.polyline(segment_points, stroke_width=0, stroke_opacity=0, fill=segment_color, fill_opacity=segment_opacity)) dwg.add(segment_group) # Draw missing-data shade if settings['missing_data_shade_enabled']: for version_id in range(num_versions): group_opacity = 1 if version_id == num_versions - 1 else 0 shade_group = dwg.g(id=settings['svg_id'] + '-shade-' + str(version_id), opacity=group_opacity) for dp_id, dp_set in enumerate(datapoints): if dp_set[version_id] is None: shade_group.add( dwg.polygon([ center, _coords_from_angle(center, angle_delta * dp_id, full_radius + 5, gap=gap), _coords_from_angle(center, angle_delta * (dp_id + 1), full_radius + 5, gap=gap) ], stroke_opacity=0, fill=settings['missing_data_shade_color'], fill_opacity=1)) """ shade_group.add(dwg.circle(r=sizes[0]//2, center=_coords_from_angle(center, angle_delta*(dp_id+0.5), full_radius-1.5*sizes[1], gap=gap), fill=COLORS['BLACK'], fill_opacity=1, stroke_opacity=0)) """ dwg.add(shade_group) # Draw outer markers if settings['apply_markers']: any_markers = False for version_id in range(num_versions): group_opacity = 1 if version_id == num_versions - 1 else 0 markers_group = dwg.g(id=settings['svg_id'] + '-markers-' + str(version_id), opacity=group_opacity) for dp_id, dp_set in enumerate(datapoints): if dp_set[version_id] is None: continue if 'marker' in dp_set[version_id] and dp_set[version_id][ 'marker'] == True: any_markers = True markers_group.add( dwg.line( _coords_from_angle(center, angle_delta * (dp_id + 0.2), full_radius - sizes[0] * 0.1, gap=gap), _coords_from_angle(center, angle_delta * (dp_id + 0.8), full_radius - sizes[0] * 0.9, gap=gap), stroke=settings['marker_color'], stroke_width=3, stroke_opacity=1)) markers_group.add( dwg.line( _coords_from_angle(center, angle_delta * (dp_id + 0.2), full_radius - sizes[0] * 0.9, gap=gap), _coords_from_angle(center, angle_delta * (dp_id + 0.8), full_radius - sizes[0] * 0.1, gap=gap), stroke=settings['marker_color'], stroke_width=3, stroke_opacity=1)) dwg.add(markers_group) if any_markers: dwg.add( dwg.text(text=settings['marker_label'], insert=_coords_from_angle( center, 0, full_radius - sizes[0] * 0.1), font_size=sizes[1], font_family='Arial', text_anchor='middle', alignment_baseline='central')) # Draw outer rings dwg.add( dwg.circle(r=full_radius - 2 * sizes[1], center=center, fill_opacity=0, stroke=COLORS['BLACK'], stroke_width=1, stroke_opacity=0.5)) dwg.add( dwg.circle(r=full_radius - 1 * sizes[1], center=center, fill_opacity=0, stroke=COLORS['BLACK'], stroke_width=1, stroke_opacity=0.5)) for i in range(num_segments + 1): dwg.add( dwg.line(_coords_from_angle(center, angle_delta * i, full_radius - 2 * sizes[1], gap=gap), _coords_from_angle(center, angle_delta * i, full_radius - 1 * sizes[1], gap=gap), stroke=COLORS['BLACK'], stroke_width=1, stroke_opacity=0.5)) # Draw segment selector if settings['segment_selector_enabled']: center_point = angle_delta * 0.5 selector_points = (_coords_from_angle(center, center_point, full_radius - 1.5 * sizes[1], gap=gap), _coords_from_angle(center, center_point - 0.02, full_radius - 0.5 * sizes[1], gap=gap), _coords_from_angle(center, center_point - 0.02, full_radius + 5, gap=gap), _coords_from_angle(center, center_point + 0.02, full_radius + 5, gap=gap), _coords_from_angle(center, center_point + 0.02, full_radius - 0.5 * sizes[1], gap=gap)) dwg.add( dwg.polygon(selector_points, stroke=COLORS['BLACK'], stroke_width=2, stroke_opacity=1, fill=COLORS['D_GREY'], fill_opacity=0.2, id=settings['svg_id'] + '-selector')) # Draw interaction segments if settings['interaction_segments_enabled']: for i in range(num_segments): dwg.add( dwg.polygon([ center, _coords_from_angle( center, angle_delta * i, full_radius + 5, gap=gap), _coords_from_angle(center, angle_delta * (i + 1), full_radius + 5, gap=gap) ], stroke=COLORS['BLACK'], stroke_width=1, stroke_opacity=0, fill=COLORS['L_GREY'], fill_opacity=0, onmousedown=settings['segment_function_name'] + '(1, ' + str(i) + ');', onmouseover=settings['segment_function_name'] + '(2, ' + str(i) + ');', onmouseup=settings['segment_function_name'] + '(3, ' + str(i) + ');', id=settings['svg_id'] + '-intseg-' + str(i))) dwg.add( dwg.circle(r=1.5 * division_size, center=center, fill=COLORS['WHITE'], fill_opacity=1, stroke_opacity=0)) # Draw center text if settings['center_text_1'] is not None: dwg.add( dwg.text(text=settings['center_text_1'], insert=(center[0], center[1] - 1.5 * sizes[1]), font_size=1.5 * sizes[1], font_family='Arial', font_weight='bold', text_anchor='middle', alignment_baseline='central')) if settings['center_text_2'] is not None: dwg.add( dwg.text(text=settings['center_text_2'], insert=(center[0], center[1] + 1 * sizes[1]), font_size=sizes[1], font_family='Arial', text_anchor='middle', alignment_baseline='central')) if settings['center_text_3'] is not None: dwg.add( dwg.text(text=settings['center_text_3'], insert=(center[0], center[1] + 3 * sizes[1]), font_size=sizes[1], font_family='Arial', text_anchor='middle', alignment_baseline='central', fill=COLORS['L_GREY'])) return dwg.tostring()