2 #@+node:@file charting/taxis.py
5 A timeaxis for gantt and resource charts.
8 #@+node:<< Copyright >>
9 ############################################################################
10 # Copyright (C) 2005, 2006, 2007, 2008 by Reithinger GmbH
13 # This file is part of faces.
15 # faces is free software; you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation; either version 2 of the License, or
18 # (at your option) any later version.
20 # faces is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the
27 # Free Software Foundation, Inc.,
28 # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 ############################################################################
31 #@-node:<< Copyright >>
35 import matplotlib
.font_manager
as font
36 import matplotlib
.transforms
as mtrans
37 import matplotlib
.colors
as colors
38 import matplotlib
.artist
as artist
39 import matplotlib
._image
as mimage
44 from faces
.pcalendar
import strftime
52 _
= faces
.plocale
.get_gettext()
54 _colorConverter
= colors
.colorConverter
56 _week_name
= _("Week")
58 def alt_week_locator(alt
=True):
60 use an alternate week locator for gantt charts.
66 _week_name
= _("Week")
72 class Locator(object):
74 #@+node:<< declarations >>
75 can_locate_free_time
= False
77 #@-node:<< declarations >>
82 self
.tick_pos
= (0, 0) # position is 0, the highest positon is 0
84 self
.format_cache
= { }
87 def get_marks(self
, intervals
, scale
, transform
):
88 xmin
, xmax
= transform
.get_bbox1().intervalx().get_bounds()
89 if intervals
[0][0] < xmin
:
90 intervals
[0] = (xmin
, intervals
[0][1])
92 if intervals
[-1][1] > xmax
:
93 intervals
[-1] = (intervals
[-1][0], xmax
)
95 middles
= map(lambda i
: (i
[0] + i
[1]) / 2, intervals
)
96 build_mark
= self
.build_mark
97 marks
= [ build_mark(i
, scale
, transform
) for i
in intervals
]
98 xs
= transform
.seq_x_y(middles
, middles
)[0]
102 def build_mark(self
, interval
, scale
, transform
):
103 format
= self
._get
_format
(interval
, transform
)
104 date
= scale
.to_num(int(interval
[0])).to_datetime()
105 quater
= 1 + (date
.month
- 1) / 3
106 decade
= 10 * (date
.year
/ 10)
107 f
= format
.replace("%Q", str(quater
))
108 f
= f
.replace("%D", str(decade
))
109 return strftime(date
, f
)
113 def fits(self
, transform
, scale
):
114 delta
= self
._delta
(scale
)
115 key
= self
.tick_pos
[0] == self
.tick_pos
[1] and "top" or "default"
116 sizes
= self
.sizes
.get(key
, self
.sizes
["default"])[0]
117 delta
= transform(delta
)
118 return sizes
[-1][0] < delta
121 def prepare(self
, renderer
, fonts
, tickers
):
122 "precalculates all possible marker sizes"
123 self
.renderer
= renderer
124 self
.fonts
= zip(tickers
, fonts
)
126 for v
in self
.sizes
.itervalues():
127 for s
in v
.itervalues():
135 def _calc_sizes(self
):
136 raise RuntimeError("abstract")
139 def _delta(self
, scale
):
140 raise RuntimeError("abstract")
143 def _get_format(self
, interval
, transform
):
144 delta
= int(interval
[0] - interval
[1])
145 format
= self
.format_cache
.get(delta
)
146 if format
: return format
148 x
, y
= transform
.seq_x_y(interval
, (0, 0))
150 key
= self
.tick_pos
[0] == self
.tick_pos
[1] and "top" or "default"
151 sizes
= self
.sizes
.get(key
, self
.sizes
["default"])[self
.tick_pos
[0]]
159 self
.format_cache
[delta
] = format
162 #@+node:_calc_markers
163 def _calc_markers(self
, markers
, format
, key
="default"):
164 extent
= self
.renderer
.get_text_width_height
166 key_fonts
= self
.sizes
.get(key
)
169 for i
, f
in self
.fonts
: key_fonts
[i
] = []
171 if not isinstance(markers
, (list, tuple)):
174 for i
, f
in self
.fonts
:
175 size
= max([extent(m
, f
, False)[0] for m
in markers
])
176 key_fonts
[i
].append((size
, str(format
)))
178 self
.sizes
[key
] = key_fonts
179 #@-node:_calc_markers
181 def is_free(self
, num_date
):
186 #@-node:class Locator
187 #@+node:class DecadeLocator
188 class DecadeLocator(Locator
):
191 def _delta(self
, scale
):
192 return scale
.week_delta
* 52 * 10
195 def _calc_sizes(self
):
196 self
._calc
_markers
("88888", "%D")
199 def __call__(self
, left
, right
, time_scale
):
200 num
= time_scale
.to_num
201 dt
= datetime
.datetime
202 left
= num(int(left
))
203 right
= num(int(right
))
204 start
= left
.to_datetime().year
/ 10
205 end
= right
.to_datetime().year
/ 10 + 2
206 locs
= map(lambda y
: num(dt(y
* 10, 1, 1)), range(start
, end
))
210 #@-node:class DecadeLocator
211 #@+node:class YearLocator
212 class YearLocator(Locator
):
215 def _delta(self
, scale
):
216 return scale
.week_delta
* 52
219 def _calc_sizes(self
):
220 self
._calc
_markers
("88888", "%IY")
223 def __call__(self
, left
, right
, time_scale
):
224 num
= time_scale
.to_num
225 dt
= datetime
.datetime
226 left
= num(int(left
))
227 right
= num(int(right
))
228 start
= left
.to_datetime().year
229 end
= right
.to_datetime().year
+ 2
230 locs
= map(lambda y
: num(dt(y
, 1, 1)), range(start
, end
))
234 #@-node:class YearLocator
235 #@+node:class QuaterLocator
236 class QuaterLocator(Locator
):
239 def _delta(self
, scale
):
240 return scale
.week_delta
* 12
243 def _calc_sizes(self
):
244 self
._calc
_markers
("Q 8/88888", "Q %Q/%IY", "top")
245 self
._calc
_markers
("Q 88", "Q %Q")
248 def __call__(self
, left
, right
, time_scale
):
249 num
= time_scale
.to_num
250 dt
= datetime
.datetime
251 left
= num(int(left
))
252 right
= num(int(right
))
253 start
= left
.to_datetime()
254 end
= right
.to_datetime()
255 start
= start
.year
* 4 + (start
.month
- 1) / 3
256 end
= end
.year
* 4 + (end
.month
- 1) / 3 + 2
257 locs
= map(lambda qy
: num(dt(qy
/4, (qy
%4)*3+1, 1)), range(start
, end
))
261 #@-node:class QuaterLocator
262 #@+node:class MonthLocator
263 class MonthLocator(Locator
):
266 def _delta(self
, scale
):
267 return scale
.week_delta
* 4
270 def _calc_sizes(self
):
271 dt
= datetime
.datetime
273 return map(lambda m
: strftime(dt(2005, m
, 1), format
), range(1, 13))
275 self
._calc
_markers
(mlist("%B 88888"), "%B %IY", "top")
276 self
._calc
_markers
(mlist("%b 88888"), "%b %IY", "top")
277 self
._calc
_markers
(mlist("%m.88888"), "%m.%IY", "top")
278 self
._calc
_markers
(mlist("%B"), "%B")
279 self
._calc
_markers
(mlist("%b"), "%b")
280 self
._calc
_markers
("8888", "%m")
283 def __call__(self
, left
, right
, time_scale
):
284 num
= time_scale
.to_num
285 dt
= datetime
.datetime
287 left
= num(int(left
))
288 right
= num(int(right
))
289 start
= left
.to_datetime()
290 end
= right
.to_datetime()
291 start
= start
.year
* 12 + start
.month
- 1
292 end
= end
.year
* 12 + end
.month
+ 1
293 locs
= map(lambda my
: num(dt(my
/12, 1+my
%12, 1)), range(start
, end
))
297 #@-node:class MonthLocator
298 #@+node:class WeekLocator
299 class WeekLocator(Locator
):
302 def _delta(self
, scale
):
303 return scale
.week_delta
306 def _calc_sizes(self
):
309 dt
= datetime
.datetime
311 return map(lambda m
: strftime(dt(2005, m
, 1), str(format
)), range(1, 13))
314 self
._calc
_markers
(mlist("%IW. " + _week_name
+ " %IB 88888"),
315 "%IW. " + _week_name
+ " %IB %IY", "top")
316 self
._calc
_markers
(mlist("%IW. " + _week_name
+ " %ib 88888"),
317 "%IW. " + _week_name
+ " %ib %IY", "top")
318 self
._calc
_markers
(mlist("%IW. " + _week_name
+ " %im.88888"),
319 "%IW. " + _week_name
+ " %m.%IY", "top")
320 self
._calc
_markers
(mlist("%IW %ib 88888"), "%IW %ib %IY", "top")
321 self
._calc
_markers
(mlist("%IW %im 88888"), "%IW %im.%IY", "top")
322 self
._calc
_markers
("888. " + _week_name
, "%IW. " + _week_name
)
323 self
._calc
_markers
("8888", "%IW")
325 # in the US week numbers are not used
326 self
._calc
_markers
(mlist("%B 88"), "%B %d")
327 self
._calc
_markers
(mlist("%b. 88"), "%b. %d")
331 def __call__(self
, left
, right
, time_scale
):
332 num
= time_scale
.to_num
333 left
= num(int(left
))
334 right
= num(int(right
)) + time_scale
.week_delta
335 start
= left
.to_datetime().replace(hour
=0, minute
=0)
336 start
-= datetime
.timedelta(days
=start
.weekday())
338 locs
= range(start
, right
, time_scale
.week_delta
)
342 #@-node:class WeekLocator
343 #@+node:class DayLocator
344 class DayLocator(Locator
):
345 #@ << declarations >>
346 #@+node:<< declarations >>
347 can_locate_free_time
= True
350 #@-node:<< declarations >>
354 def _delta(self
, scale
):
355 return scale
.day_delta
358 def _calc_sizes(self
):
359 dt
= datetime
.datetime
361 return map(lambda d
: strftime(dt(2005, 1, d
), format
), range(1, 8))
363 self
._calc
_markers
(dlist("%A %x88"), "%A %x", "top")
364 self
._calc
_markers
(dlist("%a %x88"), "%a %x", "top")
365 self
._calc
_markers
(dlist("%x88"), "%x", "top")
366 self
._calc
_markers
(dlist("%A 888."), "%A %d.")
367 self
._calc
_markers
(dlist("%a 888."), "%a %d.")
368 self
._calc
_markers
("8888", "%d")
371 def __call__(self
, left
, right
, time_scale
):
372 self
.time_scale
= time_scale
373 num
= time_scale
.to_num
374 date
= time_scale
.to_datetime
375 td
= datetime
.timedelta
376 left
= date(num(int(left
))).replace(hour
=0, minute
=0)
377 right
= date(num(int(right
)))
378 days
= (right
- left
).days
+ 2
379 locs
= map(lambda d
: num(left
+ td(days
=d
)), range(0, days
))
383 def is_free(self
, num_date
):
384 return self
.time_scale
.is_free_day(num_date
)
387 #@-node:class DayLocator
388 #@+node:class SlotLocator
389 class SlotLocator(Locator
):
390 #@ << declarations >>
391 #@+node:<< declarations >>
392 can_locate_free_time
= True
395 #@-node:<< declarations >>
399 def _delta(self
, scale
):
400 return scale
.slot_delta
403 def __call__(self
, left
, right
, time_scale
):
404 self
.time_scale
= time_scale
405 num
= time_scale
.to_num
406 date
= time_scale
.to_datetime
407 td
= datetime
.timedelta
408 left
= date(num(int(left
))).replace(hour
=0, minute
=0)
409 right
= date(num(int(right
)))
410 days
= (right
- left
).days
+ 2
411 days
= map(lambda d
: left
+ td(days
=d
), range(0, days
))
412 get_working_times
= time_scale
.chart_calendar
.get_working_times
416 slots
= get_working_times(d
.weekday())
417 locs
.extend(map(lambda s
: num(d
+ td(minutes
=s
[0])), slots
))
422 def _calc_sizes(self
):
423 self
._calc
_markers
("888:88-88:88", "%(sh)02i:%(sm)02i-%(eh)02i:%(em)02i")
424 self
._calc
_markers
("888-88", "%(sh)02i-%(eh)02i")
425 self
._calc
_markers
("888:88", "%(sh)02i:%(sm)02i")
426 self
._calc
_markers
("888", "%(sh)02i")
429 def get_marks(self
, intervals
, scale
, transform
):
430 def build_mark(interval
):
431 format
= self
._get
_format
(interval
, transform
)
432 start
= scale
.to_num(interval
[0]).to_datetime()
433 end
= scale
.to_num(interval
[1]).to_datetime()
434 vals
= { "sh" : start
.hour
,
440 middles
= map(lambda i
: (i
[0] + i
[1]) / 2, intervals
)
441 marks
= map(build_mark
, intervals
)
442 xs
= transform
.seq_x_y(middles
, (0,)*len(middles
))[0]
443 return zip(marks
, xs
)
446 def is_free(self
, num_date
):
447 return self
.time_scale
.is_free_slot(num_date
)
450 #@-node:class SlotLocator
453 _locators
= ( SlotLocator
,
461 #@+node:_zigzag_lines
462 def _zigzag_lines(locs
, top
, bottom
):
465 ys
= [ top
, bottom
, bottom
, top
] * ((len(locs
) + 1) / 2)
466 if len(locs
) % 2: del ys
[-2:]
468 #@-node:_zigzag_lines
469 #@+node:class TimeAxis
470 class TimeAxis(artist
.Artist
, widgets
._PropertyAware
):
471 #@ << declarations >>
472 #@+node:<< declarations >>
474 "family": "sans-serif",
475 #"family": [ "Arial", "Verdana", "Bitstream Vera Sans" ] ,
479 "variant" : "normal",
481 "2.size" : "x-large",
485 "0.facecolor" : 'white',
486 "facecolor" : 'darkgray',
487 "edgecolor" : 'black',
488 "grid.edgecolor" : 'darkgray',
489 "free.facecolor": "lightgrey",
491 "joinstyle" : 'miter',
492 "linestyle" : 'solid',
493 "now.edgecolor" : "black",
495 "now.linestyle" : "dashed",
496 "antialiased" : True,
504 show_free_time
= True
506 time_scale
= None # must be set by Chart
508 #@-node:<< declarations >>
512 def __init__(self
, properties
=None):
513 widgets
._PropertyAware
.__init
__(self
, properties
)
514 artist
.Artist
.__init
__(self
)
515 self
._locators
= tuple(map(lambda l
: l(), _locators
))
516 self
._last
_cache
= None
517 self
._last
_cache
_state
= None
519 self
.encoding
= locale
.getlocale()[1] or "ascii"
522 def calc_height(self
):
523 if not self
.show_scale
:
527 prop
= self
.get_property
528 def_height
= font
.fontManager
.get_default_size()
531 tickers
= (0,) + prop("tickers")
534 tsize
= self
.get_font(str(t
)).get_size_in_points()
535 self
.height
+= tsize
+ 2 * sep
539 #@+node:set_transform
540 def set_transform(self
, t
):
541 #a non scaled point y axis
545 Transformation
= mtrans
.SeparableTransformation
547 fig_point_to_pixel
= self
.get_figure().dpi
/ mtrans
.Value(72)
549 view_box
= t
.get_bbox2()
550 top
= view_box
.ur().y()
551 bottom
= view_box
.ll().y()
552 point_height
= (bottom
- top
) / fig_point_to_pixel
557 new_ll
= Point(ll
.x(), point_height
)
558 new_ur
= Point(ur
.x(), Value(0))
559 data_box
= Bbox(new_ll
, new_ur
)
561 t
= Transformation(data_box
, view_box
, t
.get_funcx(), t
.get_funcy())
562 artist
.Artist
.set_transform(self
, t
)
563 #@-node:set_transform
566 def draw(self
, renderer
):
567 if not self
.get_visible(): return
569 trans
= self
.get_transform()
572 if not self
.__prepared
:
573 self
.__prepared
= True
574 tickers
= (0,) + self
.get_property("tickers")
575 fonts
= map(lambda t
: self
.get_font(str(t
)), tickers
)
576 for l
in self
._locators
:
577 l
.prepare(renderer
, fonts
, tickers
)
579 data_box
= trans
.get_bbox1()
580 view_box
= trans
.get_bbox2()
581 width
= data_box
.width()
583 if self
._last
_width
!= width
:
584 self
._last
_width
= width
585 self
.find_ticker(renderer
)
587 cache_state
= (self
.show_grid
+ self
.show_scale
,
588 view_box
.width(), view_box
.height(),
589 renderer
, data_box
.xmin())
591 if self
._last
_cache
_state
== cache_state
and self
._last
_cache
:
593 #not now because of memory leak
594 renderer
.draw_image(0, 0, self
._last
_cache
, view_box
)
595 #renderer.restore_region(self._last_cache)
600 gc
= renderer
.new_gc()
602 if self
.get_clip_on():
603 gc
.set_clip_rectangle(self
.clipbox
.get_bounds())
605 if self
.show_grid
: self
.draw_grid(renderer
, gc
, trans
)
607 time_scale
= self
.time_scale
608 left
, right
= data_box
.intervalx().get_bounds()
609 if left
<= time_scale
.now
<= right
:
610 top
, bottom
= data_box
.intervaly().get_bounds()
611 self
.set_gc(gc
, "now")
612 renderer
.draw_lines(gc
,
613 (time_scale
.now
, time_scale
.now
),
614 (top
, bottom
), trans
)
616 if self
.show_scale
: self
.draw_scale(renderer
, gc
, trans
)
618 self
._last
_cache
_state
= cache_state
619 #self._last_cache = renderer.copy_from_bbox(view_box)
621 self
._last
_cache
= mimage
.frombuffer(\
622 renderer
.buffer_rgba(0, 0),
625 except AttributeError:
626 self
._last
_cache
= None
629 self
._last
_cache
.flipud_out()
634 def find_ticker(self
, renderer
):
635 time_scale
= self
.time_scale
637 tickers
= self
.get_property("tickers")
638 if not isinstance(tickers
, tuple):
639 tickers
= tuple(tickers
)
641 tickers
= (0,) + tickers
642 highest_locator
= tickers
[-1]
644 transform
= self
.get_transform()
645 origin
= transform
.xy_tup((0, 0))[0]
647 def delta_trans(x_delta
):
648 p
= transform
.xy_tup((x_delta
, 0))
651 def refresh_locators(lowest
):
654 loc
= self
._locators
[lowest
+ t
]
655 loc
.tick_pos
= (t
, highest_locator
)
656 loc
.format_cache
.clear()
658 for ti
in range(len(self
._locators
) - highest_locator
):
659 loc
= self
._locators
[ti
]
660 loc
.tick_pos
= (0, highest_locator
)
662 if loc
.fits(delta_trans
, time_scale
):
666 refresh_locators(len(self
._locators
) - highest_locator
- 1)
669 def draw_scale(self
, renderer
, gc
, trans
):
670 prop
= self
.get_property
671 time_scale
= self
.time_scale
673 def_height
= font
.fontManager
.get_default_size()
675 left
, right
= trans
.get_bbox1().intervalx().get_bounds()
676 dpi
= self
.get_figure().get_dpi()
678 if left
>= right
: return
681 free_face
= _colorConverter
.to_rgb(prop("free.facecolor"))
683 def dline(x1
, y1
, x2
, y2
):
684 draw_line(renderer
, gc
, x1
, y1
, x2
, y2
, trans
)
686 def draw_ticks(bottom
, locator
, name
, show_free_time
=False):
687 fp
= self
.get_font(name
)
688 top
= bottom
- fp
.get_size_in_points() - 2 * sep
690 locs
= locator(left
, right
, time_scale
)
691 lintervals
= zip(locs
[:-1], locs
[1:])
693 face
= _colorConverter
.to_rgb(prop(name
+ ".facecolor"))
694 verts
= ((left
, -bottom
), (left
, -top
),
695 (right
, -top
), (right
, -bottom
))
696 verts
= trans
.seq_xy_tups(verts
)
697 renderer
.draw_polygon(gc
, face
, verts
)
699 if show_free_time
and locator
.can_locate_free_time
:
701 for l
, r
in lintervals
:
702 #if locator.is_free((l + r) / 2):
703 if locator
.is_free(l
):
704 verts
= ((l
, -bottom
), (l
, -top
),
705 (r
, -top
), (r
, -bottom
))
706 verts
= trans
.seq_xy_tups(verts
)
707 renderer
.draw_polygon(gc
, free_face
, verts
)
709 fp
= self
.get_font(name
)
710 gc
.set_foreground(prop(name
+ ".color"))
711 x
, y
= trans
.xy_tup((0, -bottom
+ sep
))
712 markers
= locator
.get_marks(lintervals
, time_scale
, trans
)
714 self
.draw_text(renderer
, gc
, x
, y
, m
, fp
, "bc", dpi
)
716 gc
.set_foreground(prop(name
+ ".edgecolor"))
717 gc
.set_linewidth(prop(name
+ ".linewidth"))
719 xs
, ys
= _zigzag_lines(locs
, -top
, -bottom
)
720 renderer
.draw_lines(gc
, xs
, ys
, trans
)
722 gc
.set_linewidth(prop("linewidth"))
723 dline(left
, -top
, right
, -top
)
724 dline(left
, -bottom
, right
, -bottom
)
728 tickers
= prop("tickers")
730 ticks
= self
._locators
[self
.ticker
]
731 bottom
= draw_ticks(bottom
, ticks
, "0", self
.show_free_time
)
733 ticks
= self
._locators
[self
.ticker
+ t
]
734 bottom
= draw_ticks(bottom
, ticks
, str(t
))
737 def draw_grid(self
, renderer
, gc
, trans
):
738 time_scale
= self
.time_scale
739 prop
= self
.get_property
741 data_box
= trans
.get_bbox1()
742 left
, right
= data_box
.intervalx().get_bounds()
743 top
, bottom
= data_box
.intervaly().get_bounds()
745 if left
>= right
: return
747 locator
= self
._locators
[self
.ticker
]
748 locs
= locator(left
, right
, time_scale
)
749 lintervals
= zip(locs
[:-1], locs
[1:])
751 self
.set_gc(gc
, "grid")
752 if self
.show_free_time
and locator
.can_locate_free_time
:
754 free_face
= _colorConverter
.to_rgb(prop("free.facecolor"))
755 for l
, r
in lintervals
:
756 if locator
.is_free((l
+ r
) / 2):
757 verts
= trans
.seq_xy_tups(((l
, bottom
), (l
, top
),
758 (r
, top
), (r
, bottom
)))
760 renderer
.draw_polygon(gc
, free_face
, verts
)
763 gc
.set_linewidth(prop("grid.linewidth"))
765 xs
, ys
= _zigzag_lines(locs
, top
, bottom
)
766 renderer
.draw_lines(gc
, xs
, ys
, trans
)
767 draw_line(renderer
, gc
, left
, bottom
, right
, bottom
, trans
)
770 def draw_text(self
, renderer
, gc
, x
, y
, text
, fp
, align
, dpi
):
772 special draw_text for taxis using the locale encoding which is used by
773 the strftime functions
777 text
= text
.decode(self
.encoding
)
779 w
, h
= renderer
.get_text_width_height(text
, fp
, False)
782 elif align
[0] == 't':
787 elif align
[1] == 'r':
791 canvasw
, canvash
= renderer
.get_canvas_width_height()
794 renderer
.draw_text(gc
, x
, y
, text
, fp
, 0, False)
797 #@-node:class TimeAxis
799 #@-node:@file charting/taxis.py