commit 6116f0017252dc0ab7b885d9a59499b8ebbd73c9
parent 8724eac311ca92d18eb33f4b3a2fdcf229e5f1d1
Author: Kyle Milz <kyle@getaddrinfo.net>
Date: Mon, 13 Apr 2015 23:49:37 -0600
gen_svg: first pass at catmull rom
Diffstat:
M | gen_svg | | | 117 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- |
M | pricechart.css | | | 16 | +++++++++++++--- |
2 files changed, 120 insertions(+), 13 deletions(-)
diff --git a/gen_svg b/gen_svg
@@ -67,7 +67,6 @@ while (my ($manufacturer, $part_num) = $parts_sth->fetchrow_array()) {
# clamp the total size of this thing with viewBox
my $svg = SVG->new(viewBox => "0 0 $width $height");
- # $svg->style(type => "text/css", -href => "/pricechart.css");
my $defs = $svg->defs();
my ($x_scale, $y_scale) = ($center / $domain, $middle / $range);
@@ -118,8 +117,6 @@ while (my ($manufacturer, $part_num) = $parts_sth->fetchrow_array()) {
# each series on the chart represents a retailers prices
$retailer_sth->execute($part_num, $manufacturer);
while (my ($retailer) = $retailer_sth->fetchrow_array()) {
- my (@xs, @ys);
-
my $retailer_id = lc($retailer);
$retailer_id =~ s/ /_/;
@@ -130,19 +127,28 @@ while (my ($manufacturer, $part_num) = $parts_sth->fetchrow_array()) {
$url =~ s/&/&/g;
# get all prices that we've scraped per product per retailer
+ my (@xs, @ys, @pts);
$point_sth->execute($part_num, $retailer);
while (my ($date, $price) = $point_sth->fetchrow_array) {
# transform and clamp real world coordinates
- push @xs, sprintf "%.2f", ($date - $x_min) * $x_scale + $left;
- push @ys, sprintf "%.2f", $height - $bottom - ($price - $y_min) * $y_scale;
+ push @xs, sprintf "%.3f", ($date - $x_min) * $x_scale + $left;
+ push @ys, sprintf "%.3f", $height - $bottom - ($price - $y_min) * $y_scale;
+ push @pts, $xs[-1];
+ push @pts, $ys[-1];
$points++;
}
- # path sucks, spline would look nicer
- my $points = $svg->get_path(x => \@xs, y => \@ys, -type => "path");
- $defs->path(%$points, id => "path_$retailer_id");
+ if (@pts < 6) {
+ my $points = $svg->get_path(x => \@xs, y => \@ys, -type => "path");
+ $defs->path(%$points, id => "path_$retailer_id");
+ }
+ else {
+ # catmull rom time
+ my $d = catmullrom_to_bezier(\@pts);
+ $defs->tag("path", "d" => $d, id => "path_$retailer_id");
+ }
- # all of the data points can be grouped under 1 anchor
+ # the line, points, and label can be grouped under one anchor
my $anchor = $svg->anchor(-href => $url . $part_num,
target => "new_window");
@@ -153,7 +159,7 @@ while (my ($manufacturer, $part_num) = $parts_sth->fetchrow_array()) {
# now draw individual data points
$defs->circle(id => "data_point_$retailer", cx => 0, cy => 0,
- r => 2, style => "stroke: #$color; fill: white; stroke-width: 2",
+ class => "chart_data", r => 2, style => "stroke: #$color;"
);
while (my $i = each @xs) {
$anchor->use(-href => "#data_point_$retailer",
@@ -223,3 +229,94 @@ sub spin
print "\b";
print $spin_states[++$state % 4];
}
+
+sub catmullrom_to_bezier
+{
+ my $pts_ref = shift;
+ my @pts = @$pts_ref;
+
+ # catmull-rom to cubic bezier conversion matrix
+ # 0 1 0 0
+ # -1/6 1 1/6 0
+ # 0 1/6 1 -1/6
+ # 0 0 1 0
+
+ my $d = "M" . $pts[0] . ", " . $pts[1] . " ";
+ my $iLen = @pts;
+ for (my $i = 0; $iLen - 2 > $i; $i += 2) {
+ my @p;
+ if ($i == 0) {
+ push @p, {x => $pts[$i], y => $pts[$i + 1]};
+ push @p, {x => $pts[$i], y => $pts[$i + 1]};
+ push @p, {x => $pts[$i + 2], y => $pts[$i + 3]};
+ push @p, {x => $pts[$i + 4], y => $pts[$i + 5]};
+ }
+ elsif ($i == ($iLen - 4)) {
+ push @p, {x => $pts[$i - 2], y => $pts[$i - 1]};
+ push @p, {x => $pts[$i], y => $pts[$i + 1]};
+ push @p, {x => $pts[$i + 2], y => $pts[$i + 3]};
+ push @p, {x => $pts[$i + 2], y => $pts[$i + 3]};
+ }
+ else {
+ push @p, {x => $pts[$i - 2], y => $pts[$i - 1]};
+ push @p, {x => $pts[$i], y => $pts[$i + 1]};
+ push @p, {x => $pts[$i + 2], y => $pts[$i + 3]};
+ push @p, {x => $pts[$i + 4], y => $pts[$i + 5]};
+ }
+
+ # print Dumper(@p);
+
+ my @bp;
+ push @bp, {x => $p[1]{x}, y => $p[1]{y}};
+ push @bp, {
+ x => ((-$p[0]{x} + 6*$p[1]{x} + $p[2]{x}) / 6),
+ y => ((-$p[0]{y} + 6*$p[1]{y} + $p[2]{y}) / 6)
+ };
+ push @bp, {
+ x => (($p[1]{x} + 6*$p[2]{x} - $p[3]{x}) / 6),
+ y => (($p[1]{y} + 6*$p[2]{y} - $p[3]{y}) / 6)
+ };
+ push @bp, {x => $p[2]{x}, y => $p[2]{y}};
+
+ $d .= "C " . $bp[1]{x} . ", " . $bp[1]{y} . " " .
+ $bp[2]{x} . ", " . $bp[2]{y} . " " .
+ $bp[3]{x} . ", " . $bp[3]{y} . " ";
+ }
+
+ # print $d;
+ return $d;
+}
+
+# function catmullRom2bezier( points ) {
+# // alert(points)
+# var crp = points.split(/[,\s]/);
+# var d = "";
+# for (var i = 0, iLen = crp.length; iLen - 2 > i; i+=2) {
+# var p = [];
+# if ( 0 == i ) {
+# p.push( {x: parseFloat(crp[ i ]), y: parseFloat(crp[ i + 1 ])} );
+# p.push( {x: parseFloat(crp[ i ]), y: parseFloat(crp[ i + 1 ])} );
+# p.push( {x: parseFloat(crp[ i + 2 ]), y: parseFloat(crp[ i + 3 ])} );
+# p.push( {x: parseFloat(crp[ i + 4 ]), y: parseFloat(crp[ i + 5 ])} );
+# } else if ( iLen - 4 == i ) {
+# p.push( {x: parseFloat(crp[ i - 2 ]), y: parseFloat(crp[ i - 1 ])} );
+# p.push( {x: parseFloat(crp[ i ]), y: parseFloat(crp[ i + 1 ])} );
+# p.push( {x: parseFloat(crp[ i + 2 ]), y: parseFloat(crp[ i + 3 ])} );
+# p.push( {x: parseFloat(crp[ i + 2 ]), y: parseFloat(crp[ i + 3 ])} );
+# } else {
+# p.push( {x: parseFloat(crp[ i - 2 ]), y: parseFloat(crp[ i - 1 ])} );
+# p.push( {x: parseFloat(crp[ i ]), y: parseFloat(crp[ i + 1 ])} );
+# p.push( {x: parseFloat(crp[ i + 2 ]), y: parseFloat(crp[ i + 3 ])} );
+# p.push( {x: parseFloat(crp[ i + 4 ]), y: parseFloat(crp[ i + 5 ])} );
+# }
+#
+# var bp = [];
+# bp.push( { x: p[1].x, y: p[1].y } );
+# bp.push( { x: ((-p[0].x + 6*p[1].x + p[2].x) / 6), y: ((-p[0].y + 6*p[1].y + p[2].y) / 6)} );
+# bp.push( { x: ((p[1].x + 6*p[2].x - p[3].x) / 6), y: ((p[1].y + 6*p[2].y - p[3].y) / 6) } );
+# bp.push( { x: p[2].x, y: p[2].y } );
+#
+# d += "C" + bp[1].x + "," + bp[1].y + " " + bp[2].x + "," + bp[2].y + " " + bp[3].x + "," + bp[3].y + " ";
+# }
+# return d;
+# }
diff --git a/pricechart.css b/pricechart.css
@@ -59,7 +59,7 @@ p {
/* horizontal rulers, plus date ticks */
.chart_rulers {
stroke: #BBB;
- stroke-width: 1;
+ stroke-width: 0.5;
}
/* x axis labels */
@@ -77,13 +77,23 @@ p {
/* retailer date series */
.chart_series {
fill-opacity: 0;
- stroke-width: 2;
+ stroke-width: 1;
}
.chart_series:hover {
- stroke-width: 3;
+ stroke-width: 2;
}
.chart_series_text {
font-size: 0.5em;
+ text-decoration: bold;
+}
+
+.chart_data {
+ fill: white;
+ stroke-width: 1;
+}
+
+.chart_data:hover {
+ stroke-width: 2;
}