github-heatmap 1.4.0__tar.gz → 2.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/PKG-INFO +10 -2
  2. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/README.md +8 -8
  3. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/cli.py +53 -15
  4. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/config.py +3 -2
  5. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/drawer.py +30 -8
  6. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/base_loader.py +29 -9
  7. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/config.py +2 -2
  8. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/github_loader.py +2 -1
  9. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/gitlab_loader.py +1 -1
  10. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/notion_loader.py +62 -22
  11. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/weread_loader.py +23 -15
  12. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/poster.py +20 -10
  13. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/skyline/skyline.py +1 -1
  14. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/utils.py +53 -0
  15. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/PKG-INFO +10 -2
  16. github_heatmap-2.2.0/pyproject.toml +6 -0
  17. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/setup.py +1 -1
  18. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/tests/test_notion_loader.py +44 -5
  19. github_heatmap-2.2.0/tests/test_poster_utils.py +91 -0
  20. github_heatmap-1.4.0/pyproject.toml +0 -2
  21. github_heatmap-1.4.0/tests/test_poster_utils.py +0 -46
  22. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/LICENSE +0 -0
  23. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/__init__.py +0 -0
  24. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/__main__.py +0 -0
  25. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/circluar_drawer.py +0 -0
  26. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/err.py +0 -0
  27. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/html_parser/__init__.py +0 -0
  28. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/html_parser/github_parser.py +0 -0
  29. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/html_parser/gitlab_parser.py +0 -0
  30. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/html_parser/jike_parse.py +0 -0
  31. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/html_parser/kindle_parser.py +0 -0
  32. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/__init__.py +0 -0
  33. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/apple_health_loader.py +0 -0
  34. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/bbdc_loader.py +0 -0
  35. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/bilibili_loader.py +0 -0
  36. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/chatgpt_loader.py +0 -0
  37. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/cichang_loader.py +0 -0
  38. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/covid_loader.py +0 -0
  39. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/dota2_loader.py +0 -0
  40. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/duolingo_loader.py +0 -0
  41. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/forest_loader.py +0 -0
  42. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/from_github_issue_loader.py +0 -0
  43. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/garmin_loader.py +0 -0
  44. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/gpx_loader.py +0 -0
  45. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/jike_loader.py +0 -0
  46. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/json_loader.py +0 -0
  47. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/kindle_loader.py +0 -0
  48. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/leetcode_loader.py +0 -0
  49. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/multiple_loader.py +0 -0
  50. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/neodb_loader.py +0 -0
  51. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/nrc_loader.py +0 -0
  52. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/ns_loader.py +0 -0
  53. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/openlanguage_loader.py +0 -0
  54. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/shanbay_loader.py +0 -0
  55. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/strava_loader.py +0 -0
  56. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/summary_loader.py +0 -0
  57. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/todoist_loader.py +0 -0
  58. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/wakatime_loader.py +0 -0
  59. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/loader/youtube_loader.py +0 -0
  60. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/skyline/__init__.py +0 -0
  61. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/skyline/config.py +0 -0
  62. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/skyline/font/__init__.py +0 -0
  63. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap/structures.py +0 -0
  64. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/SOURCES.txt +0 -0
  65. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/dependency_links.txt +0 -0
  66. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/entry_points.txt +0 -0
  67. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/requires.txt +0 -0
  68. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/github_heatmap.egg-info/top_level.txt +0 -0
  69. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/setup.cfg +0 -0
  70. {github_heatmap-1.4.0 → github_heatmap-2.2.0}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: github_heatmap
3
- Version: 1.4.0
3
+ Version: 2.2.0
4
4
  Summary: Make everything a GitHub svg poster and Skyline!
5
5
  Home-page: https://github.com/malinkang/GitHubPoster
6
6
  Author: malinkang
@@ -31,3 +31,11 @@ Requires-Dist: stravalib; extra == "all"
31
31
  Requires-Dist: PyGithub; extra == "all"
32
32
  Requires-Dist: sdf_fork; extra == "all"
33
33
  Requires-Dist: pandas; extra == "all"
34
+ Dynamic: author
35
+ Dynamic: author-email
36
+ Dynamic: home-page
37
+ Dynamic: license
38
+ Dynamic: license-file
39
+ Dynamic: provides-extra
40
+ Dynamic: requires-dist
41
+ Dynamic: summary
@@ -435,18 +435,18 @@ github_heatmap nike --nike_refresh_token="your nike_refresh_token" --year 2012-2
435
435
  2. 点击「New integration」添加基础信息后,创建新的 Token
436
436
  3. 提交后可以看到 `Secrets` 下的 `Internal Integration Token`
437
437
 
