11from ._exceptions import ValidationError
22import matplotlib .dates as mdates
3+ import matplotlib .cbook as cbook
4+ from datetime import datetime
35import numpy as np
46
57def _locate_channel_dtype (chart , channel ):
@@ -110,24 +112,74 @@ def _locate_channel_axis(chart, channel):
110112 return {}
111113
112114def _convert_to_mpl_date (data ):
113- """Converts datetime, datetime64, strings, and Altair DateTime objects to matplotlib dates"""
114-
115- # TODO: parse both single values and sequences/iterables
116- new_data = []
117- for i in data :
118- if isinstance (i , str ): # string format for dates
119- new_data .append (mdates .datestr2num (i ))
120- elif isinstance (i , np .datetime64 ): # sequence of datetimes, datetime64s
121- new_data .append (mdates .date2num (i ))
122- elif isinstance (i , dict ): # Altair DateTime
123- """Allowed formats (for domain):
124- YYYY,
125- YYYY-MM(-01), YYYY-MM-DD, YYYY(-01)-DD,
126- ^ plus hh, hh:mm, hh:mm:ss, hh(:00):ss, (0):mm:ss
127- Could turn dict into iso datetime string and then use dateutil.parser.isoparse() or datestr2num()
128- """
129- raise NotImplementedError
115+ """Converts datetime, datetime64, strings, and Altair DateTime objects to Matplotlib dates.
116+
117+ Parameters
118+ ----------
119+ data
120+ The data to be converted to a Matplotlib date.
121+
122+ Returns
123+ -------
124+ new_data : list
125+ A list containing the converted date(s).
126+ """
127+
128+ if cbook .iterable (data ) and not isinstance (data , str ) and not isinstance (data , dict ):
129+ if len (data ) == 0 :
130+ return []
131+ else :
132+ return [_convert_to_mpl_date (i ) for i in data ]
133+ else :
134+ if isinstance (data , str ): # string format for dates
135+ data = mdates .datestr2num (data )
136+ elif isinstance (data , np .datetime64 ): # sequence of datetimes, datetime64s
137+ data = mdates .date2num (data )
138+ elif isinstance (data , dict ): # Altair DateTime
139+ data = mdates .date2num (_altair_DateTime_to_datetime (data ))
130140 else :
131141 raise TypeError
142+ return data
143+
132144
133- return new_data
145+ def _altair_DateTime_to_datetime (dt ):
146+ """Convert dictionary representation of an Altair DateTime to datetime object.
147+ Parameters
148+ ----------
149+ dt : dict
150+ The dictionary representation of the Altair DateTime object to be converted.
151+
152+ Returns
153+ -------
154+ A datetime object
155+ """
156+ MONTHS = {'Jan' : 1 , 'January' : 1 , 'Feb' : 2 , 'February' : 2 , 'Mar' : 3 , 'March' : 3 , 'Apr' : 4 , 'April' : 4 ,
157+ 'May' : 5 , 'May' : 5 , 'Jun' : 6 , 'June' : 6 , 'Jul' : 7 , 'July' : 7 , 'Aug' : 8 , 'August' : 8 ,
158+ 'Sep' : 9 , 'Sept' : 9 , 'September' : 9 , 'Oct' : 10 , 'October' : 10 , 'Nov' : 11 , 'November' : 11 ,
159+ 'Dec' : 12 , 'December' : 12 }
160+
161+ alt_to_datetime_kw_mapping = {'date' : 'day' , 'hours' : 'hour' , 'milliseconds' : 'microsecond' , 'minutes' : 'minute' ,
162+ 'month' : 'month' , 'seconds' : 'second' , 'year' : 'year' }
163+
164+ datetime_kwargs = {'year' : 0 , 'month' : 1 , 'day' : 1 , 'hour' : 0 , 'minute' : 0 , 'second' : 0 , 'microsecond' : 0 }
165+
166+ if 'day' in dt or 'quarter' in dt :
167+ raise NotImplementedError
168+ if 'year' not in dt :
169+ raise KeyError ('A year must be provided.' )
170+ if 'month' not in dt :
171+ dt ['month' ] = 1 # Default to January
172+ else :
173+ if isinstance (dt ['month' ], str ): # convert from str to number form for months
174+ dt ['month' ] = MONTHS [dt ['month' ]]
175+ if 'date' not in dt :
176+ dt ['date' ] = 1 # Default to the first of the month
177+ if 'milliseconds' in dt :
178+ dt ['milliseconds' ] = dt ['milliseconds' ]* 1000 # convert to microseconds
179+ if 'utc' in dt :
180+ raise NotImplementedError ("mpl-altair currently doesn't support timezones." )
181+
182+ for k , v in dt .items ():
183+ datetime_kwargs [alt_to_datetime_kw_mapping [k ]] = v
184+
185+ return datetime (** datetime_kwargs )
0 commit comments