コード例 #1
0
def print_table(self,
                max_rows=20,
                max_columns=6,
                output=sys.stdout,
                max_column_width=20,
                locale=None,
                max_precision=3):
    """
    Print a text-based view of the data in this table.

    The output of this method is GitHub Flavored Markdown (GFM) compatible.

    :param max_rows:
        The maximum number of rows to display before truncating the data. This
        defaults to :code:`20` to prevent accidental printing of the entire
        table. Pass :code:`None` to disable the limit.
    :param max_columns:
        The maximum number of columns to display before truncating the data.
        This defaults to :code:`6` to prevent wrapping in most cases. Pass
        :code:`None` to disable the limit.
    :param output:
        A file-like object to print to.
    :param max_column_width:
        Truncate all columns to at most this width. The remainder will be
        replaced with ellipsis.
    :param locale:
        Provide a locale you would like to be used to format the output.
        By default it will use the system's setting.
    :max_precision:
        Puts a limit on the maximum precision displayed for number types.
        Numbers with lesser precision won't be affected.
        This defaults to :code:`3`. Pass :code:`None` to disable limit.
    """
    if max_rows is None:
        max_rows = len(self._rows)

    if max_columns is None:
        max_columns = len(self._columns)

    if max_precision is None:
        max_precision = float('inf')

    ellipsis = config.get_option('ellipsis_chars')
    h_line = config.get_option('horizontal_line_char')
    v_line = config.get_option('vertical_line_char')
    locale = locale or config.get_option('default_locale')

    rows_truncated = max_rows < len(self._rows)
    columns_truncated = max_columns < len(self._column_names)
    column_names = []
    for column_name in self.column_names[:max_columns]:
        if max_column_width is not None and len(
                column_name) > max_column_width:
            column_names.append('%s...' % column_name[:max_column_width - 3])
        else:
            column_names.append(column_name)

    if columns_truncated:
        column_names.append(ellipsis)

    widths = [len(n) for n in column_names]
    number_formatters = []
    formatted_data = []

    # Determine correct number of decimal places for each Number column
    for i, c in enumerate(self._columns):
        if i >= max_columns:
            break

        if isinstance(c.data_type, Number):
            max_places = utils.max_precision(c[:max_rows])
            add_ellipsis = False
            if max_places > max_precision:
                add_ellipsis = True
                max_places = max_precision
            number_formatters.append(
                utils.make_number_formatter(max_places, add_ellipsis))
        else:
            number_formatters.append(None)

    # Format data and display column widths
    for i, row in enumerate(self._rows):
        if i >= max_rows:
            break

        formatted_row = []

        for j, v in enumerate(row):
            if j >= max_columns:
                v = ellipsis
            elif v is None:
                v = ''
            elif number_formatters[j] is not None and not math.isinf(v):
                v = format_decimal(v,
                                   format=number_formatters[j],
                                   locale=locale)
            else:
                v = six.text_type(v)

            if max_column_width is not None and len(v) > max_column_width:
                v = '%s...' % v[:max_column_width - 3]

            if len(v) > widths[j]:
                widths[j] = len(v)

            formatted_row.append(v)

            if j >= max_columns:
                break

        formatted_data.append(formatted_row)

    def write(line):
        output.write(line + '\n')

    def write_row(formatted_row):
        """
        Helper function that formats individual rows.
        """
        row_output = []

        for j, d in enumerate(formatted_row):
            # Text is left-justified, all other values are right-justified
            if isinstance(self._column_types[j], Text):
                output = ' %s ' % d.ljust(widths[j])
            else:
                output = ' %s ' % d.rjust(widths[j])

            row_output.append(output)

        text = v_line.join(row_output)

        write('%s%s%s' % (v_line, text, v_line))

    # Dashes span each width with '+' character at intersection of
    # horizontal and vertical dividers.
    divider = '%(v_line)s %(columns)s %(v_line)s' % {
        'h_line': h_line,
        'v_line': v_line,
        'columns': ' | '.join(h_line * w for w in widths)
    }

    # Headers
    write_row(column_names)
    write(divider)

    # Rows
    for formatted_row in formatted_data:
        write_row(formatted_row)

    # Row indicating data was truncated
    if rows_truncated:
        write_row([ellipsis for n in column_names])