438
- 获取用于生成 Poster 的 Notion 数据库 ID(database_id),查看[官方文档](https://developers.notion.com/docs/working-with-databases#adding-pages-to-a-database)获取更多信息。
438
+ 获取用于生成 Poster 的 Notion 数据源 ID(`data_source_id`),查看[官方文档](https://developers.notion.com/reference/query-a-data-source)获取更多信息。
439
439
 
440
- 1. 以全屏页面打开数据库
441
- 2. 复制页面链接,链接组成应该是 `https://www.notion.so/{workspace_name}/{database_id}?v={view_id}` 这样的
442
- 3. 其中 `{database_id}` 部分即为数据库 ID
440
+ 1. 在 Notion 中打开对应的数据源页面
441
+ 2. 复制页面链接,链接一般类似 `https://www.notion.so/{workspace_name}/{data_source_id}?v={view_id}`
442
+ 3. 其中 `{data_source_id}` 部分即为数据源 ID
443
443
 
444
- 注:数据库需要添加一个属性类型为 `Date` 的日期属性,该属性的值将作为生成 Poster 的日期数据使用。在生成时需将该日期属性的名称作为选项 `prop_name` 的值,默认值为 `Datetime`
444
+ 注:数据源需要至少包含一个类型为 `Date` 的属性作为日期字段,以及一个数值/公式/rollup 属性作为热力图统计值。生成时通过 `--date_prop_name` `--value_prop_name` 指定字段名。
445
445
 
446
446
  ```
447
- python3 -m github_heatmap notion --notion_token="your notion_token" --database_id="your database_id" --prop_name="your prop_name"
447
+ python3 -m github_heatmap notion --notion_token="your notion_token" --data_source_id="your data_source_id" --date_prop_name="your date_prop_name" --value_prop_name="your value_prop_name"
448
448
  or
449
- github_heatmap notion --notion_token="your notion_token" --database_id="your database_id" --prop_name="your prop_name"
449
+ github_heatmap notion --notion_token="your notion_token" --data_source_id="your data_source_id" --date_prop_name="your date_prop_name" --value_prop_name="your value_prop_name"
450
450
  ```
451
451
 
452
452
  </details>
@@ -738,4 +738,4 @@ python3 -m github_heatmap neodb --neodb_token <token> --mark_type <complete, wis
738
738
 
739
739
  谢谢就够了
740
740
 
741
- Just enjoy it
741
+ Just enjoy it
@@ -7,21 +7,22 @@ import os
7
7
  import sys
8
8
 
9
9
  from github_heatmap.circluar_drawer import CircularDrawer
10
- from github_heatmap.config import TYPE_INFO_DICT
11
- from github_heatmap.drawer import Drawer
12
- from github_heatmap.err import DepNotInstalledError
13
- from github_heatmap.loader import LOADER_DICT
14
- from github_heatmap.poster import Poster
15
- from github_heatmap.utils import parse_years, reduce_year_list
16
10
  from github_heatmap.config import (
17
- HEAD_FONT_SIZE,
18
- YEAR_FONT_SIZE,
19
- MONTH_FONT_SIZE,
20
11
  DOM_BOX_PADING,
21
12
  DOM_BOX_TUPLE,
13
+ GITHUB_LEVEL_COLORS,
14
+ HEAD_FONT_SIZE,
15
+ MARGIN_LEFT,
22
16
  MARGIN_TOP,
23
- MARGIN_LEFT
17
+ MONTH_FONT_SIZE,
18
+ TYPE_INFO_DICT,
19
+ YEAR_FONT_SIZE,
24
20
  )
21
+ from github_heatmap.drawer import Drawer
22
+ from github_heatmap.err import DepNotInstalledError
23
+ from github_heatmap.loader import LOADER_DICT
24
+ from github_heatmap.poster import Poster
25
+ from github_heatmap.utils import build_level_colors, parse_years, reduce_year_list
25
26
 
26
27
  OUT_FOLDER = os.path.join(os.getcwd(), "OUT_FOLDER")
27
28
 
@@ -50,16 +51,32 @@ def run():
50
51
  args = args_parser.parse_args()
51
52
  # without title
52
53
  no_title_types = ("issue", "multiple", "json")
54
+ base_track_color = args.track_color or args.loader.track_color or "#4DD2FF"
55
+ use_exact_github_colors = (
56
+ args.type == "github"
57
+ and not args.track_color
58
+ and not args.special_color1
59
+ and not args.special_color2
60
+ )
61
+ level_colors = (
62
+ list(GITHUB_LEVEL_COLORS)
63
+ if use_exact_github_colors
64
+ else build_level_colors(
65
+ base_track_color, args.special_color1, args.special_color2
66
+ )
67
+ )
53
68
 
54
69
  p.colors = {
55
70
  "background": args.background_color,
56
- "track": args.track_color
57
- or args.loader.track_color, # some type has default color
58
- "special": args.special_color1,
59
- "special2": args.special_color2 or args.special_color,
71
+ "track": base_track_color,
72
+ "special": level_colors[2],
73
+ "special2": level_colors[3],
60
74
  "text": args.text_color,
61
75
  "dom": args.dom_color,
62
76
  }
77
+ p.level_colors = level_colors
78
+ p.use_github_level_mapping = not (args.special_number1 or args.special_number2)
79
+ p.use_raw_level = args.use_raw_level
63
80
 
64
81
  p.tooltip_template = args.tooltip_template or None
65
82
 
@@ -68,6 +85,7 @@ def run():
68
85
  p.colors["track"] = "#025DB8"
69
86
  p.colors["special"] = "#FFD100"
70
87
  p.colors["special2"] = "#FFD100"
88
+ p.level_colors = build_level_colors("#025DB8", "#FFD100", "#FFD100")
71
89
 
72
90
  # set animate
73
91
  p.set_with_animation(args.with_animation)
@@ -111,6 +129,7 @@ def run():
111
129
  else:
112
130
  p.units = args.loader.unit
113
131
  p.tooltip_by_date = getattr(loader, "tooltip_by_date_dict", {}) or {}
132
+ p.level_thresholds = getattr(loader, "level_thresholds", ())
114
133
  p.set_tracks(tracks, years, type_list)
115
134
  else:
116
135
  if args.unit:
@@ -119,6 +138,20 @@ def run():
119
138
  p.units = args.loader.unit
120
139
  p.set_tracks({}, [to_year], type_list)
121
140
  p.tooltip_by_date = {}
141
+ p.level_thresholds_by_type = {
142
+ child_loader._type: getattr(child_loader, "level_thresholds", ())
143
+ for child_loader in loader.loader_list
144
+ }
145
+
146
+ # explicit level thresholds override quartile calculation
147
+ if args.level_thresholds:
148
+ p.level_thresholds = tuple(float(v) for v in args.level_thresholds.split(","))
149
+ p.use_github_level_mapping = True
150
+
151
+ # explicit level colors override computed colors
152
+ if args.level_colors:
153
+ p.level_colors = [c.strip() for c in args.level_colors.split(",")]
154
+ p.use_github_level_mapping = True
122
155
 
123
156
  # set title
124
157
  # we don't know issue content so use name
@@ -144,7 +177,12 @@ def run():
144
177
  MARGIN_TOP
145
178
  + HEAD_FONT_SIZE
146
179
  + poster_length
147
- * (YEAR_FONT_SIZE + MONTH_FONT_SIZE+DOM_BOX_PADING*3 + (DOM_BOX_PADING + DOM_BOX_TUPLE[0]) * 7)
180
+ * (
181
+ YEAR_FONT_SIZE
182
+ + MONTH_FONT_SIZE
183
+ + DOM_BOX_PADING * 3
184
+ + (DOM_BOX_PADING + DOM_BOX_TUPLE[0]) * 7
185
+ )
148
186
  )
149
187
  if not os.path.exists(OUT_FOLDER):
150
188
  os.mkdir(OUT_FOLDER)
@@ -1,4 +1,4 @@
1
- DOM_BOX_TUPLE = (4,4)
1
+ DOM_BOX_TUPLE = (4, 4)
2
2
  DOM_BOX_TUPLE_LIST_FOR_TWO = ((2.6, 1.3), (2.6, 1.3))
3
3
  DOM_BOX_TUPLE_LIST_FOR_THREE = ((2.7, 0.9), (2.7, 0.9), (2.7, 0.9))
4
4
 
@@ -11,7 +11,7 @@ DOM_BOX_DICT = {
11
11
  3: {"dom": DOM_BOX_TUPLE_LIST_FOR_THREE},
12
12
  }
13
13
  MARGIN_LEFT = 10
14
- MARGIN_TOP = 4
14
+ MARGIN_TOP = 4
15
15
  HEAD_FONT_SIZE = 6
16
16
  YEAR_FONT_SIZE = 3
17
17
  MONTH_FONT_SIZE = 2.5
@@ -20,6 +20,7 @@ DOM_BOX_RADIUS = 1
20
20
  DOM_BOX_PADING = 1.4
21
21
 
22
22
  DEFAULT_DOM_COLOR = "#444444"
23
+ GITHUB_LEVEL_COLORS = ("#9BE9A8", "#40C463", "#30A14E", "#216E39")
23
24
  MONTH_NAMES = [
24
25
  "Jan",
25
26
  "Feb",
@@ -7,15 +7,15 @@ from github_heatmap.config import (
7
7
  COLOR_TUPLE,
8
8
  DEFAULT_DOM_COLOR,
9
9
  DOM_BOX_DICT,
10
+ DOM_BOX_PADING,
11
+ DOM_BOX_RADIUS,
10
12
  DOM_BOX_TUPLE,
13
+ MONTH_FONT_SIZE,
11
14
  MONTH_NAMES,
12
15
  YEAR_FONT_SIZE,
13
- MONTH_FONT_SIZE,
14
- DOM_BOX_PADING,
15
- DOM_BOX_RADIUS,
16
16
  )
17
17
  from github_heatmap.err import BaseDrawError
18
- from github_heatmap.utils import interpolate_color, make_key_times
18
+ from github_heatmap.utils import interpolate_color, make_key_times, resolve_github_level
19
19
 
20
20
 
21
21
  class Drawer:
@@ -71,7 +71,21 @@ class Drawer:
71
71
  return f"{date_title} {value} {self.poster.units}"
72
72
  return date_title
73
73
 
74
- def make_color(self, length_range, length):
74
+ def _resolve_level_thresholds(self, type_name=None):
75
+ if type_name and self.poster.level_thresholds_by_type:
76
+ return self.poster.level_thresholds_by_type.get(type_name, ())
77
+ return self.poster.level_thresholds
78
+
79
+ def _make_github_level_color(self, length, type_name=None):
80
+ level = resolve_github_level(length, self._resolve_level_thresholds(type_name), self.poster.use_raw_level)
81
+ if level == 0:
82
+ return self.poster.colors.get("dom")
83
+ return self.poster.level_colors[level - 1]
84
+
85
+ def make_color(self, length_range, length, type_name=None):
86
+ if self.poster.use_github_level_mapping and self.poster.level_colors:
87
+ return self._make_github_level_color(length, type_name)
88
+
75
89
  sp2 = self.poster.special_number.get("special_number2")
76
90
  sp1 = self.poster.special_number.get("special_number1")
77
91
  has_special = False
@@ -122,7 +136,10 @@ class Drawer:
122
136
  color = self.poster.colors.get("dom")
123
137
  if day_tracks:
124
138
  color = self.make_color(self.poster.length_range_by_date, day_tracks)
125
- if day_tracks >= self.poster.special_number["special_number1"]:
139
+ if (
140
+ not self.poster.use_github_level_mapping
141
+ and day_tracks >= self.poster.special_number["special_number1"]
142
+ ):
126
143
  color = self.poster.colors.get("special2") or self.poster.colors.get(
127
144
  "special"
128
145
  )
@@ -171,7 +188,7 @@ class Drawer:
171
188
  if not num:
172
189
  continue
173
190
  dom = dom_tuple[index]
174
- color = self.make_color(length_range, num)
191
+ color = self.make_color(length_range, num, _type)
175
192
  rect = dr.rect(
176
193
  (rect_x, rect_y),
177
194
  dom,
@@ -248,7 +265,11 @@ class Drawer:
248
265
  style=self.month_names_style,
249
266
  )
250
267
  )
251
- if index > 0 and index < 53 and month!=MONTH_NAMES[github_rect_day.month - 1]:
268
+ if (
269
+ index > 0
270
+ and index < 53
271
+ and month != MONTH_NAMES[github_rect_day.month - 1]
272
+ ):
252
273
  month = MONTH_NAMES[github_rect_day.month - 1]
253
274
  dr.add(
254
275
  dr.text(
@@ -308,6 +329,7 @@ class Drawer:
308
329
  "special_number1": loader.special_number1,
309
330
  "special_number2": loader.special_number2,
310
331
  }
332
+ self.poster.level_thresholds = getattr(loader, "level_thresholds", ())
311
333
  self.poster.tooltip_by_date = (
312
334
  getattr(loader, "tooltip_by_date_dict", {}) or {}
313
335
  )
@@ -7,6 +7,7 @@ import pendulum
7
7
  from requests.utils import cookiejar_from_dict
8
8
 
9
9
  from github_heatmap.loader.config import TIME_ZONE
10
+ from github_heatmap.utils import make_github_level_thresholds
10
11
 
11
12
 
12
13
  class LoadError(Exception):
@@ -29,6 +30,7 @@ class BaseLoader(ABC):
29
30
  self.number_by_date_dict = defaultdict(int)
30
31
  self.special_number1 = None
31
32
  self.special_number2 = None
33
+ self.level_thresholds = ()
32
34
  self.number_list = []
33
35
  self.year_list = self._make_years_list()
34
36
  self.try_import_deps()
@@ -51,10 +53,10 @@ class BaseLoader(ABC):
51
53
 
52
54
  def make_special_number(self):
53
55
  """
54
- This func is to make special color number for poster
55
- special_number1 top 20%
56
- special_number2 top 20 % - 50%
56
+ Keep the legacy special-number thresholds for compatibility, and
57
+ also compute GitHub-style quartile thresholds for five-level maps.
57
58
  """
59
+ self.level_thresholds = make_github_level_thresholds(self.number_list)
58
60
  # before python below 3.5 maybe need to sort
59
61
  number_list_set = sorted(list(set(self.number_list)))
60
62
  number_list_set_len = len(number_list_set)
@@ -117,8 +119,8 @@ class BaseLoader(ABC):
117
119
  dest="track_color",
118
120
  metavar="COLOR",
119
121
  type=str,
120
- default="#4DD2FF",
121
- help='Color of tracks (default: "#4DD2FF").',
122
+ default=None,
123
+ help="Base positive-level color (default: loader-specific color).",
122
124
  )
123
125
  group.add_argument(
124
126
  "--dom-color",
@@ -140,15 +142,15 @@ class BaseLoader(ABC):
140
142
  "--special-color1",
141
143
  dest="special_color1",
142
144
  metavar="COLOR",
143
- default="yellow",
144
- help='Special track color (default: "yellow").',
145
+ default=None,
146
+ help="Optional third-level color override for the five-level heatmap.",
145
147
  )
146
148
  group.add_argument(
147
149
  "--special-color2",
148
150
  dest="special_color2",
149
151
  metavar="COLOR",
150
- default="red",
151
- help="Secondary color of special tracks (default: red).",
152
+ default=None,
153
+ help="Optional fourth-level color override for the five-level heatmap.",
152
154
  )
153
155
  group.add_argument(
154
156
  "--special-number1",
@@ -231,6 +233,24 @@ class BaseLoader(ABC):
231
233
  action="store_true",
232
234
  help="if account is CN",
233
235
  )
236
+ group.add_argument(
237
+ "--use-raw-level",
238
+ dest="use_raw_level",
239
+ action="store_true",
240
+ help="Use raw value (1-4) as level directly, skip quartile calculation",
241
+ )
242
+ group.add_argument(
243
+ "--level-thresholds",
244
+ dest="level_thresholds",
245
+ default="",
246
+ help="Explicit level thresholds (comma-separated, e.g. '360,450,99999'), overrides quartile calculation",
247
+ )
248
+ group.add_argument(
249
+ "--level-colors",
250
+ dest="level_colors",
251
+ default="",
252
+ help="Explicit level colors (comma-separated hex, e.g. '#E74C3C,#F1C40F,#2ECC71,#2ECC71'), overrides computed colors",
253
+ )
234
254
  # special here
235
255
  group.add_argument(
236
256
  "--stand-with-ukraine",
@@ -115,8 +115,8 @@ JIKE_PERSON_URL = "https://web.okjike.com/u/{user_id}"
115
115
  BBDC_API_URL = "https://learnywhere.cn/bb/dashboard/profile/search?userId={user_id}"
116
116
 
117
117
  # Notion
118
- NOTION_API_URL = "https://api.notion.com/v1/databases/{database_id}/query"
119
- NOTION_API_VERSION = "2021-08-16"
118
+ NOTION_API_URL = "https://api.notion.com/v1/data_sources/{data_source_id}/query"
119
+ NOTION_API_VERSION = "2026-03-11"
120
120
 
121
121
  # Weread
122
122
  WEREAD_BASE_URL = "https://weread.qq.com/"
@@ -1,6 +1,7 @@
1
- import requests
2
1
  from threading import Thread
3
2
 
3
+ import requests
4
+
4
5
  from github_heatmap.html_parser import GitHubParser
5
6
  from github_heatmap.loader.base_loader import BaseLoader, LoadError
6
7
  from github_heatmap.loader.config import GITHUB_CONTRIBUCTIONS_URL
@@ -1,7 +1,7 @@
1
1
  import time
2
2
 
3
3
  import requests
4
- from pendulum import parse, interval
4
+ from pendulum import interval, parse
5
5
 
6
6
  from github_heatmap.html_parser import GitLabParser
7
7
  from github_heatmap.loader.base_loader import BaseLoader, LoadError
@@ -1,10 +1,12 @@
1
- from datetime import datetime, timedelta
1
+ import argparse
2
+ import json
2
3
  import time
4
+ import warnings
3
5
  from collections import defaultdict
6
+ from datetime import datetime, timedelta
4
7
 
5
8
  import pendulum
6
9
  import requests
7
- import json
8
10
 
9
11
  from github_heatmap.loader.base_loader import BaseLoader
10
12
  from github_heatmap.loader.config import NOTION_API_URL, NOTION_API_VERSION
@@ -17,12 +19,28 @@ class NotionLoader(BaseLoader):
17
19
  def __init__(self, from_year, to_year, _type, **kwargs):
18
20
  super().__init__(from_year, to_year, _type)
19
21
  self.number_by_date_dict = self.generate_date_dict(from_year, to_year)
20
- self.notion_token = kwargs.get("notion_token", "").strip()
21
- self.database_id = kwargs.get("database_id", "").strip()
22
- self.date_prop_name = kwargs.get("date_prop_name", "")
23
- self.value_prop_name = kwargs.get("value_prop_name", "")
24
- self.database_filter = kwargs.get("database_filter", "")
25
- self.tooltip_prop_name = kwargs.get("tooltip_prop_name", "").strip()
22
+ self.notion_token = (kwargs.get("notion_token") or "").strip()
23
+ self.data_source_id = (kwargs.get("data_source_id") or "").strip()
24
+ deprecated_database_id = (kwargs.get("database_id") or "").strip()
25
+ if deprecated_database_id and not self.data_source_id:
26
+ warnings.warn(
27
+ "--database_id is deprecated; use --data_source_id instead.",
28
+ DeprecationWarning,
29
+ stacklevel=2,
30
+ )
31
+ self.data_source_id = deprecated_database_id
32
+ self.date_prop_name = kwargs.get("date_prop_name") or ""
33
+ self.value_prop_name = kwargs.get("value_prop_name") or ""
34
+ self.data_source_filter = kwargs.get("data_source_filter") or ""
35
+ deprecated_database_filter = kwargs.get("database_filter") or ""
36
+ if deprecated_database_filter and not self.data_source_filter:
37
+ warnings.warn(
38
+ "--database_filter is deprecated; use --data_source_filter instead.",
39
+ DeprecationWarning,
40
+ stacklevel=2,
41
+ )
42
+ self.data_source_filter = deprecated_database_filter
43
+ self.tooltip_prop_name = (kwargs.get("tooltip_prop_name") or "").strip()
26
44
  self.tooltip_by_date_dict = defaultdict(list)
27
45
 
28
46
  @classmethod
@@ -33,11 +51,17 @@ class NotionLoader(BaseLoader):
33
51
  type=str,
34
52
  help="The Notion internal integration token.",
35
53
  )
54
+ parser.add_argument(
55
+ "--data_source_id",
56
+ dest="data_source_id",
57
+ type=str,
58
+ help="The Notion data source id.",
59
+ )
36
60
  parser.add_argument(
37
61
  "--database_id",
38
62
  dest="database_id",
39
63
  type=str,
40
- help="The Notion database id.",
64
+ help=argparse.SUPPRESS,
41
65
  )
42
66
  parser.add_argument(
43
67
  "--date_prop_name",
@@ -45,7 +69,7 @@ class NotionLoader(BaseLoader):
45
69
  type=str,
46
70
  default="Datetime",
47
71
  required=optional,
48
- help="The database property name which stored the datetime.",
72
+ help="The Notion data source property name that stores the date.",
49
73
  )
50
74
  parser.add_argument(
51
75
  "--value_prop_name",
@@ -53,15 +77,23 @@ class NotionLoader(BaseLoader):
53
77
  type=str,
54
78
  default="Datetime",
55
79
  required=optional,
56
- help="The database property name which stored the datetime.",
57
- )
80
+ help="The Notion data source property name used as the heatmap value.",
81
+ )
82
+ parser.add_argument(
83
+ "--data_source_filter",
84
+ dest="data_source_filter",
85
+ type=str,
86
+ default="",
87
+ required=False,
88
+ help="Optional Notion data source query filter in JSON format.",
89
+ )
58
90
  parser.add_argument(
59
91
  "--database_filter",
60
92
  dest="database_filter",
61
93
  type=str,
62
94
  default="",
63
95
  required=False,
64
- help="The database property name which stored the datetime.",
96
+ help=argparse.SUPPRESS,
65
97
  )
