Pygal: Change dot type/ symbol

423 Views Asked by At

I want to change the dots in my pygal chart from the default circles to rectangles (sounds weird but makes sense in my case) and be able to define the size of the rectangles. I couldn't find a solution in the docs. With the config module I can show/ hide the dots and change the dots size but as far as I can see I can't change the dot icon. I also coulndn't find a solution in the style module.

Is there an easy way to do it?

Thanks a lot

1

There are 1 best solutions below

0
On

There's no way to achieve this using styles or configuration: the circular dots are hard-coded into the function that renders line charts. But, you can easily extend the line chart class and override this function to create a chart with any shape of dot.

If you view the source code of the Line class you will see the following code in the line function:

alter(
    self.svg.transposable_node(
        dots,
        'circle',
        cx=x,
        cy=y,
        r=serie.dots_size,
        class_='dot reactive tooltip-trigger'
    ), metadata
)

This creates a circle for each dot and adds it to the SVG data that will be used to generate the chart.

Copy the whole function into your new class and replace those lines with the following code. This will add squares instead of circles, using the dots_size configuration to determine the width and height:

alter(
    self.svg.transposable_node(
        dots,
        'rect',
        x=x - serie.dots_size / 2,
        y=y - serie.dots_size / 2,
        width=serie.dots_size,
        height=serie.dots_size,
        class_='dot reactive tooltip-trigger'
    ), metadata
)

The complete class would look something like this (it looks like a lot of code, but most of it is copy-pasted):

from pygal.util import alter, decorate

class SquareDots(pygal.Line):

    def __init__(self, *args, **kwargs):
        super(SquareDots, self).__init__(*args, **kwargs)

    def line(self, serie, rescale=False):
        serie_node = self.svg.serie(serie)
        if rescale and self.secondary_series:
            points = self._rescale(serie.points)
        else:
            points = serie.points
        view_values = list(map(self.view, points))
        if serie.show_dots:
            for i, (x, y) in enumerate(view_values):
                if None in (x, y):
                    continue
                if self.logarithmic:
                    if points[i][1] is None or points[i][1] <= 0:
                        continue
                if (serie.show_only_major_dots and self.x_labels
                        and i < len(self.x_labels)
                        and self.x_labels[i] not in self._x_labels_major):
                    continue
                metadata = serie.metadata.get(i)
                classes = []
                if x > self.view.width / 2:
                    classes.append('left')
                if y > self.view.height / 2:
                    classes.append('top')
                classes = ' '.join(classes)
                self._confidence_interval(
                    serie_node['overlay'], x, y, serie.values[i], metadata
                )
                dots = decorate(
                    self.svg,
                    self.svg.node(serie_node['overlay'], class_="dots"),
                    metadata
                )
                val = self._format(serie, i)

                # This is the part that needs to be changed.
                alter(self.svg.transposable_node(
                        dots,
                        'rect',
                        x=x - serie.dots_size / 2,
                        y=y - serie.dots_size / 2,
                        width=serie.dots_size,
                        height=serie.dots_size,
                        class_='dot reactive tooltip-trigger'
                    ), metadata
                )

                self._tooltip_data(
                    dots, val, x, y, xlabel=self._get_x_label(i)
                )
                self._static_value(
                    serie_node, val, x + self.style.value_font_size,
                    y + self.style.value_font_size, metadata
                )
        if serie.stroke:
            if self.interpolate:
                points = serie.interpolated
                if rescale and self.secondary_series:
                    points = self._rescale(points)
                view_values = list(map(self.view, points))
            if serie.fill:
                view_values = self._fill(view_values)
            if serie.allow_interruptions:
                sequences = []
                cur_sequence = []
                for x, y in view_values:
                    if y is None and len(cur_sequence) > 0:
                        sequences.append(cur_sequence)
                        cur_sequence = []
                    elif y is None:
                        continue
                    else:
                        cur_sequence.append((x, y))
                if len(cur_sequence) > 0:
                    sequences.append(cur_sequence)
            else:
                sequences = [view_values]
            if self.logarithmic:
                for seq in sequences:
                    for ele in seq[::-1]:
                        y = points[seq.index(ele)][1]
                        if y is None or y <= 0:
                            del seq[seq.index(ele)]
            for seq in sequences:
                self.svg.line(
                    serie_node['plot'],
                    seq,
                    close=self._self_close,
                    class_='line reactive' +
                    (' nofill' if not serie.fill else '')
                )

Your new class can then be used like any other pygal chart.

chart = SquareDots(dots_size=50)
chart.add("line", [1, 2, 3, 4, 3, 2])
chart.render_to_png("chart.png")

Example chart with square dots