コード例 #2
0
ファイル: print_table.py プロジェクト: skorasaurus/agate
def print_table(self, max_rows=20, max_columns=6, output=sys.stdout, max_column_width=20, locale=None, max_precision=3):
    """
    Print a text-based view of the data in this table.

    The output of this method is Github Friendly Markdown (GFM) compatible.

    :param max_rows:
        The maximum number of rows to display before truncating the data. This
        defaults to :code:`20` to prevent accidental printing of the entire
        table. Pass :code:`None` to disable the limit.
    :param max_columns:
        The maximum number of columns to display before truncating the data.
        This defaults to :code:`6` to prevent wrapping in most cases. Pass
        :code:`None` to disable the limit.
    :param output:
        A file-like object to print to.
    :param max_column_width:
        Truncate all columns to at most this width. The remainder will be
        replaced with ellipsis.
    :param locale:
        Provide a locale you would like to be used to format the output.
        By default it will use the system's setting.
    :max_precision:
        Puts a limit on the maximum precision displayed for number types.
        Numbers with lesser precision won't be affected.
        This defaults to :code:`3`. Pass :code:`None` to disable limit.
    """
    if max_rows is None:
        max_rows = len(self._rows)

    if max_columns is None:
        max_columns = len(self._columns)

    if max_precision is None:
        max_precision = float('inf')

    ellipsis = config.get_option('ellipsis_chars')
    h_line = config.get_option('horizontal_line_char')
    v_line = config.get_option('vertical_line_char')
    locale = locale or config.get_option('default_locale')

    rows_truncated = max_rows < len(self._rows)
    columns_truncated = max_columns < len(self._column_names)
    column_names = []
    for column_name in self.column_names[:max_columns]:
        if max_column_width is not None and len(column_name) > max_column_width:
            column_names.append('%s...' % column_name[:max_column_width - 3])
        else:
            column_names.append(column_name)

    if columns_truncated:
        column_names.append(ellipsis)

    widths = [len(n) for n in column_names]
    number_formatters = []
    formatted_data = []

    # Determine correct number of decimal places for each Number column
    for i, c in enumerate(self._columns):
        if i >= max_columns:
            break

        if isinstance(c.data_type, Number):
            max_places = utils.max_precision(c[:max_rows])
            add_ellipsis = False
            if max_places > max_precision:
                add_ellipsis = True
                max_places = max_precision
            number_formatters.append(utils.make_number_formatter(max_places, add_ellipsis))
        else:
            number_formatters.append(None)

    # Format data and display column widths
    for i, row in enumerate(self._rows):
        if i >= max_rows:
            break

        formatted_row = []

        for j, v in enumerate(row):
            if j >= max_columns:
                v = ellipsis
            elif v is None:
                v = ''
            elif number_formatters[j] is not None:
                v = format_decimal(
                    v,
                    format=number_formatters[j],
                    locale=locale
                )
            else:
                v = six.text_type(v)

            if max_column_width is not None and len(v) > max_column_width:
                v = '%s...' % v[:max_column_width - 3]

            if len(v) > widths[j]:
                widths[j] = len(v)

            formatted_row.append(v)

            if j >= max_columns:
                break

        formatted_data.append(formatted_row)

    def write(line):
        output.write(line + '\n')

    def write_row(formatted_row):
        """
        Helper function that formats individual rows.
        """
        row_output = []

        for j, d in enumerate(formatted_row):
            # Text is left-justified, all other values are right-justified
            if isinstance(self._column_types[j], Text):
                output = ' %s ' % d.ljust(widths[j])
            else:
                output = ' %s ' % d.rjust(widths[j])

            row_output.append(output)

        text = v_line.join(row_output)

        write('%s%s%s' % (v_line, text, v_line))

    # Dashes span each width with '+' character at intersection of
    # horizontal and vertical dividers.
    divider = '%(v_line)s %(columns)s %(v_line)s' % {
        'h_line': h_line,
        'v_line': v_line,
        'columns': ' | '.join(h_line * w for w in widths)
    }

    # Headers
    write_row(column_names)
    write(divider)

    # Rows
    for formatted_row in formatted_data:
        write_row(formatted_row)

    # Row indicating data was truncated
    if rows_truncated:
        write_row([ellipsis for n in column_names])