66
98
  parser.add_argument(
67
99
  "--tooltip_prop_name",
@@ -90,27 +122,28 @@ class NotionLoader(BaseLoader):
90
122
  ]
91
123
  },
92
124
  }
93
- if self.database_filter:
94
- payload["filter"]["and"].append(json.loads(self.database_filter))
125
+ if self.data_source_filter:
126
+ payload["filter"]["and"].append(json.loads(self.data_source_filter))
95
127
  if start_cursor:
96
128
  payload.update({"start_cursor": start_cursor})
97
-
129
+ print(payload)
98
130
  headers = {
99
131
  "Accept": "application/json",
100
132
  "Notion-Version": NOTION_API_VERSION,
101
133
  "Content-Type": "application/json",
102
134
  "Authorization": "Bearer " + self.notion_token,
103
135
  }
104
-
136
+ print(headers)
105
137
  try:
106
138
  resp = requests.post(
107
- NOTION_API_URL.format(database_id=self.database_id),
139
+ NOTION_API_URL.format(data_source_id=self.data_source_id),
108
140
  json=payload,
109
141
  headers=headers,
110
142
  )
111
143
  except requests.RequestException:
144
+ print("Failed to connect to Notion API.")
112
145
  return data_list
113
-
146
+ print(getattr(resp, "text", ""))
114
147
  if not resp.ok:
