diff --git a/leather/axis.py b/leather/axis.py index 74d27db..dcbada0 100644 --- a/leather/axis.py +++ b/leather/axis.py @@ -4,6 +4,7 @@ import six +from leather import svg from leather import theme @@ -21,19 +22,39 @@ class Axis(object): is the total number of ticks. The return value of the function will be used for display instead of the original tick value. """ - def __init__(self, ticks=None, tick_formatter=None): + def __init__(self, ticks=None, tick_formatter=None, name=None): self._ticks = ticks or theme.default_ticks - self._tick_formatter = tick_formatter + self._tick_formatter = tick_formatter or (lambda value, i, tick_count: value) + self._name = name + + def _estimate_left_tick_width(self, scale): + """ + Estimate the y axis space used by tick labels. + """ + ticks = scale.ticks(self._ticks) + tick_count = len(ticks) + max_len = 0 + + for i, value in enumerate(ticks): + max_len = max(max_len, len(self._tick_formatter(value, i, tick_count))) + + return max_len * theme.tick_font_char_width def estimate_label_margin(self, scale, orient): """ Estimate the space needed for the tick labels. """ + margin = 0 + if orient == 'left': - max_len = max(len(six.text_type(t)) for t in scale.ticks(self._ticks)) - return max_len * theme.tick_font_char_width + margin += self._estimate_left_tick_width(scale) + (theme.tick_size * 2) elif orient == 'bottom': - return theme.tick_font_char_height + margin += theme.tick_font_char_height + (theme.tick_size * 2) + + if self._name: + margin += theme.axis_title_font_char_height + theme.axis_title_gap + + return margin def to_svg(self, width, height, scale, orient): """ @@ -42,6 +63,32 @@ def to_svg(self, width, height, scale, orient): group = ET.Element('g') group.set('class', 'axis ' + orient) + # Axis title + if orient == 'left': + title_x = -(self._estimate_left_tick_width(scale) + theme.axis_title_gap) + title_y = height / 2 + dy='' + transform = svg.rotate(270, title_x, title_y) + elif orient == 'bottom': + title_x = width / 2 + title_y = height + theme.tick_font_char_height + (theme.tick_size * 2) + theme.axis_title_gap + dy='1em' + transform = '' + + title = ET.Element('text', + x=six.text_type(title_x), + y=six.text_type(title_y), + dy=dy, + fill=theme.axis_title_color, + transform=transform + ) + title.set('text-anchor', 'middle') + title.set('font-family', theme.axis_title_font_family) + title.text = self._name + + group.append(title) + + # Ticks if orient == 'left': label_x = -(theme.tick_size * 2) x1 = -theme.tick_size @@ -112,9 +159,7 @@ def to_svg(self, width, height, scale, orient): label.set('text-anchor', text_anchor) label.set('font-family', theme.tick_font_family) - if self._tick_formatter: - value = self._tick_formatter(value, i, tick_count) - + value = self._tick_formatter(value, i, tick_count) label.text = six.text_type(value) tick_group.append(label) diff --git a/leather/chart.py b/leather/chart.py index 7e3ba36..bf71f7e 100644 --- a/leather/chart.py +++ b/leather/chart.py @@ -31,41 +31,45 @@ def __init__(self, title=None): self._scales = [None, None] self._axes = [None, None] - def _set_scale(self, dimension, scale): - """ - Set a new :class:`.Scale` for this chart. - """ - self._scales[dimension] = scale - def set_x_scale(self, scale): """ Set the X :class:`.Scale` for this chart. """ - self._set_scale(X, scale) + self._scales[X] = scale def set_y_scale(self, scale): """ Set the Y :class:`.Scale` for this chart. """ - self._set_scale(Y, scale) + self._scales[Y] = scale - def _set_axis(self, dimension, axis): + def set_x_axis(self, axis): """ - Set a new :class:`.Axis` for this chart. + Set a new :class:`.Axis` class for this chart. """ - self._axes[dimension] = axis + self._axes[X] = axis - def set_x_axis(self, axis): + def set_y_axis(self, axis): """ - Set the X :class:`.Axis` for this chart. + Set the Y :class:`.Axis` class for this chart. """ - self._set_axis(X, axis) + self._axes[Y] = axis - def set_y_axis(self, axis): + def add_x_axis(self, ticks=None, tick_formatter=None, name=None): + """ + Create and add an X :class:`.Axis`. + + If you want to use a custom axis class use :meth:`.set_x_axis` instead. """ - Set the Y :class:`.Axis` for this chart. + self._axes[X] = Axis(ticks, tick_formatter, name) + + def add_y_axis(self, ticks=None, tick_formatter=None, name=None): + """ + Create and add an Y :class:`.Axis`. + + If you want to use a custom axis class use :meth:`.set_y_axis` instead. """ - self._set_axis(Y, axis) + self._axes[Y] = Axis(ticks, tick_formatter, name) def add_series(self, series): """ @@ -200,7 +204,7 @@ def to_svg_group(self, width=None, height=None): label.text = six.text_type(self._title) header_group.append(label) - header_margin += theme.title_font_char_height + header_margin += theme.title_font_char_height + theme.title_gap margin_group.append(header_group) diff --git a/leather/svg.py b/leather/svg.py index 804674d..b2256a5 100644 --- a/leather/svg.py +++ b/leather/svg.py @@ -34,3 +34,10 @@ def translate(x, y): Generate an SVG transform statement representing a simple translation. """ return 'translate(%i %i)' % (x, y) + +def rotate(deg, x, y): + """ + Generate an SVG transform statement representing rotation around a given + point. + """ + return 'rotate(%i %i %i)' % (deg, x, y) diff --git a/leather/theme.py b/leather/theme.py index 435e28d..1d5d85f 100644 --- a/leather/theme.py +++ b/leather/theme.py @@ -31,11 +31,34 @@ title_font_size = 16 #: Approximate glyph height of the title font -title_font_char_height = 18 +title_font_char_height = 16 #: Approximate glyph width of the title font title_font_char_width = 9 +#: Gap between title and rest of chart +title_gap = 4 + +# AXIS + +#: Axis title text color +axis_title_color = '#333' + +#: Axis title font +axis_title_font_family = 'Monaco' + +#: Axis title font size +axis_title_font_size = 14 + +#: Approximate glyph height of the axis title font +axis_title_font_char_height = 14 + +#: Approximate glyph width of the axis title font +axis_title_font_char_width = 8 + +#: Gap between axis title and rest of chart +axis_title_gap = 8 + # TICKS #: Default number of ticks to display diff --git a/test.py b/test.py index 0ff8430..82eabb5 100644 --- a/test.py +++ b/test.py @@ -9,5 +9,7 @@ data = list(reader)[:10] chart = leather.Chart('Test') +chart.add_x_axis(name='Test X Axis name') +chart.add_y_axis(name='The Y Axis has arrived') chart.add_bars(data, x=1, y=0) chart.to_svg('test.svg')