<commit_before>use SVG::Box;

enum SVG::Plot::AxisPosition <Zero SmallestValue LargestValue>;

class SVG::Plot;
has $.height            = 300;
has $.width             = 500;
has $.fill-width        = 0.80;
has $.label-font-size   = 12;
has $.legend-font-size  = $.label-font-size;

has @.legends is rw;
has @.values  is rw;
has @.x       is rw;    # only used in 'xy' variants
has @.labels  is rw = @.values[0].keys;
has @.links   is rw;

has $.plot-width        = $.width  * 0.80;
has $.plot-height       = $.height * (@.legends ?? 0.5 !! 0.65);

has $.title             = '';

has &.x-tick-step       = -> $max {
    10 ** $max.log10.floor  / 2
}

has &.y-tick-step       = -> $max {
    10 ** $max.log10.floor  / 2
}

has $.max-x-labels      = $.plot-width / (1.5 * $.label-font-size);

has $.label-spacing     = ($.height - $.plot-height) / 20;

has @.colors = <#3333ff #ffdd66 #aa2222 #228844 #eebb00 #8822bb>;

multi method plot(:$full = True, :$stacked-bars!) {

    my $label-skip = ceiling(@.values[0] / $.max-x-labels);
    my $max_x      = @.values[0].elems;

    # maximum value of the sum over each column
    my $max_y      =  [max] @.values[0].keys.map: {
        [+] @.values.map: -> $a { $a[$_] }
    };
    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / $max_x;
    my $step_y     = $.plot-height / $max_y;

    my @svg_d = gather {
        my $bar-width = $.fill-width * $step_x;
        for @.values[0].keys Z @.labels -> $k, $l {
            my $y-offset  = 0;
            for ^$datasets -> $d {
                my $v = @.values[$d][$k];
                my $p = 'rect' => [
                    :y(-$v * $step_y - $y-offset),
                    :x($k * $step_x),
                    :width($bar-width),
                    :height($v * $step_y),
                    :style("fill:{ @.colors[$d % *] }; stroke: none"),
                ];
                $y-offset += $v * $step_y;
                take $.linkify($k, $p);
            }
        }

        $.plot-x-labels(:$step_x, :$label-skip);
        $.y-ticks(0, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    @.wrap-in-svg-header-if-necessary($svg, :wrap($full));
}

multi method plot(:$full = True, :$bars!) {

    my $label-skip = ceiling(@.values[0] / $.max-x-labels);
    my $max_x      = @.values[0].elems;
    my $max_y      = [max] @.values.map: { [max] @($_) };

    # the minimum is only interesting if it's smaller than 0.
    # if all the values are non-negative, the bars should still start
    # at 0
    my $min_y      = ([min] @.values.map: { [min] @($_) }) min 0;

    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / $max_x;
    my $step_y     = $.plot-height / ($max_y - $min_y);

    my @svg_d = gather {
        my $bar-width = $.fill-width * $step_x / $datasets;
        for @.values[0].keys Z @.labels -> $k, $l {
            for ^$datasets -> $d {
                my $v = @.values[$d][$k];
                my ($y, $h) = (($v - $min_y) * $step_y, $v * $step_y);
                if $h < 0 {
                    $y = abs($min_y * $step_y);
                    $h = abs($v * $step_y);
                }
                my $p = 'rect' => [
                    :y(-$y),
                    :x($k * $step_x + $d * $bar-width),
                    :width($bar-width),
                    :height(abs($h)),
                    :style("fill:{ @.colors[$d % *] }"),
                ];
                take $.linkify($k, $p);
            }
        }

        $.plot-x-labels(:$step_x, :$label-skip);
        $.y-ticks($min_y, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    @.wrap-in-svg-header-if-necessary($svg, :wrap($full));
}

multi method plot(:$full = True, :$points!) {

    my $label-skip = ceiling(@.values[0] / $.max-x-labels);
    my $max_x      = @.values[0].elems;
    my $max_y      = [max] @.values.map: { [max] @($_) };
    my $min_y      = [min] @.values.map: { [min] @($_) };
    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / $max_x;
    my $step_y     = $.plot-height / ($max_y - $min_y);

    my @svg_d = gather {
        for @.values[0].keys Z @.labels -> $k, $l {
            for ^$datasets -> $d {
                my $v = @.values[$d][$k];
                my $p = 'circle' => [
                    :cy(-($v-$min_y) * $step_y),
                    :cx(($k + 0.5) * $step_x),
                    :r(3),
                    :style("fill:{ @.colors[$d % @.colors.elems] }"),
                ];
                take $.linkify($k, $p);
            }
        }

        $.plot-x-labels(:$step_x, :$label-skip);
        $.y-ticks($min_y, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    @.wrap-in-svg-header-if-necessary($svg, :wrap($full));
}

multi method plot(:$full = True, :$xy-points!) {
    my $label-skip = ceiling(@.values[0] / $.max-x-labels);

    my $max_x      = [max] @.x;
    my $min_x      = [min] @.x;

    my $max_y      = [max] @.values.map: { [max] @($_) };
    my $min_y      = [min] @.values.map: { [min] @($_) };

    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / ($max_x - $min_x);
    my $step_y     = $.plot-height / ($max_y - $min_y);

    my @svg_d = gather {
        for @.values[0].keys -> $k {
            for ^$datasets -> $d {
                my $v = @.values[$d][$k];
                my $x = @.x[$k];

                my $p = 'circle' => [
                    :cy(-($v-$min_y) * $step_y),
                    :cx(($x - $min_x) * $step_x),
                    :r(3),
                    :style("fill:{ @.colors[$d % @.colors.elems] }"),
                ];
                take $.linkify($k, $p);
            }
        }

        $.x-ticks($min_x, $max_x, $step_x);
        $.y-ticks($min_y, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    @.wrap-in-svg-header-if-necessary($svg, :wrap($full));
}

multi method plot(:$full = True, :$xy-lines!) {
    my $label-skip = ceiling(@.values[0] / $.max-x-labels);

    my $max_x      = [max] @.x;
    my $min_x      = [min] @.x;

    if $max_x == $min_x {
        die "There's just one x value ($max_x), refusing to plot\n";
    }

    my $max_y      = [max] @.values.map: { [max] @($_) };
    my $min_y      = [min] @.values.map: { [min] @($_) };

    if $max_y == $min_y {
        die "There's just one y value ($max_x), refusing to plot\n";
    }

    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / ($max_x - $min_x);
    my $step_y     = $.plot-height / ($max_y - $min_y);

    my @svg_d = gather {
        for ^$datasets -> $d {
            my ($prev-x, $prev-y);
            for @.values[0].keys -> $k {
                my $v = @.values[$d][$k];
                my $x = @.x[$k];
                if defined $prev-x {
                    my $p = 'line' => [
                        :x1($prev-x),
                        :x2(($x - $min_x) * $step_x),
                        :y1($prev-y),
                        :y2(-($v-$min_y) * $step_y),
                        :style("stroke:{ @.colors[$d % @.colors.elems] }; stroke-width: 1.5"),
                    ];
                    take $.linkify($k, $p);
                }
                $prev-x = ($x - $min_x) * $step_x;
                $prev-y = -($v-$min_y) * $step_y;
            }
        }

        $.x-ticks($min_x, $max_x, $step_x);
        $.y-ticks($min_y, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    @.wrap-in-svg-header-if-necessary($svg, :wrap($full));
}

multi method plot(:$full = True, :$lines!) {

    my $label-skip = ceiling(@.values[0] / $.max-x-labels);
    my $max_x      = @.values[0].elems;
    my $max_y      = [max] @.values.map: { [max] @($_) };
    my $min_y      = [min] @.values.map: { [min] @($_) };
    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / $max_x;
    my $step_y     = $.plot-height / ($max_y - $min_y);

    my @svg_d = gather {
        for ^$datasets -> $d {
            my @previous-coordinates;
            for @.values[0].keys Z @.labels -> $k, $l {
                my $v = @.values[$d][$k];
                my @coord = ($k + 0.5) * $step_x, - ($v - $min_y) * $step_y;
                if @previous-coordinates {
                    my $p = 'line' => [
                        :x1(@previous-coordinates[0]),
                        :y1(@previous-coordinates[1]),
                        :x2(@coord[0]),
                        :y2(@coord[1]),
                        :style("stroke:{ @.colors[$d % @.colors.elems] }; stroke-width: 1.5"),
                    ];
                    take $.linkify($k, $p);
                }
                @previous-coordinates = @coord;
            }
        }

        $.plot-x-labels(:$step_x, :$label-skip);
        $.y-ticks($min_y, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    my $lb;
    if @.legends {
        $lb = $.plot-legend-box();
        $lb = self.apply-coordinate-transform(
            @($lb.svg),
            translate   => (($.width - $lb.width) / 2, $.height - $lb.height - 5),
        );
    } else {
        $lb = [];
    }
    @.wrap-in-svg-header-if-necessary($svg, $lb, :wrap($full));
}

method y-ticks($min_y, $max_y, $scale_y, $x = 0) {
    my $step = (&.y-tick-step).($max_y - $min_y);
    my $y_anchor = ($min_y / $step).Int * $step;

    loop (my $y = $y_anchor; $y <= $max_y; $y += $step) {
        take 'line' => [
            :x1($x - $.label-spacing / 2),
            :x2($x + $.label-spacing / 2),
            :y1(-($y - $min_y) * $scale_y),
            :y2(-($y - $min_y) * $scale_y),
            :style('stroke:black; stroke-width: 1'),
        ];
        take 'text' => [
            :x($x - 1.5 * $.label-spacing),
            :y(-($y - $min_y) * $scale_y),
            :font-size($.label-font-size),
            :text-anchor<end>,
            :dominant-baseline<middle>,
            ~ $y,
        ];
    }
}

method x-ticks($min_x, $max_x, $scale_x, $y = 0) {
    my $step = (&.x-tick-step).($max_x - $min_x);
    my $x_anchor = ($min_x / $step).Int * $step;

    loop (my $x = $x_anchor; $x <= $max_x; $x += $step) {
        take 'line' => [
            :y1($y - $.label-spacing / 2),
            :y2($y + $.label-spacing / 2),
            :x1(($x - $min_x) * $scale_x),
            :x2(($x - $min_x) * $scale_x),
            :style('stroke:black; stroke-width: 1'),
        ];
        take 'text' => [
            :y($y + 1.5 * ($.label-spacing + $.label-font-size)),
            :x(($x - $min_x) * $scale_x),
            :font-size($.label-font-size),
            :text-anchor<middle>,
            :dominant-baseline<middle>,
            ~ $x,
        ];
    }
}

method plot-x-labels(:$label-skip, :$step_x) {
    for @.values[0].keys Z @.labels -> $k, $l {
        if $k %% $label-skip {
            # note that the rotation is applied first,
            # so we have to  transform our
            # coordinates first:
            # x ->   y
            # y -> - x
            my $t = 'text' => [
                :transform('rotate(-90)'),
                :y(($k + 0.5 * $.fill-width) * $step_x),
                :x(-$.label-spacing),
                :font-size($.label-font-size),
                :dominant-baseline<middle>,
                :text-anchor<end>,
                ~$l,
            ];
            take $.linkify($k, $t);
        }
    }
}

# plots coordinate system, title etc.
multi method eyecandy() {
    @.plot-coordinate-system,
    @.plot-title,
}

multi method plot-coordinate-system() {
    # RAKUDO: can't use explicit return with a flattening list
    'line' => [
        :x1(0),
        :y1(0),
        :x2($.plot-width),
        :y2(0),
        :style('stroke:black; stroke-width: 2'),
    ],
    'line' => [
        :x1(0),
        :y1(0),
        :x2(0),
        :y2(-$.plot-height),
        :style('stroke:black; stroke-width: 2'),
    ];
}

multi method plot-title() {
#    return SVG::Box.new(
#
#    );
    return 'text' => [
        :x($.width * 0.5),
        :y(-$.plot-height - 0.1 * ($.height - $.plot-height) ),
        :text-anchor<middle>,
        $.title,
    ];
}

multi method apply-standard-transform(*@things) {
    $.apply-coordinate-transform(
        @things,
        translate   => (
            0.8 * ($.width - $.plot-width),
            $.plot-height + (@.legends ?? 0.2 !! 0.35)
            * ($.height - $.plot-height)
        ),
    );

}

multi method apply-coordinate-transform(*@things, :@translate) {
    return 'g' => [
        :transform("translate({@translate[0]},{@translate[1]})"),
        @things,
    ]
}

method linkify($key, *@things) {
    my $link = @.links[$key];
    defined($link)
        ?? ('a' => [
                'xlink:href' => $link,
                :target<_top>,
                @things
            ])
        !! @things;
}

multi method plot-legend-box() {
    my $height = 10 + 1.4 * $.legend-font-size * @.legends;
    my $width  = 10 + 0.7 * $.legend-font-size
                          * (([max] @.legends>>.chars) + 2);
    return unless @.legends;

    my $svg = gather {
        take 'rect' => [
            x       => 0,
            y       => 0,
            height  => $height,
            width   => $width,
            style   => 'stroke: black; strok-width: 0.5; fill: none',
        ];
        for @.legends.kv -> $i, $l {
            take 'rect' => [
                x       => 10,
                y       => 5 + 1.4 * $i * $.legend-font-size,
                height  => $.legend-font-size,
                width   => $.legend-font-size,
                style   => "stroke: black; stroke-width: 0.2; fill: "
                           ~ @.colors[$i % *] ~ ";",
            ];
            take 'text' => [
                x       => 2 * $.legend-font-size,
                y       => 5 + 1.4 * $i * $.legend-font-size
                           + 0.5 * $.legend-font-size,
                dominant-baseline   => 'central',
                $l,
            ];
        }
    }

    return SVG::Box.new(
        x       => 0,
        y       => 0,
        svg     => $svg,
        height  => $height,
        width   => $width,
        name    => 'legend',
    );
}

method wrap-in-svg-header-if-necessary(*@things, :$wrap) {
    return $wrap
        ??
            :svg([
                    :width($.width), :height($.height),
                    'xmlns' => 'http://www.w3.org/2000/svg',
                    'xmlns:svg' => 'http://www.w3.org/2000/svg',
                    'xmlns:xlink' => 'http://www.w3.org/1999/xlink',
                    @things
            ])
        !!@things;
}

=begin Pod

=head1 NAME

SVG::Plot - simple SVG bar charts

=head1 VERSION
<commit_msg>xmlns is now handled by SVG.pm<commit_after>use SVG::Box;

enum SVG::Plot::AxisPosition <Zero SmallestValue LargestValue>;

class SVG::Plot;
has $.height            = 300;
has $.width             = 500;
has $.fill-width        = 0.80;
has $.label-font-size   = 12;
has $.legend-font-size  = $.label-font-size;

has @.legends is rw;
has @.values  is rw;
has @.x       is rw;    # only used in 'xy' variants
has @.labels  is rw = @.values[0].keys;
has @.links   is rw;

has $.plot-width        = $.width  * 0.80;
has $.plot-height       = $.height * (@.legends ?? 0.5 !! 0.65);

has $.title             = '';

has &.x-tick-step       = -> $max {
    10 ** $max.log10.floor  / 2
}

has &.y-tick-step       = -> $max {
    10 ** $max.log10.floor  / 2
}

has $.max-x-labels      = $.plot-width / (1.5 * $.label-font-size);

has $.label-spacing     = ($.height - $.plot-height) / 20;

has @.colors = <#3333ff #ffdd66 #aa2222 #228844 #eebb00 #8822bb>;

multi method plot(:$full = True, :$stacked-bars!) {

    my $label-skip = ceiling(@.values[0] / $.max-x-labels);
    my $max_x      = @.values[0].elems;

    # maximum value of the sum over each column
    my $max_y      =  [max] @.values[0].keys.map: {
        [+] @.values.map: -> $a { $a[$_] }
    };
    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / $max_x;
    my $step_y     = $.plot-height / $max_y;

    my @svg_d = gather {
        my $bar-width = $.fill-width * $step_x;
        for @.values[0].keys Z @.labels -> $k, $l {
            my $y-offset  = 0;
            for ^$datasets -> $d {
                my $v = @.values[$d][$k];
                my $p = 'rect' => [
                    :y(-$v * $step_y - $y-offset),
                    :x($k * $step_x),
                    :width($bar-width),
                    :height($v * $step_y),
                    :style("fill:{ @.colors[$d % *] }; stroke: none"),
                ];
                $y-offset += $v * $step_y;
                take $.linkify($k, $p);
            }
        }

        $.plot-x-labels(:$step_x, :$label-skip);
        $.y-ticks(0, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    @.wrap-in-svg-header-if-necessary($svg, :wrap($full));
}

multi method plot(:$full = True, :$bars!) {

    my $label-skip = ceiling(@.values[0] / $.max-x-labels);
    my $max_x      = @.values[0].elems;
    my $max_y      = [max] @.values.map: { [max] @($_) };

    # the minimum is only interesting if it's smaller than 0.
    # if all the values are non-negative, the bars should still start
    # at 0
    my $min_y      = ([min] @.values.map: { [min] @($_) }) min 0;

    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / $max_x;
    my $step_y     = $.plot-height / ($max_y - $min_y);

    my @svg_d = gather {
        my $bar-width = $.fill-width * $step_x / $datasets;
        for @.values[0].keys Z @.labels -> $k, $l {
            for ^$datasets -> $d {
                my $v = @.values[$d][$k];
                my ($y, $h) = (($v - $min_y) * $step_y, $v * $step_y);
                if $h < 0 {
                    $y = abs($min_y * $step_y);
                    $h = abs($v * $step_y);
                }
                my $p = 'rect' => [
                    :y(-$y),
                    :x($k * $step_x + $d * $bar-width),
                    :width($bar-width),
                    :height(abs($h)),
                    :style("fill:{ @.colors[$d % *] }"),
                ];
                take $.linkify($k, $p);
            }
        }

        $.plot-x-labels(:$step_x, :$label-skip);
        $.y-ticks($min_y, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    @.wrap-in-svg-header-if-necessary($svg, :wrap($full));
}

multi method plot(:$full = True, :$points!) {

    my $label-skip = ceiling(@.values[0] / $.max-x-labels);
    my $max_x      = @.values[0].elems;
    my $max_y      = [max] @.values.map: { [max] @($_) };
    my $min_y      = [min] @.values.map: { [min] @($_) };
    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / $max_x;
    my $step_y     = $.plot-height / ($max_y - $min_y);

    my @svg_d = gather {
        for @.values[0].keys Z @.labels -> $k, $l {
            for ^$datasets -> $d {
                my $v = @.values[$d][$k];
                my $p = 'circle' => [
                    :cy(-($v-$min_y) * $step_y),
                    :cx(($k + 0.5) * $step_x),
                    :r(3),
                    :style("fill:{ @.colors[$d % @.colors.elems] }"),
                ];
                take $.linkify($k, $p);
            }
        }

        $.plot-x-labels(:$step_x, :$label-skip);
        $.y-ticks($min_y, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    @.wrap-in-svg-header-if-necessary($svg, :wrap($full));
}

multi method plot(:$full = True, :$xy-points!) {
    my $label-skip = ceiling(@.values[0] / $.max-x-labels);

    my $max_x      = [max] @.x;
    my $min_x      = [min] @.x;

    my $max_y      = [max] @.values.map: { [max] @($_) };
    my $min_y      = [min] @.values.map: { [min] @($_) };

    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / ($max_x - $min_x);
    my $step_y     = $.plot-height / ($max_y - $min_y);

    my @svg_d = gather {
        for @.values[0].keys -> $k {
            for ^$datasets -> $d {
                my $v = @.values[$d][$k];
                my $x = @.x[$k];

                my $p = 'circle' => [
                    :cy(-($v-$min_y) * $step_y),
                    :cx(($x - $min_x) * $step_x),
                    :r(3),
                    :style("fill:{ @.colors[$d % @.colors.elems] }"),
                ];
                take $.linkify($k, $p);
            }
        }

        $.x-ticks($min_x, $max_x, $step_x);
        $.y-ticks($min_y, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    @.wrap-in-svg-header-if-necessary($svg, :wrap($full));
}

multi method plot(:$full = True, :$xy-lines!) {
    my $label-skip = ceiling(@.values[0] / $.max-x-labels);

    my $max_x      = [max] @.x;
    my $min_x      = [min] @.x;

    if $max_x == $min_x {
        die "There's just one x value ($max_x), refusing to plot\n";
    }

    my $max_y      = [max] @.values.map: { [max] @($_) };
    my $min_y      = [min] @.values.map: { [min] @($_) };

    if $max_y == $min_y {
        die "There's just one y value ($max_x), refusing to plot\n";
    }

    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / ($max_x - $min_x);
    my $step_y     = $.plot-height / ($max_y - $min_y);

    my @svg_d = gather {
        for ^$datasets -> $d {
            my ($prev-x, $prev-y);
            for @.values[0].keys -> $k {
                my $v = @.values[$d][$k];
                my $x = @.x[$k];
                if defined $prev-x {
                    my $p = 'line' => [
                        :x1($prev-x),
                        :x2(($x - $min_x) * $step_x),
                        :y1($prev-y),
                        :y2(-($v-$min_y) * $step_y),
                        :style("stroke:{ @.colors[$d % @.colors.elems] }; stroke-width: 1.5"),
                    ];
                    take $.linkify($k, $p);
                }
                $prev-x = ($x - $min_x) * $step_x;
                $prev-y = -($v-$min_y) * $step_y;
            }
        }

        $.x-ticks($min_x, $max_x, $step_x);
        $.y-ticks($min_y, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    @.wrap-in-svg-header-if-necessary($svg, :wrap($full));
}

multi method plot(:$full = True, :$lines!) {

    my $label-skip = ceiling(@.values[0] / $.max-x-labels);
    my $max_x      = @.values[0].elems;
    my $max_y      = [max] @.values.map: { [max] @($_) };
    my $min_y      = [min] @.values.map: { [min] @($_) };
    my $datasets   = +@.values;

    my $step_x     = $.plot-width  / $max_x;
    my $step_y     = $.plot-height / ($max_y - $min_y);

    my @svg_d = gather {
        for ^$datasets -> $d {
            my @previous-coordinates;
            for @.values[0].keys Z @.labels -> $k, $l {
                my $v = @.values[$d][$k];
                my @coord = ($k + 0.5) * $step_x, - ($v - $min_y) * $step_y;
                if @previous-coordinates {
                    my $p = 'line' => [
                        :x1(@previous-coordinates[0]),
                        :y1(@previous-coordinates[1]),
                        :x2(@coord[0]),
                        :y2(@coord[1]),
                        :style("stroke:{ @.colors[$d % @.colors.elems] }; stroke-width: 1.5"),
                    ];
                    take $.linkify($k, $p);
                }
                @previous-coordinates = @coord;
            }
        }

        $.plot-x-labels(:$step_x, :$label-skip);
        $.y-ticks($min_y, $max_y, $step_y);
    }

    my $svg = $.apply-standard-transform(
        @svg_d,
        @.eyecandy(),
    );

    my $lb;
    if @.legends {
        $lb = $.plot-legend-box();
        $lb = self.apply-coordinate-transform(
            @($lb.svg),
            translate   => (($.width - $lb.width) / 2, $.height - $lb.height - 5),
        );
    } else {
        $lb = [];
    }
    @.wrap-in-svg-header-if-necessary($svg, $lb, :wrap($full));
}

method y-ticks($min_y, $max_y, $scale_y, $x = 0) {
    my $step = (&.y-tick-step).($max_y - $min_y);
    my $y_anchor = ($min_y / $step).Int * $step;

    loop (my $y = $y_anchor; $y <= $max_y; $y += $step) {
        take 'line' => [
            :x1($x - $.label-spacing / 2),
            :x2($x + $.label-spacing / 2),
            :y1(-($y - $min_y) * $scale_y),
            :y2(-($y - $min_y) * $scale_y),
            :style('stroke:black; stroke-width: 1'),
        ];
        take 'text' => [
            :x($x - 1.5 * $.label-spacing),
            :y(-($y - $min_y) * $scale_y),
            :font-size($.label-font-size),
            :text-anchor<end>,
            :dominant-baseline<middle>,
            ~ $y,
        ];
    }
}

method x-ticks($min_x, $max_x, $scale_x, $y = 0) {
    my $step = (&.x-tick-step).($max_x - $min_x);
    my $x_anchor = ($min_x / $step).Int * $step;

    loop (my $x = $x_anchor; $x <= $max_x; $x += $step) {
        take 'line' => [
            :y1($y - $.label-spacing / 2),
            :y2($y + $.label-spacing / 2),
            :x1(($x - $min_x) * $scale_x),
            :x2(($x - $min_x) * $scale_x),
            :style('stroke:black; stroke-width: 1'),
        ];
        take 'text' => [
            :y($y + 1.5 * ($.label-spacing + $.label-font-size)),
            :x(($x - $min_x) * $scale_x),
            :font-size($.label-font-size),
            :text-anchor<middle>,
            :dominant-baseline<middle>,
            ~ $x,
        ];
    }
}

method plot-x-labels(:$label-skip, :$step_x) {
    for @.values[0].keys Z @.labels -> $k, $l {
        if $k %% $label-skip {
            # note that the rotation is applied first,
            # so we have to  transform our
            # coordinates first:
            # x ->   y
            # y -> - x
            my $t = 'text' => [
                :transform('rotate(-90)'),
                :y(($k + 0.5 * $.fill-width) * $step_x),
                :x(-$.label-spacing),
                :font-size($.label-font-size),
                :dominant-baseline<middle>,
                :text-anchor<end>,
                ~$l,
            ];
            take $.linkify($k, $t);
        }
    }
}

# plots coordinate system, title etc.
multi method eyecandy() {
    @.plot-coordinate-system,
    @.plot-title,
}

multi method plot-coordinate-system() {
    # RAKUDO: can't use explicit return with a flattening list
    'line' => [
        :x1(0),
        :y1(0),
        :x2($.plot-width),
        :y2(0),
        :style('stroke:black; stroke-width: 2'),
    ],
    'line' => [
        :x1(0),
        :y1(0),
        :x2(0),
        :y2(-$.plot-height),
        :style('stroke:black; stroke-width: 2'),
    ];
}

multi method plot-title() {
#    return SVG::Box.new(
#
#    );
    return 'text' => [
        :x($.width * 0.5),
        :y(-$.plot-height - 0.1 * ($.height - $.plot-height) ),
        :text-anchor<middle>,
        $.title,
    ];
}

multi method apply-standard-transform(*@things) {
    $.apply-coordinate-transform(
        @things,
        translate   => (
            0.8 * ($.width - $.plot-width),
            $.plot-height + (@.legends ?? 0.2 !! 0.35)
            * ($.height - $.plot-height)
        ),
    );

}

multi method apply-coordinate-transform(*@things, :@translate) {
    return 'g' => [
        :transform("translate({@translate[0]},{@translate[1]})"),
        @things,
    ]
}

method linkify($key, *@things) {
    my $link = @.links[$key];
    defined($link)
        ?? ('a' => [
                'xlink:href' => $link,
                :target<_top>,
                @things
            ])
        !! @things;
}

multi method plot-legend-box() {
    my $height = 10 + 1.4 * $.legend-font-size * @.legends;
    my $width  = 10 + 0.7 * $.legend-font-size
                          * (([max] @.legends>>.chars) + 2);
    return unless @.legends;

    my $svg = gather {
        take 'rect' => [
            x       => 0,
            y       => 0,
            height  => $height,
            width   => $width,
            style   => 'stroke: black; strok-width: 0.5; fill: none',
        ];
        for @.legends.kv -> $i, $l {
            take 'rect' => [
                x       => 10,
                y       => 5 + 1.4 * $i * $.legend-font-size,
                height  => $.legend-font-size,
                width   => $.legend-font-size,
                style   => "stroke: black; stroke-width: 0.2; fill: "
                           ~ @.colors[$i % *] ~ ";",
            ];
            take 'text' => [
                x       => 2 * $.legend-font-size,
                y       => 5 + 1.4 * $i * $.legend-font-size
                           + 0.5 * $.legend-font-size,
                dominant-baseline   => 'central',
                $l,
            ];
        }
    }

    return SVG::Box.new(
        x       => 0,
        y       => 0,
        svg     => $svg,
        height  => $height,
        width   => $width,
        name    => 'legend',
    );
}

method wrap-in-svg-header-if-necessary(*@things, :$wrap) {
    return $wrap
        ??
            :svg([
                    :width($.width), :height($.height),
                    @things
            ])
        !!@things;
}

=begin Pod

=head1 NAME

SVG::Plot - simple SVG bar charts

=head1 VERSION

{'fraction_non_alphanumeric': 0.2009889666307858, 'fraction_numerical': 0.013085306781485468, 'mean_word_length': 1.3579473350253808, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 150, 'https://': 0, 'lorem ipsum': 0, 'www.': 3, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1404950', 'n_tokens_mistral': 11181, 'n_tokens_neox': 10602, 'n_words': 2123}