115
148
  # Treat non-OK responses as an empty result set so we can still draw
116
149
  # a heatmap even when the Notion API yields no rows for the period.
@@ -119,6 +152,7 @@ class NotionLoader(BaseLoader):
119
152
  data = resp.json()
120
153
  except ValueError:
121
154
  return data_list
155
+ print(len(data.get("results", [])))
122
156
  results = data["results"]
123
157
  next_cursor = data["next_cursor"]
124
158
  data_list.extend(results)
@@ -209,7 +243,9 @@ class NotionLoader(BaseLoader):
209
243
  [p.get("name") or p.get("id", "") for p in item.get("people", [])]
210
244
  ).strip()
211
245
  if item_type == "relation":
212
- return ", ".join([r.get("id", "") for r in item.get("relation", [])]).strip()
246
+ return ", ".join(
247
+ [r.get("id", "") for r in item.get("relation", [])]
248
+ ).strip()
213
249
  if item_type == "date":
214
250
  return cls._format_date(item.get("date", {}))
215
251
  if item_type in ("url", "email", "phone_number"):
@@ -272,7 +308,9 @@ class NotionLoader(BaseLoader):
272
308
  if prop_type == "date":
273
309
  return cls._format_date(prop.get("date", {}))
274
310
  if prop_type == "relation":