コード例 #3
0
ファイル: print_html.py プロジェクト: jean/agate
def print_html(self, max_rows=20, max_columns=6, output=sys.stdout, max_column_width=20, locale=None):
    """
    Print an HTML version of this table.

    :param max_rows:
        The maximum number of rows to display before truncating the data. This
        defaults to :code:`20` to prevent accidental printing of the entire
        table. Pass :code:`None` to disable the limit.
    :param max_columns:
        The maximum number of columns to display before truncating the data.
        This defaults to :code:`6` to prevent wrapping in most cases. Pass
        :code:`None` to disable the limit.
    :param output:
        A file-like object to print to. Defaults to :code:`sys.stdout`, unless
        running in Jupyter. (See above.)
    :param max_column_width:
        Truncate all columns to at most this width. The remainder will be
        replaced with ellipsis.
    :param locale:
        Provide a locale you would like to be used to format the output.
        By default it will use the system's setting.
    """
    if max_rows is None:
        max_rows = len(self._rows)

    if max_columns is None:
        max_columns = len(self._columns)

    ellipsis = config.get_option('ellipsis_chars')
    locale = locale or config.get_option('default_locale')

    rows_truncated = max_rows < len(self._rows)
    columns_truncated = max_columns < len(self._column_names)

    column_names = list(self._column_names[:max_columns])

    if columns_truncated:
        column_names.append(ellipsis)

    number_formatters = []
    formatted_data = []

    # Determine correct number of decimal places for each Number column
    for i, c in enumerate(self._columns):
        if i >= max_columns:
            break

        if isinstance(c.data_type, Number):
            max_places = utils.max_precision(c[:max_rows])
            number_formatters.append(utils.make_number_formatter(max_places))
        else:
            number_formatters.append(None)

    # Format data
    for i, row in enumerate(self._rows):
        if i >= max_rows:
            break

        formatted_row = []

        for j, v in enumerate(row):
            if j >= max_columns:
                v = ellipsis
            elif v is None:
                v = ''
            elif number_formatters[j] is not None:
                v = format_decimal(
                    v,
                    format=number_formatters[j],
                    locale=locale
                )
            else:
                v = six.text_type(v)

            if max_column_width is not None and len(v) > max_column_width:
                v = '%s...' % v[:max_column_width - 3]

            formatted_row.append(v)

            if j >= max_columns:
                break

        formatted_data.append(formatted_row)

    def write(line):
        output.write(line + '\n')

    def write_row(formatted_row):
        """
        Helper function that formats individual rows.
        """
        write('<tr>')

        for j, d in enumerate(formatted_row):
            # Text is left-justified, all other values are right-justified
            if isinstance(self._column_types[j], Text):
                write('<td style="text-align: left;">%s</td>' % d)
            else:
                write('<td style="text-align: right;">%s</td>' % d)

        write('</tr>')

    # Header
    write('<table>')
    write('<thead>')
    write('<tr>')

    for i, col in enumerate(column_names):
        write('<th>%s</th>' % col)

    write('</tr>')
    write('</thead>')
    write('<tbody>')

    # Rows
    for formatted_row in formatted_data:
        write_row(formatted_row)

    # Row indicating data was truncated
    if rows_truncated:
        write_row([ellipsis for n in column_names])

    # Footer
    write('</tbody>')
    write('</table>')
