newsworthycharts 1.71.1__tar.gz → 1.71.4__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 (53) hide show
  1. {newsworthycharts-1.71.1/newsworthycharts.egg-info → newsworthycharts-1.71.4}/PKG-INFO +27 -4
  2. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/README.rst +23 -0
  3. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/__init__.py +1 -1
  4. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/categoricalchart.py +5 -1
  5. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/chart.py +28 -46
  6. newsworthycharts-1.71.4/newsworthycharts/rankchart.py +173 -0
  7. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/scatterplot.py +7 -2
  8. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/serialchart.py +44 -5
  9. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4/newsworthycharts.egg-info}/PKG-INFO +27 -4
  10. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts.egg-info/requires.txt +2 -2
  11. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/setup.py +2 -2
  12. newsworthycharts-1.71.1/newsworthycharts/rankchart.py +0 -40
  13. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/LICENSE.txt +0 -0
  14. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/MANIFEST.in +0 -0
  15. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/bubblemap.py +0 -0
  16. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/choroplethmap.py +0 -0
  17. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/custom/__init__.py +0 -0
  18. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/custom/climate_cars.py +0 -0
  19. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/datawrapper.py +0 -0
  20. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/lib/__init__.py +0 -0
  21. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/lib/color_fn.py +0 -0
  22. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/lib/colors.py +0 -0
  23. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/lib/datalist.py +0 -0
  24. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/lib/formatter.py +0 -0
  25. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/lib/geography.py +0 -0
  26. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/lib/locator.py +0 -0
  27. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/lib/mimetypes.py +0 -0
  28. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/lib/utils.py +0 -0
  29. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/map.py +0 -0
  30. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/maps/se-4.gpkg +0 -0
  31. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/maps/se-7.gpkg +0 -0
  32. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/rangeplot.py +0 -0
  33. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/rc/newsworthy +0 -0
  34. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/seasonalchart.py +0 -0
  35. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/storage.py +0 -0
  36. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/stripechart.py +0 -0
  37. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/translations/datawrapper_regions.csv +0 -0
  38. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/translations/regions.py +0 -0
  39. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts/translations/se_municipalities.csv +0 -0
  40. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts.egg-info/SOURCES.txt +0 -0
  41. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts.egg-info/dependency_links.txt +0 -0
  42. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts.egg-info/not-zip-safe +0 -0
  43. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/newsworthycharts.egg-info/top_level.txt +0 -0
  44. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/setup.cfg +0 -0
  45. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/test/test_categorical_chart.py +0 -0
  46. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/test/test_choropleth_maps.py +0 -0
  47. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/test/test_custom_climate_cars.py +0 -0
  48. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/test/test_data_list.py +0 -0
  49. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/test/test_datawrapper.py +0 -0
  50. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/test/test_main.py +0 -0
  51. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/test/test_rangeplot.py +0 -0
  52. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/test/test_scatterplot.py +0 -0
  53. {newsworthycharts-1.71.1 → newsworthycharts-1.71.4}/test/test_serial_chart.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: newsworthycharts
3
- Version: 1.71.1
3
+ Version: 1.71.4
4
4
  Summary: Matplotlib wrapper to create charts and publish them on Amazon S3
5
5
  Home-page: https://github.com/jplusplus/newsworthycharts
6
- Download-URL: https://github.com/jplusplus/newsworthycharts/archive/1.71.1.tar.gz
6
+ Download-URL: https://github.com/jplusplus/newsworthycharts/archive/1.71.4.tar.gz
7
7
  Author: Jens Finnäs and Leo Wallentin, J++ Stockholm
8
8
  Author-email: stockholm@jplusplus.org
9
9
  License: MIT
@@ -15,13 +15,13 @@ Requires-Dist: matplotlib==3.10.0
15
15
  Requires-Dist: langcodes>=3.3
16
16
  Requires-Dist: Babel<3,>=2.14.0
17
17
  Requires-Dist: PyYAML>=3
18
- Requires-Dist: adjustText==0.7.3
18
+ Requires-Dist: adjustText==1.3.0
19
19
  Requires-Dist: numpy>2
20
20
  Requires-Dist: python-dateutil<3,>=2