275
- return ", ".join([r.get("id", "") for r in prop.get("relation", [])]).strip()
311
+ return ", ".join(
312
+ [r.get("id", "") for r in prop.get("relation", [])]
313
+ ).strip()
276
314
  if prop_type == "files":
277
315
  texts = []
278
316
  for file_obj in prop.get("files", []):
@@ -308,7 +346,9 @@ class NotionLoader(BaseLoader):
308
346
  if rollup_type == "date":
309
347
  return cls._format_date(rollup.get("date", {}))
310
348
  if rollup_type == "array":
311
- texts = [cls._extract_rollup_item(item) for item in rollup.get("array", [])]
349
+ texts = [
350
+ cls._extract_rollup_item(item) for item in rollup.get("array", [])
351
+ ]
312
352
  texts = [text for text in texts if text]
313
353
  return ", ".join(texts)
314
354
  return ""
@@ -1,22 +1,25 @@
1
1
  import json
2
+ import os
3
+
2
4
  import pendulum
3
5
  import requests
4
- import os
5
6
 
6
7
  from github_heatmap.loader.base_loader import BaseLoader
7
8
  from github_heatmap.loader.config import WEREAD_BASE_URL, WEREAD_HISTORY_URL
8
9
 
9
10
  headers = {
10
- 'User-Agent': "WeRead/8.2.5 WRBrand/xiaomi Dalvik/2.1.0 (Linux; U; Android 12; Redmi Note 7 Pro Build/SQ3A.220705.004)",
11
- 'Connection': "Keep-Alive",
12
- 'Accept-Encoding': "gzip",
13
- 'baseapi': "32",
14
- 'appver': "8.2.5.10163885",
15
- 'osver': "12",
16
- 'channelId': "11",
17
- 'basever': "8.2.5.10163885",
18
- 'Content-Type': "application/json; charset=UTF-8"
11
+ "User-Agent": "WeRead/8.2.5 WRBrand/xiaomi Dalvik/2.1.0 (Linux; U; Android 12; Redmi Note 7 Pro Build/SQ3A.220705.004)",
12
+ "Connection": "Keep-Alive",
13
+ "Accept-Encoding": "gzip",
14
+ "baseapi": "32",
15
+ "appver": "8.2.5.10163885",
16
+ "osver": "12",
17
+ "channelId": "11",
18
+ "basever": "8.2.5.10163885",
19
+ "Content-Type": "application/json; charset=UTF-8",
19
20
  }