コード例 #4
0
def print_bars(self,
               label_column_name='group',
               value_column_name='Count',
               domain=None,
               width=120,
               output=sys.stdout,
               printable=False):
    """
    Print a text-based bar chart based on this table.

    :param label_column_name:
        The column containing the label values. Defaults to :code:`group`, which
        is the default output of :meth:`.Table.pivot` or :meth:`.Table.bins`.
    :param value_column_name:
        The column containing the bar values. Defaults to :code:`Count`, which
        is the default output of :meth:`.Table.pivot` or :meth:`.Table.bins`.
    :param domain:
        A 2-tuple containing the minimum and maximum values for the chart's
        x-axis. The domain must be large enough to contain all values in
        the column.
    :param width:
        The width, in characters, to use for the bar chart. Defaults to
        :code:`120`.
    :param output:
        A file-like object to print to. Defaults to :code:`sys.stdout`.
    :param printable:
        If true, only printable characters will be outputed.
    """
    tick_mark = config.get_option('tick_char')
    horizontal_line = config.get_option('horizontal_line_char')
    locale = config.get_option('default_locale')

    if printable:
        bar_mark = config.get_option('printable_bar_char')
        zero_mark = config.get_option('printable_zero_line_char')
    else:
        bar_mark = config.get_option('bar_char')
        zero_mark = config.get_option('zero_line_char')

    y_label = label_column_name
    label_column = self._columns[label_column_name]

    # if not isinstance(label_column.data_type, Text):
    #     raise ValueError('Only Text data is supported for bar chart labels.')

    x_label = value_column_name
    value_column = self._columns[value_column_name]

    if not isinstance(value_column.data_type, Number):
        raise DataTypeError(
            'Only Number data is supported for bar chart values.')

    output = output
    width = width

    # Format numbers
    decimal_places = utils.max_precision(value_column)
    value_formatter = utils.make_number_formatter(decimal_places)

    formatted_labels = []

    for label in label_column:
        formatted_labels.append(six.text_type(label))

    formatted_values = []
    for value in value_column:
        if value is None:
            formatted_values.append('-')
        else:
            formatted_values.append(
                format_decimal(value, format=value_formatter, locale=locale))

    max_label_width = max(max([len(label) for label in formatted_labels]),
                          len(y_label))
    max_value_width = max(max([len(value) for value in formatted_values]),
                          len(x_label))

    plot_width = width - (max_label_width + max_value_width + 2)

    min_value = Min(value_column_name).run(self)
    max_value = Max(value_column_name).run(self)

    # Calculate dimensions
    if domain:
        x_min = Decimal(domain[0])
        x_max = Decimal(domain[1])

        if min_value < x_min or max_value > x_max:
            raise ValueError('Column contains values outside specified domain')
    else:
        x_min, x_max = utils.round_limits(min_value, max_value)

    # All positive
    if x_min >= 0:
        x_min = Decimal('0')
        plot_negative_width = 0
        zero_line = 0
        plot_positive_width = plot_width - 1
    # All negative
    elif x_max <= 0:
        x_max = Decimal('0')
        plot_negative_width = plot_width - 1
        zero_line = plot_width - 1
        plot_positive_width = 0
    # Mixed signs
    else:
        spread = x_max - x_min
        negative_portion = (x_min.copy_abs() / spread)

        # Subtract one for zero line
        plot_negative_width = int(
            ((plot_width - 1) * negative_portion).to_integral_value())
        zero_line = plot_negative_width
        plot_positive_width = plot_width - (plot_negative_width + 1)

    def project(value):
        if value >= 0:
            return plot_negative_width + int(
                (plot_positive_width * (value / x_max)).to_integral_value())
        else:
            return plot_negative_width - int(
                (plot_negative_width * (value / x_min)).to_integral_value())

    # Calculate ticks
    ticks = OrderedDict()

    # First tick
    ticks[0] = x_min
    ticks[plot_width - 1] = x_max

    tick_fractions = [Decimal('0.25'), Decimal('0.5'), Decimal('0.75')]

    # All positive
    if x_min >= 0:
        for fraction in tick_fractions:
            value = x_max * fraction
            ticks[project(value)] = value
    # All negative
    elif x_max <= 0:
        for fraction in tick_fractions:
            value = x_min * fraction
            ticks[project(value)] = value
    # Mixed signs
    else:
        # Zero tick
        ticks[zero_line] = Decimal('0')

        # Halfway between min and 0
        value = x_min * Decimal('0.5')
        ticks[project(value)] = value

        # Halfway between 0 and max
        value = x_max * Decimal('0.5')
        ticks[project(value)] = value

    decimal_places = utils.max_precision(ticks.values())
    tick_formatter = utils.make_number_formatter(decimal_places)

    ticks_formatted = OrderedDict()

    for k, v in ticks.items():
        ticks_formatted[k] = format_decimal(v,
                                            format=tick_formatter,
                                            locale=locale)

    def write(line):
        output.write(line + '\n')

    # Chart top
    top_line = u'%s %s' % (y_label.ljust(max_label_width),
                           x_label.rjust(max_value_width))
    write(top_line)

    # Bars
    for i, label in enumerate(formatted_labels):
        value = value_column[i]
        if value == 0 or value is None:
            bar_width = 0
        elif value > 0:
            bar_width = project(value) - plot_negative_width
        elif value < 0:
            bar_width = plot_negative_width - project(value)

        label_text = label.ljust(max_label_width)
        value_text = formatted_values[i].rjust(max_value_width)

        bar = bar_mark * bar_width

        if value is not None and value >= 0:
            gap = (u' ' * plot_negative_width)

            # All positive
            if x_min <= 0:
                bar = gap + zero_mark + bar
            else:
                bar = bar + gap + zero_mark
        else:
            bar = u' ' * (plot_negative_width - bar_width) + bar

            # All negative or mixed signs
            if value is None or x_max > value:
                bar = bar + zero_mark

        bar = bar.ljust(plot_width)

        write('%s %s %s' % (label_text, value_text, bar))

    # Axis & ticks
    axis = horizontal_line * plot_width
    tick_text = u' ' * width

    for i, (tick, label) in enumerate(ticks_formatted.items()):
        # First tick
        if tick == 0:
            offset = 0
        # Last tick
        elif tick == plot_width - 1:
            offset = -(len(label) - 1)
        else:
            offset = int(-(len(label) / 2))

        pos = (width - plot_width) + tick + offset

        # Don't print intermediate ticks that would overlap
        if tick != 0 and tick != plot_width - 1:
            if tick_text[pos - 1:pos + len(label) +
                         1] != ' ' * (len(label) + 2):
                continue

        tick_text = tick_text[:pos] + label + tick_text[pos + len(label):]
        axis = axis[:tick] + tick_mark + axis[tick + 1:]

    write(axis.rjust(width))
    write(tick_text)