21
21
  Requires-Dist: Pillow==11.1.0
22
22
  Requires-Dist: requests>=2.22
23
23
  Requires-Dist: matplotlib-label-lines==0.5.1
24
- Requires-Dist: geopandas>=1
24
+ Requires-Dist: geopandas==1.0.1
25
25
  Requires-Dist: mapclassify==2.8.1
26
26
 
27
27
  This module contains methods for producing graphs and publishing them on Amazon S3, or in the location of your choice.
@@ -150,6 +150,7 @@ These settings are available for all chart types:
150
150
  - logo = None # Path to image that will be embedded in the caption area. Can also be set though a style property
151
151
  - color_fn = None # Custom coloring function
152
152
  - legend_title = None # Title for the legend
153
+ - revert_value_axis = False # Revert the value axis to put 0 top
153
154
 
154
155
  **SerialChart**
155
156
 
@@ -269,6 +270,28 @@ Roadmap
269
270
  Changelog
270
271
  ---------
271
272
 
273
+ - 1.71.4
274
+
275
+ - BumpChart: Use dual colors in line markers when rank is shared
276
+
277
+ - 1.71.3
278
+
279
+ - Revert 1.71.2 changes to rendering, to make file sizes predictable again
280
+
281
+ - 1.71.2
282
+
283
+ - Annotates all isolated value sequences in BumpChart
284
+ - Allows shared ranks in BumpChart
285
+ - Prefers showing all ticks in BumpChart
286
+ - Fixes some padding issues with reverted value axis
287
+ - Adds `revert_value_axis` option for all charts
288
+ - New custom label collision algorithm for serial charts
289
+ - Removes unused(?) label collision algorithm for categorical charts
290
+ - Upgrades adjustText (now used in ScatterPlot only) to 1.3.0
291
+ - Adds `_after_add_data()` hook for subclasses and extensions
292
+ - Pins Geopandas version (currently 1.0.1)
293
+ - Smaller vertical annotation offset (partially reverting 1.71.1)
294
+
272
295
  - 1.71.1
273
296
 
274
297
  - Allow setting line marker size and style in `BumpChart`
@@ -124,6 +124,7 @@ These settings are available for all chart types:
124
124
  - logo = None # Path to image that will be embedded in the caption area. Can also be set though a style property
125
125
  - color_fn = None # Custom coloring function
126
126
  - legend_title = None # Title for the legend
127
+ - revert_value_axis = False # Revert the value axis to put 0 top
127
128
 
128
129
  **SerialChart**
129
130
 
@@ -243,6 +244,28 @@ Roadmap
243
244
  Changelog
244
245
  ---------
245
246
 
247
+ - 1.71.4
248
+
249
+ - BumpChart: Use dual colors in line markers when rank is shared
250
+
251
+ - 1.71.3
252
+
253
+ - Revert 1.71.2 changes to rendering, to make file sizes predictable again
254
+
255
+ - 1.71.2
256
+
257
+ - Annotates all isolated value sequences in BumpChart
258
+ - Allows shared ranks in BumpChart
259
+ - Prefers showing all ticks in BumpChart
260
+ - Fixes some padding issues with reverted value axis
261
+ - Adds `revert_value_axis` option for all charts
262
+ - New custom label collision algorithm for serial charts
263
+ - Removes unused(?) label collision algorithm for categorical charts
264
+ - Upgrades adjustText (now used in ScatterPlot only) to 1.3.0
265
+ - Adds `_after_add_data()` hook for subclasses and extensions
266
+ - Pins Geopandas version (currently 1.0.1)
267
+ - Smaller vertical annotation offset (partially reverting 1.71.1)
268
+
246
269
  - 1.71.1
247
270
 
248
271
  - Allow setting line marker size and style in `BumpChart`
@@ -1,4 +1,4 @@
1
- __version__ = "1.71.1"
1
+ __version__ = "1.71.4"
2
2
 
3
3
  from .chart import Chart
4
4
  from .choroplethmap import ChoroplethMap
@@ -1,6 +1,6 @@
1
1
  from .chart import Chart
2
2
  from .lib.utils import to_float
3
- from adjustText import adjust_text
3
+ # from adjustText import adjust_text
4
4
  import numpy as np