21
+
22
+
20
23
  class WereadLoader(BaseLoader):
21
24
  track_color = "#2EA8F7"
22
25
  unit = "mins"
@@ -45,7 +48,11 @@ class WereadLoader(BaseLoader):
45
48
  # )
46
49
 
47
50
  def refresh_token(self):
48
- body = {"deviceId":self.device_id ,"refreshToken":self.token,"activationCode":self.activation_code}
51
+ body = {
52
+ "deviceId": self.device_id,
53
+ "refreshToken": self.token,
54
+ "activationCode": self.activation_code,
55
+ }
49
56
  r = self.session.post(
50
57
  "https://api.notionhub.app/refresh-weread-token", json=body
51
58
  )
@@ -54,13 +61,14 @@ class WereadLoader(BaseLoader):
54
61
  vid = response_data.get("vid")
55
62
  accessToken = response_data.get("accessToken")
56
63
  if vid and accessToken:
57
- self.session.headers.update({"vid": str(vid), "accessToken": accessToken})
64
+ self.session.headers.update(
65
+ {"vid": str(vid), "accessToken": accessToken}
66
+ )
58
67
  else:
59
68
  print("Failed to refresh token")
60
69
  else:
61
70
  print("Failed to refresh token")
62
-
63
-
71
+
64
72
  def get_api_data(self):
65
73
  r = self.session.get(WEREAD_HISTORY_URL)
66
74
  if not r.ok:
@@ -75,7 +83,7 @@ class WereadLoader(BaseLoader):
75
83
 
76
84
  def make_track_dict(self):
77
85
  api_data = self.get_api_data()
78
- if("readTimes" in api_data):
86
+ if "readTimes" in api_data:
79
87
  readTimes = dict(sorted(api_data["readTimes"].items(), reverse=True))
80
88
  for k, v in readTimes.items():
81
89
  k = pendulum.from_timestamp(int(k), tz=self.time_zone)
@@ -1,14 +1,12 @@
1
1
  """Create a poster from track data."""
2
+
2
3
  from collections import defaultdict
3
4
 
4
5
  import svgwrite
5
6
 
7
+ from github_heatmap.config import HEAD_FONT_SIZE, MARGIN_LEFT, MARGIN_TOP
6
8
  from github_heatmap.structures import XY, ValueRange
7
- from github_heatmap.config import (
8
- HEAD_FONT_SIZE,
9
- MARGIN_LEFT,
10
- MARGIN_TOP
11
- )
9
+
12
10
 
13
11
  class Poster:
14
12
  def __init__(self):
@@ -28,7 +26,7 @@ class Poster:
28
26
  self.width = 400
29
27
  self.height = 300
30
28
  self.years = None
31
- self.offset = XY(MARGIN_LEFT,MARGIN_TOP)
29
+ self.offset = XY(MARGIN_LEFT, MARGIN_TOP)
32
30
  # maybe support more type
33
31
  self.tracks_drawer = None
34
32
  self.trans = None
@@ -36,6 +34,11 @@ class Poster:
36
34
  self.animation_time = 10
37
35
  self.year_tracks_date_count_dict = defaultdict(int)
38
36
  self.year_tracks_type_dict = defaultdict(dict)
37
+ self.level_colors = []
38
+ self.level_thresholds = ()
39
+ self.level_thresholds_by_type = {}
40
+ self.use_github_level_mapping = True
41
+ self.use_raw_level = False
39
42
 
40
43
  # for year summary
41
44
  self.is_summary = False
@@ -103,10 +106,17 @@ class Poster:
103
106
  def __draw_header(self, d):
104
107
  self.offset.y += HEAD_FONT_SIZE
105
108
  text_color = self.colors["text"]
106
- title_style = f"font-size:{HEAD_FONT_SIZE}px; font-family:Arial; font-weight:bold;"
107
- d.add(d.text(self.title, insert=(self.offset.x, self.offset.y), fill=text_color, style=title_style))
108
-
109
-
109
+ title_style = (
110
+ f"font-size:{HEAD_FONT_SIZE}px; font-family:Arial; font-weight:bold;"
111
+ )
112
+ d.add(
113
+ d.text(
114
+ self.title,
115
+ insert=(self.offset.x, self.offset.y),
116
+ fill=text_color,
117
+ style=title_style,
118
+ )
119
+ )
110
120
 
111
121
  def __draw_footer(self, d):
112
122
  self.tracks_drawer.draw_footer(d)
@@ -2,7 +2,7 @@ import math
2
2
  import os
3
3
 
4
4
  import numpy as np
5
- from pendulum import parse, interval
5
+ from pendulum import interval, parse
6
6
  from sdf import X, Y, box, ease, measure_text, rectangle, text, union
7
7
 
8
8
  import github_heatmap.skyline
@@ -1,3 +1,4 @@
1
+ import math
1
2
  import re
2
3
  from itertools import count as itercount
3
4
  from itertools import takewhile
@@ -20,6 +21,21 @@ def interpolate_color(color1, color2, ratio):
20
21
  return c3.hex_l
21
22
 
22
23
 
24
+ def build_level_colors(track_color, special_color1=None, special_color2=None):
25
+ """
26
+ Build four positive contribution shades for a five-level heatmap.
27
+ """
28
+ level1 = interpolate_color(track_color, "#ffffff", 0.35)
29
+ if special_color1 and special_color2:
30
+ level2 = interpolate_color(track_color, special_color1, 0.35)
31
+ return [level1, level2, special_color1, special_color2]
32
+
33
+ level2 = track_color
34
+ level3 = interpolate_color(track_color, "#000000", 0.18)
35
+ level4 = interpolate_color(track_color, "#000000", 0.35)
36
+ return [level1, level2, level3, level4]
37
+
38
+
23
39
  def parse_years(s):
