涉及日期时间的 Pygal 图会抛出日期早于 1970 年的错误

Pygal plots involving datetime throw error with dates prior to 1970

在 pygal 中尝试 sample code 涉及日期时间或日期的 XY 图,1970 年之前的任何日期都会导致此回溯:

Traceback (most recent call last):
  File "C:/Users/***/dt_test.py", line 30, in <module>
    datetimeline.render()
  File "C:\Python\Python36\lib\site-packages\pygal\graph\public.py", line 52, 
in render
    self.setup(**kwargs)
  File "C:\Python\Python36\lib\site-packages\pygal\graph\base.py", line 217, 
in setup
self._draw()
  File "C:\Python\Python36\lib\site-packages\pygal\graph\graph.py", line 924, 
in _draw
    self._compute_x_labels()
  File "C:\Python\Python36\lib\site-packages\pygal\graph\dual.py", line 61, 
in _compute_x_labels
    self._x_labels = list(zip(map(self._x_format, x_pos), x_pos))
  File "C:\Python\Python36\lib\site-packages\pygal\graph\time.py", line 103, 
in datetime_to_str
    dt = datetime.utcfromtimestamp(x)
OSError: [Errno 22] Invalid argument

其他人有这种行为吗? (我正在使用 PyCharm。) l也许 'millennium' 返回了一个意外的负数?

(编辑) 我用了"date"下的代码,运行下的PyCharm:

from datetime import datetime
datetimeline = pygal.DateTimeLine(
    x_label_rotation=35, truncate_label=-1,
    x_value_formatter=lambda dt: dt.strftime('%d, %b %Y at %I:%M:%S %p'))
datetimeline.add("Serie", [
    (datetime(2013, 1, 2, 12, 0), 300),
    (datetime(2013, 1, 12, 14, 30, 45), 412),
    (datetime(2013, 2, 2, 6), 823),
    (datetime(2013, 2, 22, 9, 45), 672)
])
datetimeline.render()

...当我将“2013”​​更改为“1969”时,我得到了上面显示的 Traceback。

这是由 python 用于某些日期和时间处理的基础 C 函数的限制引起的。这些功能在不同平台上的实现方式不同,这就是为什么它不会影响所有人(我不得不借用一个 Windows 框来复制错误)。

docs for datetime.utcfromtimestamp 提到了这些限制:

This may raise OverflowError, if the timestamp is out of the range of values supported by the platform C gmtime() function, and OSError on gmtime() failure. It’s common for this to be restricted to years in 1970 through 2038.

幸运的是,相同的文档提出了一种解决方法,并且由于 DateTimeLine class 中包含麻烦行的函数非常短,我们可以轻松创建自己的 class继承自 DateTimeLine 并覆盖函数。

import pygal
from datetime import datetime, timedelta, timezone

class MyDateTimeLine(pygal.DateTimeLine):

    @property
    def _x_format(self):
        def datetime_to_str(x):
            dt = datetime(1970, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=x)
            return self.x_value_formatter(dt)
        return datetime_to_str

这几乎是从 source code for DateTimeLine 复制而来的。唯一的变化是为 dt.

赋值的行

我们现在可以使用这个 class 作为 DateTimeLine 的替代品来绘制 2013 年或 1969 年的日期:

formatter = lambda dt: dt.strftime("%d, %b %Y at %I:%M:%S %p")

chart = MyDateTimeLine(x_label_rotation=35, truncate_label=-1,
                       x_value_formatter=formatter, width=640, height=300)
chart.add("2013", [(datetime(2013, 1, 2, 12, 0), 300),
                   (datetime(2013, 1, 12, 14, 30, 45), 412),
                   (datetime(2013, 2, 2, 6), 823),
                   (datetime(2013, 2, 22, 9, 45), 672)])
chart.render_to_png("chart2013.png")

chart = MyDateTimeLine(x_label_rotation=35, truncate_label=-1,
                       x_value_formatter=formatter, width=640, height=300)
chart.add("1969", [(datetime(1969, 1, 2, 12, 0), 300),
                   (datetime(1969, 1, 12, 14, 30, 45), 412),
                   (datetime(1969, 2, 2, 6), 823),
                   (datetime(1969, 2, 22, 9, 45), 672)])
chart.render_to_png("chart1969.png")