コード例 #5
0
ファイル: print_html.py プロジェクト: mathdesc/agate
def print_html(self,
               max_rows=20,
               max_columns=6,
               output=sys.stdout,
               max_column_width=20,
               locale=None,
               max_precision=3):
    """
    Print an HTML version of this table.

    :param max_rows:
        The maximum number of rows to display before truncating the data. This
        defaults to :code:`20` to prevent accidental printing of the entire
        table. Pass :code:`None` to disable the limit.
    :param max_columns:
        The maximum number of columns to display before truncating the data.
        This defaults to :code:`6` to prevent wrapping in most cases. Pass
        :code:`None` to disable the limit.
    :param output:
        A file-like object to print to. Defaults to :code:`sys.stdout`, unless
        running in Jupyter. (See above.)
    :param max_column_width:
        Truncate all columns to at most this width. The remainder will be
        replaced with ellipsis.
    :param locale:
        Provide a locale you would like to be used to format the output.
        By default it will use the system's setting.
    :max_precision:
        Puts a limit on the maximum precision displayed for number types.
        Numbers with lesser precision won't be affected.
        This defaults to :code:`3`. Pass :code:`None` to disable limit.
    """
    if max_rows is None:
        max_rows = len(self._rows)

    if max_columns is None:
        max_columns = len(self._columns)

    if max_precision is None:
        max_precision = float('inf')

    ellipsis = config.get_option('ellipsis_chars')
    locale = locale or config.get_option('default_locale')

    rows_truncated = max_rows < len(self._rows)
    columns_truncated = max_columns < len(self._column_names)

    column_names = list(self._column_names[:max_columns])

    if columns_truncated:
        column_names.append(ellipsis)

    number_formatters = []
    formatted_data = []

    # Determine correct number of decimal places for each Number column
    for i, c in enumerate(self._columns):
        if i >= max_columns:
            break

        if isinstance(c.data_type, Number):
            max_places = utils.max_precision(c[:max_rows])
            add_ellipsis = False
            if max_places > max_precision:
                add_ellipsis = True
                max_places = max_precision
            number_formatters.append(
                utils.make_number_formatter(max_places, add_ellipsis))
        else:
            number_formatters.append(None)

    # Format data
    for i, row in enumerate(self._rows):
        if i >= max_rows:
            break

        formatted_row = []

        for j, v in enumerate(row):
            if j >= max_columns:
                v = ellipsis
            elif v is None:
                v = ''
            elif number_formatters[j] is not None and not math.isinf(v):
                v = format_decimal(v,
                                   format=number_formatters[j],
                                   locale=locale)
            else:
                v = six.text_type(v)

            if max_column_width is not None and len(v) > max_column_width:
                v = '%s...' % v[:max_column_width - 3]

            formatted_row.append(v)

            if j >= max_columns:
                break

        formatted_data.append(formatted_row)

    def write(line):
        output.write(line + '\n')

    def write_row(formatted_row):
        """
        Helper function that formats individual rows.
        """
        write('<tr>')

        for j, d in enumerate(formatted_row):
            # Text is left-justified, all other values are right-justified
            if isinstance(self._column_types[j], Text):
                write('<td style="text-align: left;">%s</td>' % d)
            else:
                write('<td style="text-align: right;">%s</td>' % d)

        write('</tr>')

    # Header
    write('<table>')
    write('<thead>')
    write('<tr>')

    for i, col in enumerate(column_names):
        write('<th>%s</th>' % col)

    write('</tr>')
    write('</thead>')
    write('<tbody>')

    # Rows
    for formatted_row in formatted_data:
        write_row(formatted_row)

    # Row indicating data was truncated
    if rows_truncated:
        write_row([ellipsis for n in column_names])

    # Footer
    write('</tbody>')
    write('</table>')