24
40
  """Parse a plaintext range of years into a pair of years
25
41
 
@@ -59,6 +75,43 @@ def make_key_times(year_count):
59
75
  return [str(round(i, 2)) for i in s]
60
76
 
61
77
 
78
+ def make_github_level_thresholds(number_list):
79
+ """
80
+ GitHub exposes contribution levels as NONE plus four quartiles.
81
+ This uses the non-zero daily values and a nearest-rank percentile.
82
+ """
83
+ positive_values = sorted(v for v in number_list if v > 0)
84
+ if not positive_values:
85
+ return ()
86
+
87
+ total = len(positive_values)
88
+
89
+ def nearest_rank(percentile):
90
+ index = max(0, math.ceil(total * percentile) - 1)
91
+ return positive_values[index]
92
+
93
+ return tuple(nearest_rank(p) for p in (0.25, 0.50, 0.75))
94
+
95
+
96
+ def resolve_github_level(value, thresholds, use_raw_level=False):
97
+ if value <= 0:
98
+ return 0
99
+ if use_raw_level:
100
+ # 直接使用传入的值作为级别(1-4),跳过四分位数计算
101
+ return max(1, min(4, int(value)))
102
+ if not thresholds:
103
+ return 1
104
+
105
+ q1, q2, q3 = thresholds
106
+ if value <= q1:
107
+ return 1
108
+ if value <= q2:
109
+ return 2
110
+ if value <= q3:
111
+ return 3
112
+ return 4
113
+
114
+
62
115
  def reduce_year_list(year_list, tracks_dict):
63
116
  """
64
117
  format year list
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: github_heatmap
3
- Version: 1.4.0
3
+ Version: 2.2.0
4
4
  Summary: Make everything a GitHub svg poster and Skyline!
5
5
  Home-page: https://github.com/malinkang/GitHubPoster
6
6
  Author: malinkang
@@ -31,3 +31,11 @@ Requires-Dist: stravalib; extra == "all"
31
31
  Requires-Dist: PyGithub; extra == "all"
32
32
  Requires-Dist: sdf_fork; extra == "all"
33
33
  Requires-Dist: pandas; extra == "all"
