def _draw_rings(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, center: XY, radius_range: ValueRange) -> None: length_range = self.poster.length_range_by_date if not length_range.is_valid(): return min_length = length_range.lower() max_length = length_range.upper() assert min_length is not None assert max_length is not None ring_distance = self._determine_ring_distance(max_length) if ring_distance is None: return distance = ring_distance while distance < max_length: radius = radius_range.interpolate( (distance / max_length).magnitude) g.add( dr.circle( center=center.tuple(), r=radius, stroke=self._ring_color, stroke_opacity="0.2", fill="none", stroke_width=0.3, )) distance += ring_distance
def draw(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY) -> None: """Draw the heatmap based on tracks.""" bbox = self._determine_bbox() year_groups: typing.Dict[int, svgwrite.container.Group] = {} for tr in self.poster.tracks: year = tr.start_time().year if year not in year_groups: g_year = dr.g(id=f"year{year}") g.add(g_year) year_groups[year] = g_year else: g_year = year_groups[year] color = self.color(self.poster.length_range, tr.length(), tr.special) for line in utils.project(bbox, size, offset, tr.polylines): for opacity, width in [(0.1, 5.0), (0.2, 2.0), (1.0, 0.3)]: g_year.add( dr.polyline( points=line, stroke=color, stroke_opacity=opacity, fill="none", stroke_width=width, stroke_linejoin="round", stroke_linecap="round", ))
def draw(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY) -> None: """For each track, draw it on the poster.""" if self.poster.tracks is None: raise PosterError("No tracks to draw.") cell_size, counts = utils.compute_grid(len(self.poster.tracks), size) if cell_size is None or counts is None: raise PosterError("Unable to compute grid.") count_x, count_y = counts[0], counts[1] spacing_x = 0 if count_x <= 1 else (size.x - cell_size * count_x) / ( count_x - 1) spacing_y = 0 if count_y <= 1 else (size.y - cell_size * count_y) / ( count_y - 1) offset.x += (size.x - count_x * cell_size - (count_x - 1) * spacing_x) / 2 offset.y += (size.y - count_y * cell_size - (count_y - 1) * spacing_y) / 2 year_groups: typing.Dict[int, svgwrite.container.Group] = {} for (index, tr) in enumerate(self.poster.tracks): year = tr.start_time().year if year not in year_groups: g_year = dr.g(id=f"year{year}") g.add(g_year) year_groups[year] = g_year else: g_year = year_groups[year] p = XY(index % count_x, index // count_x) * XY( cell_size + spacing_x, cell_size + spacing_y) self._draw_track( dr, g_year, tr, 0.9 * XY(cell_size, cell_size), offset + 0.05 * XY(cell_size, cell_size) + p, )
def _draw_circle_segment( self, dr: svgwrite.Drawing, g: svgwrite.container.Group, tracks: typing.List[Track], a1: float, a2: float, rr: ValueRange, center: XY, values: str = "", key_times: str = "", ) -> None: length = sum([t.length() for t in tracks]) has_special = len([t for t in tracks if t.special]) > 0 color = self.color(self.poster.length_range_by_date, length, has_special) max_length = self.poster.length_range_by_date.upper() if self._max_distance: max_length = self._max_distance.to_base_units() assert max_length is not None r1 = rr.lower() assert r1 is not None r2 = rr.interpolate((length / max_length).magnitude) sin_a1, cos_a1 = math.sin(a1), math.cos(a1) sin_a2, cos_a2 = math.sin(a2), math.cos(a2) path = dr.path( d=("M", center.x + r1 * sin_a1, center.y - r1 * cos_a1), fill=color, stroke="none", ) path.push("l", (r2 - r1) * sin_a1, (r1 - r2) * cos_a1) path.push( f"a{r2},{r2} 0 0,0 {r2 * (sin_a2 - sin_a1)},{r2 * (cos_a1 - cos_a2)}" ) path.push("l", (r1 - r2) * sin_a2, (r2 - r1) * cos_a2) date_title = str(tracks[0].start_time().date()) str_length = utils.format_float(self.poster.m2u(length)) path.set_desc(title=f"{date_title} {str_length} {self.poster.u()}") if self.poster.with_animation: path.add( svgwrite.animate.Animate( "opacity", dur=f"{self.poster.animation_time}s", values=values, keyTimes=key_times, repeatCount="indefinite", )) g.add(path)
def _draw_track(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, tr: Track, size: XY, offset: XY) -> None: color = self.color(self.poster.length_range, tr.length(), tr.special) str_length = utils.format_float(self.poster.m2u(tr.length())) date_title = str(tr.start_time.date()) if tr.start_time else "Unknown Date" for line in utils.project(tr.bbox(), size, offset, tr.polylines): polyline = dr.polyline( points=line, stroke=color, fill="none", stroke_width=0.5, stroke_linejoin="round", stroke_linecap="round", ) polyline.set_desc(title=f"{date_title} {str_length} {self.poster.u()}") g.add(polyline)
def _draw_circle_segment( self, dr: svgwrite.Drawing, g: svgwrite.container.Group, tracks: typing.List[Track], a1: float, a2: float, rr: ValueRange, center: XY, ) -> None: length = sum([t.length() for t in tracks]) has_special = len([t for t in tracks if t.special]) > 0 color = self.color(self.poster.length_range_by_date, length, has_special) max_length = self.poster.length_range_by_date.upper() assert max_length is not None r1 = rr.lower() assert r1 is not None r2 = rr.interpolate((length / max_length).magnitude) sin_a1, cos_a1 = math.sin(a1), math.cos(a1) sin_a2, cos_a2 = math.sin(a2), math.cos(a2) path = dr.path( d=("M", center.x + r1 * sin_a1, center.y - r1 * cos_a1), fill=color, stroke="none", ) path.push("l", (r2 - r1) * sin_a1, (r1 - r2) * cos_a1) path.push( f"a{r2},{r2} 0 0,0 {r2 * (sin_a2 - sin_a1)},{r2 * (cos_a1 - cos_a2)}" ) path.push("l", (r1 - r2) * sin_a2, (r2 - r1) * cos_a2) date_title = str(tracks[0].start_time.date() ) if tracks[0].start_time else "Unknown date" str_length = utils.format_float(self.poster.m2u(length)) path.set_desc(title=f"{date_title} {str_length} {self.poster.u()}") g.add(path)
def _draw_year(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY, year: int) -> None: min_size = min(size.x, size.y) outer_radius = 0.5 * min_size - 6 radius_range = ValueRange.from_pair(outer_radius / 4, outer_radius) center = offset + 0.5 * size if self._rings: self._draw_rings(dr, g, center, radius_range) year_style = f"dominant-baseline: central; font-size:{min_size * 4.0 / 80.0}px; font-family:Arial;" month_style = f"font-size:{min_size * 3.0 / 80.0}px; font-family:Arial;" g.add( dr.text( f"{year}", insert=center.tuple(), fill=self.poster.colors["text"], text_anchor="middle", alignment_baseline="middle", style=year_style, )) df = 360.0 / (366 if calendar.isleap(year) else 365) day = 0 date = datetime.date(year, 1, 1) while date.year == year: text_date = date.strftime("%Y-%m-%d") a1 = math.radians(day * df) a2 = math.radians((day + 1) * df) if date.day == 1: (_, last_day) = calendar.monthrange(date.year, date.month) a3 = math.radians((day + last_day - 1) * df) sin_a1, cos_a1 = math.sin(a1), math.cos(a1) sin_a3, cos_a3 = math.sin(a3), math.cos(a3) r1 = outer_radius + 1 r2 = outer_radius + 6 r3 = outer_radius + 2 g.add( dr.line( start=(center + r1 * XY(sin_a1, -cos_a1)).tuple(), end=(center + r2 * XY(sin_a1, -cos_a1)).tuple(), stroke=self.poster.colors["text"], stroke_width=0.3, )) path = dr.path( d=("M", center.x + r3 * sin_a1, center.y - r3 * cos_a1), fill="none", stroke="none", ) path.push( f"a{r3},{r3} 0 0,1 {r3 * (sin_a3 - sin_a1)},{r3 * (cos_a1 - cos_a3)}" ) g.add(path) tpath = svgwrite.text.TextPath( path, self.poster.month_name(date.month), startOffset=(0.5 * r3 * (a3 - a1))) text = dr.text( "", fill=self.poster.colors["text"], text_anchor="middle", style=month_style, ) text.add(tpath) g.add(text) if text_date in self.poster.tracks_by_date: self._draw_circle_segment( dr, g, self.poster.tracks_by_date[text_date], a1, a2, radius_range, center, ) day += 1 date += datetime.timedelta(1)
def _draw(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY, year: int) -> None: min_size = min(size.x, size.y) year_size = min_size * 4.0 / 80.0 year_style = f"font-size:{year_size}px; font-family:Arial;" month_style = f"font-size:{min_size * 3.0 / 80.0}px; font-family:Arial;" day_style = f"dominant-baseline: central; font-size:{min_size * 1.0 / 80.0}px; font-family:Arial;" day_length_style = f"font-size:{min_size * 1.0 / 80.0}px; font-family:Arial;" g.add( dr.text( f"{year}", insert=offset.tuple(), fill=self.poster.colors["text"], alignment_baseline="hanging", style=year_style, )) offset.y += year_size size.y -= year_size count_x = 31 for month in range(1, 13): date = datetime.date(year, month, 1) (_, last_day) = calendar.monthrange(year, month) count_x = max(count_x, date.weekday() + last_day) cell_size = min(size.x / count_x, size.y / 36) spacing = XY( (size.x - cell_size * count_x) / (count_x - 1), (size.y - cell_size * 3 * 12) / 11, ) for month in range(1, 13): date = datetime.date(year, month, 1) y = month - 1 y_pos = offset.y + (y * 3 + 1) * cell_size + y * spacing.y g.add( dr.text( self.poster.month_name(month), insert=(offset.x, y_pos - 2), fill=self.poster.colors["text"], alignment_baseline="hanging", style=month_style, )) day_offset = date.weekday() while date.month == month: x = date.day - 1 x_pos = offset.x + (day_offset + x) * cell_size + x * spacing.x pos = (x_pos + 0.05 * cell_size, y_pos + 1.15 * cell_size) dim = (cell_size * 0.9, cell_size * 0.9) text_date = date.strftime("%Y-%m-%d") if text_date in self.poster.tracks_by_date: tracks = self.poster.tracks_by_date[text_date] length = sum([t.length() for t in tracks]) has_special = len([t for t in tracks if t.special]) > 0 color = self.color(self.poster.length_range_by_date, length, has_special) g.add(dr.rect(pos, dim, fill=color)) g.add( dr.text( utils.format_float(self.poster.m2u(length)), insert=( pos[0] + cell_size / 2, pos[1] + cell_size + cell_size / 2, ), text_anchor="middle", style=day_length_style, fill=self.poster.colors["text"], )) else: g.add(dr.rect(pos, dim, fill="#444444")) g.add( dr.text( localized_day_of_week_name(date.weekday(), short=True), insert=( offset.x + (day_offset + x) * cell_size + cell_size / 2, pos[1] + cell_size / 2, ), text_anchor="middle", alignment_baseline="middle", style=day_style, )) date += datetime.timedelta(1)
def draw(self, dr: svgwrite.Drawing, g: svgwrite.container.Group, size: XY, offset: XY) -> None: if self.poster.tracks is None: raise PosterError("No tracks to draw") year_size = 200 * 4.0 / 80.0 year_style = f"font-size:{year_size}px; font-family:Arial;" year_length_style = f"font-size:{110 * 3.0 / 80.0}px; font-family:Arial;" month_names_style = "font-size:2.5px; font-family:Arial" total_length_year_dict = self.poster.total_length_year_dict for year in self.poster.years.iter(): g_year = dr.g(id=f"year{year}") g.add(g_year) start_date_weekday, _ = calendar.monthrange(year, 1) github_rect_first_day = datetime.date(year, 1, 1) # Github profile the first day start from the last Monday of the last year or the first Monday of this year # It depands on if the first day of this year is Monday or not. github_rect_day = github_rect_first_day + datetime.timedelta( -start_date_weekday) year_length = total_length_year_dict.get(year, 0) year_length_str = utils.format_float(self.poster.m2u(year_length)) month_names = [ locale.nl_langinfo(day)[:3] # Get only first three letters for day in [ locale.MON_1, locale.MON_2, locale.MON_3, locale.MON_4, locale.MON_5, locale.MON_6, locale.MON_7, locale.MON_8, locale.MON_9, locale.MON_10, locale.MON_11, locale.MON_12, ] ] km_or_mi = self.poster.u() g_year.add( dr.text( f"{year}", insert=offset.tuple(), fill=self.poster.colors["text"], alignment_baseline="hanging", style=year_style, )) g_year.add( dr.text( f"{year_length_str} {km_or_mi}", insert=(offset.tuple()[0] + 165, offset.tuple()[1] + 2), fill=self.poster.colors["text"], alignment_baseline="hanging", style=year_length_style, )) # add month name up to the poster one by one because of svg text auto trim the spaces. for num, name in enumerate(month_names): g_year.add( dr.text( f"{name}", insert=(offset.tuple()[0] + 15.5 * num, offset.tuple()[1] + 14), fill=self.poster.colors["text"], style=month_names_style, )) rect_x = 10.0 dom = (2.6, 2.6) # add every day of this year for 53 weeks and per week has 7 days for _i in range(54): rect_y = offset.y + year_size + 2 for _j in range(7): if int(github_rect_day.year) > year: break rect_y += 3.5 color = "#444444" date_title = str(github_rect_day) if date_title in self.poster.tracks_by_date: tracks = self.poster.tracks_by_date[date_title] length = sum([t.length() for t in tracks]) distance1 = self.poster.special_distance[ "special_distance"] distance2 = self.poster.special_distance[ "special_distance2"] has_special = distance1 < length < distance2 color = self.color(self.poster.length_range_by_date, length, has_special) if length >= distance2: special_color = self.poster.colors.get( "special2") or self.poster.colors.get( "special") if special_color is not None: color = special_color str_length = utils.format_float( self.poster.m2u(length)) date_title = f"{date_title} {str_length} {km_or_mi}" rect = dr.rect((rect_x, rect_y), dom, fill=color) rect.set_desc(title=date_title) g_year.add(rect) github_rect_day += datetime.timedelta(1) rect_x += 3.5 offset.y += 3.5 * 9 + year_size + 1.5