generate_chart.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. #!/usr/bin/env python
  2. # Copyright 2020 Espressif Systems (Shanghai) PTE LTD
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. #
  16. import argparse
  17. import datetime as dt
  18. import json
  19. import matplotlib.dates
  20. import matplotlib.patches as mpatches
  21. import matplotlib.pyplot as plt
  22. import numpy as np
  23. import requests
  24. from dateutil import parser
  25. from dateutil.relativedelta import relativedelta
  26. from matplotlib.dates import MONTHLY, DateFormatter, RRuleLocator, rrulewrapper
  27. class Version(object):
  28. def __init__(self, version_name, explicit_start_date, explicit_end_date, explicit_end_service_date=None):
  29. self.version_name = version_name
  30. self._start_date = parser.parse(explicit_start_date)
  31. self._end_of_life_date = parser.parse(explicit_end_date)
  32. self._end_service_date = parser.parse(
  33. explicit_end_service_date) if explicit_end_service_date is not None else self.compute_end_service_date()
  34. self.start_date_matplotlib_format = matplotlib.dates.date2num(self._start_date)
  35. self.end_of_life_date_matplotlib_format = matplotlib.dates.date2num(self._end_of_life_date)
  36. self.end_service_date_matplotlib_format = matplotlib.dates.date2num(self._end_service_date)
  37. @staticmethod
  38. def add_months(source_date, months):
  39. return source_date + relativedelta(months=+months)
  40. def get_start_date(self):
  41. return self._start_date
  42. def get_end_of_life_date(self):
  43. return self._end_of_life_date
  44. def get_end_service_date(self):
  45. return self._end_service_date
  46. def compute_end_service_date(self):
  47. return self.add_months(self._start_date, 12)
  48. class ChartVersions(object):
  49. def __init__(self, url=None, filename=None):
  50. self._releases = self._get_releases_from_url(url=url, filename=filename)
  51. self.sorted_releases_supported = sorted(self.filter_old_versions(self._releases), key=lambda x: x.version_name,
  52. reverse=True)
  53. def get_releases_as_json(self):
  54. return {
  55. x.version_name: {
  56. 'start_date': x.get_start_date().strftime('%Y-%m-%d'),
  57. 'end_service': x.get_end_service_date().strftime('%Y-%m-%d'),
  58. 'end_date': x.get_end_of_life_date().strftime('%Y-%m-%d')
  59. } for x in self.sorted_releases_supported
  60. }
  61. @staticmethod
  62. def parse_chart_releases_from_js(js_as_string):
  63. return json.loads(js_as_string[js_as_string.find('RELEASES: ') + len('RELEASES: '):js_as_string.rfind('};')])
  64. def _get_all_version_from_url(self, url=None, filename=None):
  65. releases_file = requests.get(url).text if url is not None else ''.join(open(filename).readlines())
  66. return self.parse_chart_releases_from_js(releases_file)
  67. def _get_releases_from_url(self, url=None, filename=None):
  68. all_versions = self._get_all_version_from_url(url, filename)
  69. return [
  70. Version(version_name=x,
  71. explicit_start_date=all_versions[x]['start_date'],
  72. explicit_end_date=all_versions[x]['end_date'] if 'end_date' in all_versions[x].keys() else None,
  73. explicit_end_service_date=all_versions[x]['end_service'] if 'end_service' in all_versions[
  74. x].keys() else None)
  75. for x in all_versions.keys()
  76. ]
  77. @staticmethod
  78. def filter_old_versions(versions):
  79. return list(
  80. filter(lambda x: x.get_end_of_life_date() >= dt.datetime.now(x.get_end_of_life_date().tzinfo), versions))
  81. @staticmethod
  82. def months_timedelta(datetime_1, datetime2):
  83. datetime_1, datetime2 = (datetime2, datetime_1) if datetime_1 > datetime2 else (datetime_1, datetime2)
  84. return (datetime2.year * 12 + datetime2.month) - (datetime_1.year * 12 + datetime_1.month)
  85. @staticmethod
  86. def find_next_multiple_of_power_two(number, initial=3):
  87. """
  88. Computes the next multiple of the number by some power of two.
  89. >>> ChartVersions.find_next_multiple_of_power_two(7, 3)
  90. 12
  91. """
  92. msb = number.bit_length()
  93. return 3 if number <= 1 else initial << msb - 2 << (1 & number >> msb - 2)
  94. def find_nearest_multiple_of_power_two(self, number, initial=3, prefer_next=False):
  95. next_num = self.find_next_multiple_of_power_two(number=number - 1, initial=initial)
  96. previous_num = next_num >> 1
  97. return next_num if abs(next_num - number) < (abs(previous_num - number) + int(prefer_next)) else previous_num
  98. def create_chart(self,
  99. figure_size=(41.8330013267, 16.7332005307),
  100. subplot=111,
  101. step_size=0.5,
  102. bar_height=0.3,
  103. version_alpha=0.8,
  104. lts_service_color='darkred',
  105. lts_maintenance_color='red',
  106. bar_align='center',
  107. date_interval=None,
  108. output_chart_name='docs/chart',
  109. output_chart_extension='.png',
  110. months_surrounding_chart=4,
  111. service_period_label='Service period (Recommended for new designs)',
  112. maintenance_period_text='Maintenance period'):
  113. fig = plt.figure(figsize=figure_size)
  114. ax = fig.add_subplot(subplot)
  115. labels_count = len(self.sorted_releases_supported)
  116. pos = np.arange(step_size, labels_count * step_size + step_size, step_size)
  117. for release, i in zip(self.sorted_releases_supported, range(labels_count)):
  118. start_date = release.start_date_matplotlib_format
  119. end_of_service_date = release.end_service_date_matplotlib_format
  120. end_date = release.end_of_life_date_matplotlib_format
  121. ax.barh((i * step_size) + step_size, (end_of_service_date or end_date) - start_date, left=start_date,
  122. height=bar_height, align=bar_align,
  123. color=lts_service_color,
  124. alpha=version_alpha,
  125. edgecolor=lts_service_color)
  126. if end_of_service_date is not None:
  127. ax.barh((i * step_size) + step_size, end_date - end_of_service_date, left=end_of_service_date,
  128. height=bar_height, align=bar_align,
  129. color=lts_maintenance_color, alpha=version_alpha, edgecolor=lts_maintenance_color)
  130. ax.set_ylim(bottom=0, ymax=labels_count * step_size + step_size)
  131. max_ax_date = Version.add_months(
  132. max(self.sorted_releases_supported,
  133. key=lambda version: version.get_end_of_life_date().replace(tzinfo=None)).get_end_of_life_date(),
  134. months_surrounding_chart + 1).replace(day=1)
  135. min_ax_date = Version.add_months(
  136. min(self.sorted_releases_supported,
  137. key=lambda version: version.get_start_date().replace(tzinfo=None)).get_start_date(),
  138. -months_surrounding_chart).replace(day=1)
  139. x_ax_interval = date_interval or self.find_nearest_multiple_of_power_two(
  140. self.months_timedelta(max_ax_date, min_ax_date) // 10)
  141. ax.set_xlim(xmin=min_ax_date, xmax=max_ax_date)
  142. ax.grid(color='g', linestyle=':')
  143. ax.xaxis_date()
  144. rule = rrulewrapper(MONTHLY, interval=x_ax_interval)
  145. loc = RRuleLocator(rule)
  146. formatter = DateFormatter('%b %Y')
  147. ax.xaxis.set_major_locator(loc)
  148. ax.xaxis.set_major_formatter(formatter)
  149. x_labels = ax.get_xticklabels()
  150. plt.ylabel('ESP-IDF Release', size=12)
  151. ax.invert_yaxis()
  152. fig.autofmt_xdate()
  153. darkred_patch = mpatches.Patch(color=lts_service_color, label=service_period_label)
  154. red_patch = mpatches.Patch(color=lts_maintenance_color, label=maintenance_period_text)
  155. plt.setp(plt.yticks(pos, map(lambda x: x.version_name, self.sorted_releases_supported))[1], rotation=0,
  156. fontsize=10, family='Tahoma')
  157. plt.setp(x_labels, rotation=30, fontsize=11, family='Tahoma')
  158. plt.legend(handles=[darkred_patch, red_patch], prop={'size': 10, 'family': 'Tahoma'},
  159. bbox_to_anchor=(1.01, 1.165), loc='upper right')
  160. fig.set_size_inches(11, 5, forward=True)
  161. plt.savefig(output_chart_name + output_chart_extension, bbox_inches='tight')
  162. print('Saved into ' + output_chart_name + output_chart_extension)
  163. if __name__ == '__main__':
  164. arg_parser = argparse.ArgumentParser(
  165. description='Create chart of version support. Set the url or filename with versions.'
  166. 'If you set both filename and url the script will prefer filename.')
  167. arg_parser.add_argument('--url', metavar='URL', default='https://dl.espressif.com/dl/esp-idf/idf_versions.js')
  168. arg_parser.add_argument('--filename',
  169. help='Set the name of the source file, if is set, the script ignores the url.')
  170. arg_parser.add_argument('--output-format', help='Set the output format of the image.', default='svg')
  171. arg_parser.add_argument('--output-file', help='Set the name of the output file.', default='docs/chart')
  172. args = arg_parser.parse_args()
  173. ChartVersions(url=args.url if args.filename is None else None, filename=args.filename).create_chart(
  174. output_chart_extension='.' + args.output_format.lower()[-3:], output_chart_name=args.output_file)