5
5
 
6
6
 
@@ -251,11 +251,15 @@ class CategoricalChart(Chart):
251
251
  )
252
252
  if idx > 0:
253
253
  texts += [*_texts]
254
+ """
254
255
  adjust_text(
255
256
  texts,
256
257
  ax=self.ax,
257
258
  autoalign="x" if self.bar_orientation == "horizontal" else "y",
259
+ only_move="x" if self.bar_orientation == "horizontal" else "y",
260
+ # will replace autoalign in newer versions
258
261
  )
262
+ """
259
263
 
260
264
 
261
265
  class CategoricalChartWithReference(CategoricalChart):
@@ -242,11 +242,11 @@ class Chart(object):
242
242
  elif direction == "left":
243
243
  opts["verticalalignment"] = "center"
244
244
  opts["horizontalalignment"] = "right"
245
- opts["xytext"] = (-offset, -self._nwc_style["annotation.fontsize"] / 0.75)
245
+ opts["xytext"] = (-offset, -self._nwc_style["annotation.fontsize"] / 4)
246
246
  elif direction == "right":
247
247
  opts["verticalalignment"] = "center"
248
248
  opts["horizontalalignment"] = "left"
249
- opts["xytext"] = (offset, -self._nwc_style["annotation.fontsize"] / 0.75)
249
+ opts["xytext"] = (offset, -self._nwc_style["annotation.fontsize"] / 4)
250
250
  else:
251
251
  msg = f"'{direction}' is an unknown direction for an annotation"
252
252
  raise Exception(msg)
@@ -255,7 +255,7 @@ class Chart(object):
255
255
  opts.update(kwargs)
256
256
 
257
257
  ann = self.ax.annotate(text, xy=xy, **opts)
258
- # ann = self.ax.text(text, xy[0], xy[1])
258
+ # ann = self.ax.text(text, xy[0], xy[1], **opts)
259
259
  self._annotations.append(ann)
260
260
 
261
261
  return ann
@@ -343,6 +343,13 @@ class Chart(object):
343
343
  pass
344
344
  # raise NotImplementedError("This method should be overridden")
345
345
 
346
+ def _after_add_data(self):
347
+ """ Extra operations after data has been added.
348
+ Typically used by subclasses and extensions
349
+ """
350
+ pass
351
+ # raise NotImplementedError("This method should be overridden")
352
+
346
353
  def _mark_broken_axis(self, axis="y"):
347
354
  """Adds a symbol to mark that an axis is broken
348
355
  """
@@ -477,6 +484,8 @@ class Chart(object):
477
484
  # Apply all changes, in the correct order for consistent rendering
478
485
  if len(self.data):
479
486
  self._add_data()
487
+ # Extra hook for extensions and subclasses
488
+ self._after_add_data()
480
489
 
481
490
  # Calculate size in inches
482
491
  # Until 1.45 we did this on init, but now we'd like to enable dynamic heights
@@ -673,7 +682,6 @@ class Chart(object):
673
682
  + self._footer_rel_height
674
683
  )
675
684
  # print(sub_canvas_height, self._note_rel_height, self._footer_rel_height)
676
- self._fig.subplots_adjust(bottom=sub_canvas_height)
677
685
 
678
686
  if self.revert_value_axis:
679
687
  if hasattr(self, "orientation") and self.orientation == "horizontal":
@@ -687,6 +695,7 @@ class Chart(object):
687
695
  value_ticks = [x for x in value_ticks if x <= self.data.max_val and x >= self.ymin]
688
696
  value_axis.set_ticks(value_ticks)
689
697
  # self.ax.set_ylim(self.data.max_val, self.ymin)
698
+ self._fig.subplots_adjust(bottom=sub_canvas_height)
690
699
 
691
700
  @classmethod