34
+ Dynamic: author
35
+ Dynamic: author-email
36
+ Dynamic: home-page
37
+ Dynamic: license
38
+ Dynamic: license-file
39
+ Dynamic: provides-extra
40
+ Dynamic: requires-dist
41
+ Dynamic: summary
@@ -0,0 +1,6 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [tool.isort]
6
+ profile = "black"
@@ -6,7 +6,7 @@ setup(
6
6
  author_email="linkang.ma@gmail.com",
7
7
  url="https://github.com/malinkang/GitHubPoster",
8
8
  license="MIT",
9
- version="1.4.0",
9
+ version="2.2.0",
10
10
  description="Make everything a GitHub svg poster and Skyline!",
11
11
  packages=find_packages(),
12
12
  include_package_data=True,
@@ -1,3 +1,4 @@
1
+ import pytest
1
2
  import requests
2
3
 
3
4
  from github_heatmap.loader import notion_loader
@@ -10,7 +11,7 @@ def build_loader():
10
11
  2024,
11
12
  "notion",
12
13
  notion_token="token",
13
- database_id="db",
14
+ data_source_id="ds",
14
15
  date_prop_name="Date",
15
16
  value_prop_name="Value",
16
17
  tooltip_prop_name="Tooltip",
@@ -46,10 +47,7 @@ def test_notion_loader_make_track_dict_with_tooltips():
46
47
  loader.make_track_dict()
47
48
 
48
49
  assert loader.number_by_date_dict["2024-01-01"] == 7
49
- assert (
50
- loader.tooltip_by_date_dict["2024-01-01"]
51
- == "Study\nRead, Write"
52
- )
50
+ assert loader.tooltip_by_date_dict["2024-01-01"] == "Study\nRead, Write"
53
51
 
54
52
 
55
53
  def test_notion_loader_extract_property_text_variants():
@@ -133,3 +131,44 @@ def test_notion_loader_handles_request_exception(monkeypatch):
133
131
  assert tracks
134
132
  assert all(value == 0 for value in tracks.values())
135
133
  assert loader.number_list
134
+
135
+
136
+ def test_notion_loader_queries_data_source_endpoint(monkeypatch):
137
+ loader = build_loader()
138
+ captured = {}
139
+
140
+ class DummyResp:
141
+ ok = True
142
+
143
+ @staticmethod
144
+ def json():
145
+ return {"results": [], "next_cursor": None, "has_more": False}
146
+
147
+ def fake_post(url, json, headers):
148
+ captured["url"] = url
149
+ captured["json"] = json
150
+ captured["headers"] = headers
151
+ return DummyResp()
152
+
153
+ monkeypatch.setattr(notion_loader.requests, "post", fake_post)
154
+
155
+ loader.get_api_data()
156
+
157
+ assert captured["url"].endswith("/v1/data_sources/ds/query")
158
+ assert captured["headers"]["Notion-Version"] == "2026-03-11"
159
+ assert captured["json"]["filter"]["and"][0]["property"] == "Date"
160
+
161
+
162
+ def test_notion_loader_accepts_deprecated_database_id():
163
+ with pytest.warns(DeprecationWarning, match="--database_id is deprecated"):
164
+ loader = NotionLoader(
165
+ 2024,
166
+ 2024,
167
+ "notion",
168
+ notion_token="token",
169
+ database_id="legacy-db",
170
+ date_prop_name="Date",
171
+ value_prop_name="Value",
172
+ )
173
+
174
+ assert loader.data_source_id == "legacy-db"
@@ -0,0 +1,91 @@
1
+ from github_heatmap.config import GITHUB_LEVEL_COLORS
2
+ from github_heatmap.drawer import Drawer
3
+ from github_heatmap.poster import Poster
4
+ from github_heatmap.utils import (
5
+ build_level_colors,
6
+ interpolate_color,
7
+ make_github_level_thresholds,
8
+ make_key_times,
9
+ parse_years,
10
+ resolve_github_level,
11
+ )
12
+
13
+
14
+ def test_interpolate_color():
15
+ assert interpolate_color("#000000", "#ffffff", 0) == "#000000"
16
+ assert interpolate_color("#000000", "#ffffff", 1) == "#ffffff"
17
+ assert interpolate_color("#000000", "#ffffff", 0.5) == "#7f7f7f"
18
+ assert interpolate_color("#000000", "#ffffff", -100) == "#000000"
19
+ assert interpolate_color("#000000", "#ffffff", 12345) == "#ffffff"
20
+
21
+
22
+ def test_parse_years():
23
+ assert parse_years("2012") == (2012, 2012)
24
+ assert parse_years("2015-2021") == (2015, 2021)
25
+ assert parse_years("2021-2015") == (2015, 2021)
26
+
27
+
28
+ def test_make_key_times():
29
+ assert make_key_times(5) == ["0", "0.2", "0.4", "0.6", "0.8", "1"]
30
+
31
+
32
+ def test_make_github_level_thresholds():
33
+ assert make_github_level_thresholds([]) == ()
34
+ assert make_github_level_thresholds([0, 1, 1, 2, 3, 5, 8, 13]) == (1, 3, 8)
35
+
36
+
37
+ def test_resolve_github_level():
38
+ thresholds = (2, 5, 9)
39
+
40
+ assert resolve_github_level(0, thresholds) == 0
41
+ assert resolve_github_level(2, thresholds) == 1
42
+ assert resolve_github_level(5, thresholds) == 2
43
+ assert resolve_github_level(9, thresholds) == 3
44
+ assert resolve_github_level(10, thresholds) == 4
45
+
46
+
47
+ def test_build_level_colors_uses_exact_palette_when_provided():
48
+ assert list(GITHUB_LEVEL_COLORS) == [
49
+ "#9BE9A8",
50
+ "#40C463",
51
+ "#30A14E",
52
+ "#216E39",
53
+ ]
54
+ assert len(build_level_colors("#40C463")) == 4
55
+
56
+
57
+ def test_drawer_tooltip_formatting():
58
+ poster = Poster()
59
+ poster.units = "XP"
60
+ drawer = Drawer(poster)
61
+
62
+ assert drawer._format_tooltip("2024-01-01", 10) == "2024-01-01 10 XP"
63
+ assert drawer._format_tooltip("2024-01-01") == "2024-01-01"
64
+
65
+ poster.tooltip_template = "{date}: {value}{unit}"
66
+ assert drawer._format_tooltip("2024-01-01", 5) == "2024-01-01: 5XP"
67
+ assert drawer._format_tooltip("2024-01-01") == "2024-01-01"
68
+
69
+ poster.tooltip_template = "{date} {value} for {type}"
70
+ assert drawer._format_tooltip("2024-01-01", 7, "run") == "2024-01-01 7 for run"
71
+
72
+ poster.tooltip_by_date = {"2024-01-01": "Custom"}
73
+ assert drawer._format_tooltip("2024-01-01", 5) == "Custom"
74
+
75
+ poster.tooltip_by_date = {"2024-01-02": {"run": "Run note"}}
76
+ assert drawer._format_tooltip("2024-01-02", 3, "run") == "Run note"
77
+
78
+
79
+ def test_drawer_make_color_uses_github_levels():
80
+ poster = Poster()
81
+ poster.use_github_level_mapping = True
82
+ poster.level_colors = list(GITHUB_LEVEL_COLORS)
83
+ poster.level_thresholds = (2, 5, 9)
84
+ poster.colors["dom"] = "#ebedf0"
85
+ drawer = Drawer(poster)
86
+
87
+ assert drawer.make_color(poster.length_range_by_date, 0) == "#ebedf0"
88
+ assert drawer.make_color(poster.length_range_by_date, 2) == "#9BE9A8"
89
+ assert drawer.make_color(poster.length_range_by_date, 5) == "#40C463"
90
+ assert drawer.make_color(poster.length_range_by_date, 9) == "#30A14E"
91
+ assert drawer.make_color(poster.length_range_by_date, 12) == "#216E39"
@@ -1,2 +0,0 @@
1
- [tool.isort]
2
- profile = "black"
@@ -1,46 +0,0 @@
1
- from github_heatmap.drawer import Drawer
2
- from github_heatmap.poster import Poster
3
- from github_heatmap.utils import interpolate_color, make_key_times, parse_years
4
-
5
-
6
- def test_interpolate_color():
7
- assert interpolate_color("#000000", "#ffffff", 0) == "#000000"
8
- assert interpolate_color("#000000", "#ffffff", 1) == "#ffffff"
9
- assert interpolate_color("#000000", "#ffffff", 0.5) == "#7f7f7f"
10
- assert interpolate_color("#000000", "#ffffff", -100) == "#000000"
11
- assert interpolate_color("#000000", "#ffffff", 12345) == "#ffffff"
12
-
13
-
14
- def test_parse_years():
15
- assert parse_years("2012") == (2012, 2012)
16
- assert parse_years("2015-2021") == (2015, 2021)
17
- assert parse_years("2021-2015") == (2015, 2021)
18
-
19
-
20
- def test_make_key_times():
21
- assert make_key_times(5) == ["0", "0.2", "0.4", "0.6", "0.8", "1"]
22
-
23
-
24
- def test_drawer_tooltip_formatting():
25
- poster = Poster()
26
- poster.units = "XP"
27
- drawer = Drawer(poster)
28
-
29
- assert drawer._format_tooltip("2024-01-01", 10) == "2024-01-01 10 XP"
30
- assert drawer._format_tooltip("2024-01-01") == "2024-01-01"
31
-
32
- poster.tooltip_template = "{date}: {value}{unit}"
33
- assert drawer._format_tooltip("2024-01-01", 5) == "2024-01-01: 5XP"
34
- assert drawer._format_tooltip("2024-01-01") == "2024-01-01"
35
-
36
- poster.tooltip_template = "{date} {value} for {type}"
37
- assert (
38
- drawer._format_tooltip("2024-01-01", 7, "run")
39
- == "2024-01-01 7 for run"
40
- )
41
-
42
- poster.tooltip_by_date = {"2024-01-01": "Custom"}
43
- assert drawer._format_tooltip("2024-01-01", 5) == "Custom"
44
-
45
- poster.tooltip_by_date = {"2024-01-02": {"run": "Run note"}}
46
- assert drawer._format_tooltip("2024-01-02", 3, "run") == "Run note"
File without changes
File without changes