コード例 #6
0
ファイル: print_bars.py プロジェクト: jean/agate
def print_bars(self, label_column_name='group', value_column_name='Count', domain=None, width=120, output=sys.stdout, printable=False):
    """
    Print a text-based bar chart based on this table.

    :param label_column_name:
        The column containing the label values. Defaults to :code:`group`, which
        is the default output of :meth:`.Table.pivot` or :meth:`.Table.bins`.
    :param value_column_name:
        The column containing the bar values. Defaults to :code:`Count`, which
        is the default output of :meth:`.Table.pivot` or :meth:`.Table.bins`.
    :param domain:
        A 2-tuple containing the minimum and maximum values for the chart's
        x-axis. The domain must be large enough to contain all values in
        the column.
    :param width:
        The width, in characters, to use for the bar chart. Defaults to
        :code:`120`.
    :param output:
        A file-like object to print to. Defaults to :code:`sys.stdout`.
    :param printable:
        If true, only printable characters will be outputed.
    """
    tick_mark = config.get_option('tick_char')
    horizontal_line = config.get_option('horizontal_line_char')
    locale = config.get_option('default_locale')

    if printable:
        bar_mark = config.get_option('printable_bar_char')
        zero_mark = config.get_option('printable_zero_line_char')
    else:
        bar_mark = config.get_option('bar_char')
        zero_mark = config.get_option('zero_line_char')

    y_label = label_column_name
    label_column = self._columns[label_column_name]

    # if not isinstance(label_column.data_type, Text):
    #     raise ValueError('Only Text data is supported for bar chart labels.')

    x_label = value_column_name
    value_column = self._columns[value_column_name]

    if not isinstance(value_column.data_type, Number):
        raise DataTypeError('Only Number data is supported for bar chart values.')

    output = output
    width = width

    # Format numbers
    decimal_places = utils.max_precision(value_column)
    value_formatter = utils.make_number_formatter(decimal_places)

    formatted_labels = []

    for label in label_column:
        formatted_labels.append(six.text_type(label))

    formatted_values = []
    for value in value_column:
        if value is None:
            formatted_values.append('-')
        else:
            formatted_values.append(format_decimal(
                value,
                format=value_formatter,
                locale=locale
            ))

    max_label_width = max(max([len(l) for l in formatted_labels]), len(y_label))
    max_value_width = max(max([len(v) for v in formatted_values]), len(x_label))

    plot_width = width - (max_label_width + max_value_width + 2)

    min_value = Min(value_column_name).run(self)
    max_value = Max(value_column_name).run(self)

    # Calculate dimensions
    if domain:
        x_min = Decimal(domain[0])
        x_max = Decimal(domain[1])

        if min_value < x_min or max_value > x_max:
            raise ValueError('Column contains values outside specified domain')
    else:
        x_min, x_max = utils.round_limits(min_value, max_value)

    # All positive
    if x_min >= 0:
        x_min = Decimal('0')
        plot_negative_width = 0
        zero_line = 0
        plot_positive_width = plot_width - 1
    # All negative
    elif x_max <= 0:
        x_max = Decimal('0')
        plot_negative_width = plot_width - 1
        zero_line = plot_width - 1
        plot_positive_width = 0
    # Mixed signs
    else:
        spread = x_max - x_min
        negative_portion = (x_min.copy_abs() / spread)

        # Subtract one for zero line
        plot_negative_width = int(((plot_width - 1) * negative_portion).to_integral_value())
        zero_line = plot_negative_width
        plot_positive_width = plot_width - (plot_negative_width + 1)

    def project(value):
        if value >= 0:
            return plot_negative_width + int((plot_positive_width * (value / x_max)).to_integral_value())
        else:
            return plot_negative_width - int((plot_negative_width * (value / x_min)).to_integral_value())

    # Calculate ticks
    ticks = OrderedDict()

    # First tick
    ticks[0] = x_min
    ticks[plot_width - 1] = x_max

    tick_fractions = [Decimal('0.25'), Decimal('0.5'), Decimal('0.75')]

    # All positive
    if x_min >= 0:
        for fraction in tick_fractions:
            value = x_max * fraction
            ticks[project(value)] = value
    # All negative
    elif x_max <= 0:
        for fraction in tick_fractions:
            value = x_min * fraction
            ticks[project(value)] = value
    # Mixed signs
    else:
        # Zero tick
        ticks[zero_line] = Decimal('0')

        # Halfway between min and 0
        value = x_min * Decimal('0.5')
        ticks[project(value)] = value

        # Halfway between 0 and max
        value = x_max * Decimal('0.5')
        ticks[project(value)] = value

    decimal_places = utils.max_precision(ticks.values())
    tick_formatter = utils.make_number_formatter(decimal_places)

    ticks_formatted = OrderedDict()

    for k, v in ticks.items():
        ticks_formatted[k] = format_decimal(
            v,
            format=tick_formatter,
            locale=locale
        )

    def write(line):
        output.write(line + '\n')

    # Chart top
    top_line = u'%s %s' % (y_label.ljust(max_label_width), x_label.rjust(max_value_width))
    write(top_line)

    # Bars
    for i, label in enumerate(formatted_labels):
        value = value_column[i]
        if value == 0 or value is None:
            bar_width = 0
        elif value > 0:
            bar_width = project(value) - plot_negative_width
        elif value < 0:
            bar_width = plot_negative_width - project(value)

        label_text = label.ljust(max_label_width)
        value_text = formatted_values[i].rjust(max_value_width)

        bar = bar_mark * bar_width

        if value is not None and value >= 0:
            gap = (u' ' * plot_negative_width)

            # All positive
            if x_min <= 0:
                bar = gap + zero_mark + bar
            else:
                bar = bar + gap + zero_mark
        else:
            bar = u' ' * (plot_negative_width - bar_width) + bar

            # All negative or mixed signs
            if value is None or x_max > value:
                bar = bar + zero_mark

        bar = bar.ljust(plot_width)

        write('%s %s %s' % (label_text, value_text, bar))

    # Axis & ticks
    axis = horizontal_line * plot_width
    tick_text = u' ' * width

    for i, (tick, label) in enumerate(ticks_formatted.items()):
        # First tick
        if tick == 0:
            offset = 0
        # Last tick
        elif tick == plot_width - 1:
            offset = -(len(label) - 1)
        else:
            offset = int(-(len(label) / 2))

        pos = (width - plot_width) + tick + offset

        # Don't print intermediate ticks that would overlap
        if tick != 0 and tick != plot_width - 1:
            if tick_text[pos - 1:pos + len(label) + 1] != ' ' * (len(label) + 2):
                continue

        tick_text = tick_text[:pos] + label + tick_text[pos + len(label):]
        axis = axis[:tick] + tick_mark + axis[tick + 1:]

    write(axis.rjust(width))
    write(tick_text)