692
701
  def init_from(cls, args: dict, storage=LocalStorage(),
@@ -724,7 +733,7 @@ class Chart(object):
724
733
  chart.ticks = args["ticks"]
725
734
  return chart
726
735
 
727
- def render(
736
+ def _render(
728
737
  self,
729
738
  key: str,
730
739
  img_format: str,
@@ -733,9 +742,6 @@ class Chart(object):
733
742
  storage_options: dict={}
734
743
  ):
735
744
  """Render file, and send to storage."""
736
- # Apply all changes, in the correct order for consistent rendering
737
- self._apply_changes_before_rendering(factor=factor, transparent=transparent)
738
-
739
745
  # Save plot in memory, to write it directly to storage
740
746
  buf = BytesIO()
741
747
  args = {
@@ -775,6 +781,19 @@ class Chart(object):
775
781
  buf.seek(0)
776
782
  self._storage.save(key, buf, img_format, storage_options)
777
783
 
784
+ def render(
785
+ self,
786
+ key: str,
787
+ img_format: str,
788
+ transparent: bool=False,
789
+ factor: int=1,
790
+ storage_options: dict={}
791
+ ):
792
+ """Render file, and send to storage."""
793
+ # Apply all changes, in the correct order for consistent rendering
794
+ self._apply_changes_before_rendering(factor=factor, transparent=transparent)
795
+ self._render(key, img_format, transparent, factor, storage_options)
796
+
778
797
  def render_all(self, key: str, transparent=False, factor=1, storage_options={}):
779
798
  """
780
799
  Render all available formats
@@ -785,44 +804,7 @@ class Chart(object):
785
804
  for file_format in self.file_types:
786
805
  if file_format == "dw":
787
806
  continue
788
-
789
- # Save plot in memory, to write it directly to storage
790
- buf = BytesIO()
791
- args = {
792
- 'format': file_format,
793
- 'transparent': transparent,
794
- 'dpi': self._fig.dpi * factor,
795
- }
796
- if file_format == "pdf":
797
- args["metadata"] = {
798
- 'Creator': "Newsworthy",
799
- 'Producer': f"NWCharts {__version__}",
800
- }
801
- elif file_format == "png":
802
- args["metadata"] = {
803
- 'Author': "Newsworthy",
804
- 'Software': f"NWCharts {__version__}",
805
- }
806
- elif file_format == "svg":
807
- args["metadata"] = {
808
- 'Publisher': "Newsworthy",
809
- 'Creator': f"NWCharts {__version__}",
810
- }
811
- elif file_format in ["jpg", "jpeg"]:
812
- args["pil_kwargs"] = {
813
- "quality": 100,
814
- "optimize": True,
815
- }
816
- """
817
- Not yet implemented in Pillow
818
- args["metadata"] = {
819
- 'Publisher': "Newsworthy",
820
- 'Creator': f"NWCharts {__version__}",
821
- }
822
- """
823
- self._fig.savefig(buf, **args) # bbox_inches="tight")
824
- buf.seek(0)
825
- self._storage.save(key, buf, file_format, storage_options)
807
+ self._render(key, file_format, transparent, factor, storage_options)
826
808
 
827
809
  @property
828
810
  def title(self):
@@ -0,0 +1,173 @@
1
+ """
2
+ A chart showing ranking over time (like ”most popular baby names”)
3
+ """
4
+ from .serialchart import SerialChart
5
+ from .lib.utils import to_date
6
+ import numpy as np
7
+
8
+
9
+ class BumpChart(SerialChart):
10
+ """Plot a rank chart
11
+
12
+ Data should be a list of iterables of (rank, date string) tuples, eg:
13
+ `[ [("2010-01-01", 2), ("2011-01-01", 3)] ]`, combined with a list of
14
+ labels in the same order
15
+ """
16
+
17
+ def __init__(self, *args, **kwargs):
18
+ super(BumpChart, self).__init__(*args, **kwargs)
19
+
20
+ if self.line_width is None:
21
+ self.line_width = 0.9
22
+ if self.line_marker_size is None:
23
+ self.line_marker_size = 6
24
+ self.label_placement = 'none'
25
+ self.type = "line"
26
+ self.decimals = 0
27
+ self.revert_value_axis = True
28
+ self.ymin = 1
29
+ self.allow_broken_y_axis = False
30
+ self.grid = False
31
+ self.accentuate_baseline = False
32
+
33
+ self.line_marker = "o-"
34
+
35
+ def _get_line_colors(self, i, *args):
36
+ if not self.data:
37
+ # Don't waste time
38
+ return None
39
+ if self.highlight and self.highlight in self.labels and i == self.labels.index(self.highlight):
40
+ return self._nwc_style["strong_color"]
41
+ elif self.colors and i < len(self.colors):
42
+ return self.colors[i]
43
+ return self._nwc_style["neutral_color"]
44
+
45
+ """
46
+ def _get_marker_fill(self, i):
47
+ pass
48
+ """
49
+
50
+ def _after_add_data(self):
51
+ # Print out every rank
52
+ if self.data.max_val < 30:
53
+ _range = list(range(1, int(self.data.max_val) + 1))
54
+ self.ax.yaxis.set_ticks(_range, _range)
55
+
56
+ # Recolor markers with more than one line passing through
57
+ # (MPL does not allow access to individual markers, so we'll overwrite them)
58
+
59
+ for date in self.data.x_points:
60
+ value_colors = {}
61
+ for i, serie in enumerate(self.data):
62
+ values = np.array(self.serie_values[i], dtype=np.float64)
63
+ dates = [x[0] for x in serie]
64
+ color = self._get_line_colors(i)
65
+ if date not in dates:
66
+ continue
67
+ idx = dates.index(date)
68
+ if np.isnan(values[idx]):
69
+ continue
70
+ val = int(values[idx])
71
+ if val not in value_colors:
72
+ value_colors[val] = []
73
+ value_colors[val].append(color)
74
+ for val, colors in value_colors.items():
75
+ if len(colors) < 2:
76
+ continue
77
+ elif len(colors) == 2:
78
+ self.ax.plot(
79
+ to_date(date),
80
+ val,
81
+ self.line_marker,
82
+ markersize=self.line_marker_size,
83
+ color="None",
84
+ fillstyle="left",
85
+ markeredgewidth=0,
86
+ markerfacecolor=colors[0],
87
+ markerfacecoloralt=colors[1],
88
+ zorder=100,
89
+ )
90
+ else:
91
+ self.ax.plot(
92
+ to_date(date),
93
+ val,
94
+ self.line_marker,
95
+ markersize=self.line_marker_size,
96
+ color=self._nwc_style["neutral_color"],
97
+ markeredgewidth=0,
98
+ zorder=100,
99
+ )
100
+ # Add labels
101
+ slots_occupied = {
102
+ to_date(k): [] for k in self.data.x_points
103
+ }
104
+ # distance between rank ticks
105
+ # y1 = self.ax.transData.transform((0, 1))
106
+ # y2 = self.ax.transData.transform((0, 2))
107
+ # dist = abs(y1[1] - y2[1])
108
+ for i, serie in enumerate(self.data):
109
+ values = np.array(self.serie_values[i], dtype=np.float64)
110
+ dates = [to_date(x[0]) for x in serie]
111
+ color = self._get_line_colors(i)
112
+
113
+ endpoints = [
114
+ (d, values[idx])
115
+ for (idx, d) in enumerate(dates) if idx == len(dates) - 1 or np.isnan(values[idx + 1])
116
+ ]
117
+ for ep in endpoints:
118
+ position = ep[1]
119
+ while position in slots_occupied[ep[0]]:
120
+ position += 1
121
+ # pos_diff = position - ep[1]
122
+ slots_occupied[ep[0]].append(position)
123
+ self._annotate_point(
124
+ self.labels[i],
125
+ # (ep[0], ep[1]),
126
+ (ep[0], position),
127
+ "right",
128
+ offset=15,
129
+ color=color,
130
+ va="center",
131
+ # xytext=(15, -abs(y1[1] - y2[1]) * pos_diff),
132
+ # arrowprops=dict(arrowstyle="->", color=color) if pos_diff > 1 else None,
133
+ )
134
+ """
135
+ labels = []
136
+ for i, serie in enumerate(self.data):
137
+ values = np.array(self.serie_values[i], dtype=np.float64)
138
+ dates = [to_date(x[0]) for x in serie]
139
+ color = self._get_line_colors(i)
140
+
141
+ endpoints = [
142
+ (d, values[idx])
143
+ for (idx, d) in enumerate(dates) if idx == len(dates) - 1 or np.isnan(values[idx + 1])
144
+ ]
145
+ for ep in endpoints:
146
+ lbl = self._annotate_point(
147
+ self.labels[i],
148
+ (ep[0], ep[1]),
149
+ "right",
150
+ offset=15,
151
+ color=color,
152
+ va="center",
153
+ # arrowprops=dict(arrowstyle="->", color=color),
154
+ )
155
+ loops = 0
156
+ overlap = True if len(labels) > 0 else False
157
+ while overlap:
158
+ for i, bb in enumerate(labels):
159
+ if i == len(labels) - 1:
160
+ overlap = False
161
+ break
162
+ bbox1 = lbl.get_window_extent()
163
+ bbox2 = labels[i].get_window_extent()
164
+ print(bbox1, bbox2)
165
+ if bbox1.y1 < bbox2.y0 + 10 and bbox1.x1 > bbox2.x0 + 5: # allow for some overlap
166
+ xy1 = lbl.xyann
167
+ lbl.xyann = (xy1[0], xy1[1] + 1)
168
+ break
169
+ loops += 1
170
+ if loops > 500:
171
+ break
172
+ labels.append(lbl)
173
+ """
@@ -114,8 +114,13 @@ class ScatterPlot(Chart):
114
114
 
115
115
  # These settings could be fine-tuned
116
116
  # Weren't able to add lines between points and labels for example
117
- adjust_text(self._annotations, ax=self.ax, autoalign="y",
118
- expand_points=(1, 1),)
117
+ adjust_text(
118
+ self._annotations,
119
+ ax=self.ax,
120
+ autoalign="y",
121
+ only_move="y", # replacing autoalign i newer versions
122
+ expand_points=(1, 1),
123
+ )
119
124
 
120
125
  if self.ymin is not None:
121
126
  self.ax.set_ylim(self.ymin)
@@ -4,7 +4,7 @@ from .lib.utils import to_float, to_date, guess_date_interval
4
4
  import numpy as np
5
5
  from math import inf
6
6
  from dateutil.relativedelta import relativedelta
7
- from adjustText import adjust_text
7
+ # from adjustText import adjust_text
8
8
  from labellines import labelLines
9
9
 
10
10
 
@@ -179,6 +179,7 @@ class SerialChart(Chart):
179
179
  # Replace None values with 0's to be able to plot bars
180
180
  _values = [0 if v is None else v for v in _values]
181
181
  serie_values.append(_values)
182
+ self.serie_values = serie_values
182
183
 
183
184
  # Select a date to highlight
184
185
  highlight_date = None
@@ -253,6 +254,9 @@ class SerialChart(Chart):
253
254
  color = self._nwc_style["strong_color"]
254
255
  else:
255
256
  color = self._nwc_style["neutral_color"]
257
+ marker_fill = None
258
+ if hasattr(self, "_get_marker_fill"):
259
+ marker_fill = self._get_marker_fill(i)
256
260
 
257
261
  line, = self.ax.plot(
258
262
  dates,
@@ -260,6 +264,8 @@ class SerialChart(Chart):
260
264
  self.line_marker,
261
265
  markersize=self.line_marker_size,
262
266
  color=color,
267
+ markerfacecolor=marker_fill,
268
+ markeredgewidth=0,
263
269
  zorder=zo,
264
270
  lw=lw,
265
271
  )
@@ -306,6 +312,7 @@ class SerialChart(Chart):
306
312
  offset=15,
307
313
  color=color,
308
314
  va="center",
315
+ # arrowprops=dict(arrowstyle="->", color=color),
309
316
  )
310
317
  # store labels to check for overlap later
311
318
  line_label_elems.append(lbl)
@@ -575,8 +582,11 @@ class SerialChart(Chart):
575
582
 
576
583
  padding_top = ymax * 0.15
577
584
 
578
- self.ax.set_ylim(ymin=ymin - padding_bottom,
579
- ymax=ymax + padding_top)
585
+ if not self.revert_value_axis:
586
+ self.ax.set_ylim(
587
+ ymin=ymin - padding_bottom,
588
+ ymax=ymax + padding_top,
589
+ )
580
590
 
581
591
  self.ax.yaxis.set_major_formatter(y_formatter)
582
592
  self.ax.yaxis.grid(self.grid)
@@ -676,12 +686,13 @@ class SerialChart(Chart):
676
686
  )
677
687
 
678
688
  def _adust_texts_vertically(self, elements, ha="left"):
689
+ """
690
+ from adjustText import get_bboxes
679
691
  if len(elements) == 2:
680
692
  # Hack: check for overlap and adjust labels only
681
693
  # if such overlap exist.
682
694
  # `adjust_text` tended to offset labels unnecessarily
683
695
  # but it might just be that I haven't worked out how to use it properly
684
- from adjustText import get_bboxes
685
696
  bb1, bb2 = get_bboxes(elements, self._fig.canvas.get_renderer(), (1.0, 1.0), self.ax)
686
697
  if (
687
698
  # first label is above
@@ -692,4 +703,32 @@ class SerialChart(Chart):
692
703
  adjust_text(elements, autoalign="y", ha=ha)
693
704
 
694
705
  else:
695
- adjust_text(elements, autoalign="y", ha=ha)
706
+ adjust_text(
707
+ elements,
708
+ autoalign="y",
709
+ only_move="y", # will replace autoalign in newer versions
710
+ ax=self.ax,
711
+ max_move=(0, 10), # (10, 10) is default
712
+ )
713
+ """
714
+ overlap = True
715
+ loops = 0
716
+ while overlap:
717
+ for i, bb in enumerate(elements):
718
+ if i == len(elements) - 1:
719
+ overlap = False
720
+ break
721
+ bbox1 = elements[i].get_window_extent()
722
+ bbox2 = elements[i + 1].get_window_extent()
723
+ if bbox1.y1 > bbox2.y0 + 10 and bbox1.x1 > bbox2.x0 + 5: # allow for some overlap
724
+ loops += 1
725
+ xy1 = elements[i].xyann
726
+ # xy2 = elements[i + 1].xyann
727
+ # elements[i].update_positions((bbox1.x0, bbox1.y0 - 5))
728
+ # elements[i].update_positions((xy1[0], xy1[1] - 0.02))
729
+ elements[i].xyann = (xy1[0], xy1[1] - 0.01)
730
+ # elements[i].set(arrowprops=dict(arrowstyle="->"))
731
+ # elements[i + 1].xyann = (xy2[0], xy2[1] + 0.005)
732
+ break
733
+ if loops > 5_000:
734
+ break
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: newsworthycharts
3
- Version: 1.71.1
3
+ Version: 1.71.4
4
4
  Summary: Matplotlib wrapper to create charts and publish them on Amazon S3
5
5
  Home-page: https://github.com/jplusplus/newsworthycharts
6
- Download-URL: https://github.com/jplusplus/newsworthycharts/archive/1.71.1.tar.gz
6
+ Download-URL: https://github.com/jplusplus/newsworthycharts/archive/1.71.4.tar.gz
7
7
  Author: Jens Finnäs and Leo Wallentin, J++ Stockholm
8
8
  Author-email: stockholm@jplusplus.org
9
9
  License: MIT
@@ -15,13 +15,13 @@ Requires-Dist: matplotlib==3.10.0
15
15
  Requires-Dist: langcodes>=3.3
16
16
  Requires-Dist: Babel<3,>=2.14.0
17
17
  Requires-Dist: PyYAML>=3
18
- Requires-Dist: adjustText==0.7.3
18
+ Requires-Dist: adjustText==1.3.0
19
19
  Requires-Dist: numpy>2
20
20
  Requires-Dist: python-dateutil<3,>=2
21
21
  Requires-Dist: Pillow==11.1.0
22
22
  Requires-Dist: requests>=2.22
23
23
  Requires-Dist: matplotlib-label-lines==0.5.1
24
- Requires-Dist: geopandas>=1
24
+ Requires-Dist: geopandas==1.0.1
25
25
  Requires-Dist: mapclassify==2.8.1
26
26
 
27
27
  This module contains methods for producing graphs and publishing them on Amazon S3, or in the location of your choice.
@@ -150,6 +150,7 @@ These settings are available for all chart types:
150
150
  - logo = None # Path to image that will be embedded in the caption area. Can also be set though a style property
151
151
  - color_fn = None # Custom coloring function
152
152
  - legend_title = None # Title for the legend
153
+ - revert_value_axis = False # Revert the value axis to put 0 top
153
154
 
154
155
  **SerialChart**
155
156
 
@@ -269,6 +270,28 @@ Roadmap
269
270
  Changelog
270
271
  ---------
271
272
 
273
+ - 1.71.4
274
+
275
+ - BumpChart: Use dual colors in line markers when rank is shared
276
+
277
+ - 1.71.3
278
+
279
+ - Revert 1.71.2 changes to rendering, to make file sizes predictable again
280
+
281
+ - 1.71.2
282
+
283
+ - Annotates all isolated value sequences in BumpChart
284
+ - Allows shared ranks in BumpChart
285
+ - Prefers showing all ticks in BumpChart
286
+ - Fixes some padding issues with reverted value axis
287
+ - Adds `revert_value_axis` option for all charts
288
+ - New custom label collision algorithm for serial charts
289
+ - Removes unused(?) label collision algorithm for categorical charts
290
+ - Upgrades adjustText (now used in ScatterPlot only) to 1.3.0
291
+ - Adds `_after_add_data()` hook for subclasses and extensions
292
+ - Pins Geopandas version (currently 1.0.1)
293
+ - Smaller vertical annotation offset (partially reverting 1.71.1)
294
+
272
295
  - 1.71.1
273
296
 
274
297
  - Allow setting line marker size and style in `BumpChart`
@@ -3,11 +3,11 @@ matplotlib==3.10.0
3
3
  langcodes>=3.3
4
4
  Babel<3,>=2.14.0
5
5
  PyYAML>=3
6
- adjustText==0.7.3
6
+ adjustText==1.3.0
7
7
  numpy>2
8
8
  python-dateutil<3,>=2
9
9
  Pillow==11.1.0
10
10
  requests>=2.22
11
11
  matplotlib-label-lines==0.5.1
12
- geopandas>=1
12
+ geopandas==1.0.1
13
13
  mapclassify==2.8.1
@@ -29,13 +29,13 @@ setup(
29
29
  "langcodes>=3.3",
30
30
  "Babel>=2.14.0,<3",
31
31
  "PyYAML>=3",
32
- "adjustText==0.7.3",
32
+ "adjustText==1.3.0",
33
33
  "numpy>2",
34
34
  "python-dateutil>=2,<3",
35
35
  "Pillow==11.1.0",
36
36
  "requests>=2.22",
37
37
  "matplotlib-label-lines==0.5.1",
38
- "geopandas>=1",
38
+ "geopandas==1.0.1",
39
39
  "mapclassify==2.8.1",
40
40
  ],
41
41
  setup_requires=["flake8"],
@@ -1,40 +0,0 @@
1
- """
2
- A chart showing ranking over time (like ”most popular baby names”)
3
- """
4
- from .serialchart import SerialChart
5
-
6
-
7
- class BumpChart(SerialChart):
8
- """Plot a rank chart
9
-
10
- Data should be a list of iterables of (rank, date string) tuples, eg:
11
- `[ [("2010-01-01", 2), ("2011-01-01", 3)] ]`, combined with a list of
12
- labels in the same order
13
- """
14
-
15
- def __init__(self, *args, **kwargs):
16
- super(BumpChart, self).__init__(*args, **kwargs)
17
-
18
- if self.line_width is None:
19
- self.line_width = 0.9
20
- self.label_placement = 'line'
21
- self.type = "line"
22
- self.decimals = 0
23
- self.revert_value_axis = True
24
- self.ymin = 1
25
- self.allow_broken_y_axis = False
26
- self.grid = False
27
- self.accentuate_baseline = False
28
-
29
- self.line_marker = "o-"
30
- self.line_marker_size = 5
31
-
32
- def _get_line_colors(self, i, *args):
33
- if not self.data:
34
- # Don't waste time
35
- return None
36
- if self.highlight and self.highlight in self.labels and i == self.labels.index(self.highlight):
37
- return self._nwc_style["strong_color"]
38
- elif self.colors and i < len(self.colors):
39
- return self.colors[i]
40
- return self._nwc_style["neutral_color"]