commit cc73095412523023fa1bcf914440e5a31e0818a0
parent 1b2746fc343b91123d03f0efa41f5ec98b37756f
Author: Kyle Milz <kyle@getaddrinfo.net>
Date:   Tue, 31 Mar 2015 20:40:52 -0600
gen_svg: redo
Diffstat:
| M | gen_svg |  |  | 234 | ++++++++++++++++++++++++++++++++++++++++++++++--------------------------------- | 
| M | pricechart.css |  |  | 32 | ++++++++++++++++++++++++++++++++ | 
2 files changed, 168 insertions(+), 98 deletions(-)
diff --git a/gen_svg b/gen_svg
@@ -20,154 +20,186 @@ my $dbh = get_dbh($cfg->{http}, undef, $args{v});
 
 my $svg_dir = $cfg->{http}{chroot} . $cfg->{http}{htdocs} . "/svg";
 xmkdir($svg_dir, $args{v});
-print "info: svg output is $svg_dir\n" if ($args{v});
 
-my ($left, $center, $right, $top, $middle, $bottom) = (30, 900, 70, 20, 160, 20);
+# we don't output svg's when -n is given
+print "info: svg output dir is $svg_dir\n" if ($args{v} && !$args{n});
+
+my ($left, $center, $right, $top, $middle, $bottom) = (10, 935, 55, 10, 150, 20);
 my $width = $right + $center + $left;
 my $height = $top + $middle + $bottom;
 
 print "info: r, c, l, w = $right, $center, $left, $width\n" if ($args{v});
 print "info: b, m, t, h = $bottom, $middle, $top, $height\n" if ($args{v});
 
-my $sql = "select date, price, color from prices where " .
-	"part_num = ? and vendor = ? order by date";
-my $point_sth = $dbh->prepare($sql);
+my $sql = "select manufacturer, part_num from products";
+my $parts_sth = $dbh->prepare($sql);
 
-$sql = "select distinct vendor from prices where part_num = ?";
+$sql = "select distinct retailer from prices where part_num = ? and manufacturer = ?";
 my $retailer_sth = $dbh->prepare($sql);
 
-$sql = "select manufacturer, part_num, description from products";
-my $parts_sth = $dbh->prepare($sql);
+$sql = "select date, price from prices where " .
+	"part_num = ? and retailer = ? order by date";
+my $point_sth = $dbh->prepare($sql);
 
 $parts_sth->execute();
-print "info: generating svg's  " if ($args{v});
+print "info: generating  " if ($args{v});
 my ($raw_total, $rendered_total, $points, $series) = (0, 0, 0, 0);
-while (my ($brand, $part_num, $description) = $parts_sth->fetchrow_array()) {
+while (my ($manufacturer, $part_num) = $parts_sth->fetchrow_array()) {
 	$raw_total++;
 	spin() if ($args{v});
 
-	# make sure we have at least one price to work with
-	$sql = "select min(date), max(date), min(price), max(price) " .
-		"from prices where part_num = ?";
-	my ($x_min, $x_max, $y_min, $y_max) =
-		$dbh->selectrow_array($sql, undef, $part_num);
+	# make sure we have at least two prices to work with
+	$sql = "select min(date), max(date), min(price), max(price), " .
+		"count(date), count(price) from prices where part_num = ?" .
+		"and manufacturer = ?";
+	my ($x_min, $x_max, $y_min, $y_max, $nx, $ny) =
+		$dbh->selectrow_array($sql, undef, $part_num, $manufacturer);
 	next unless (defined $x_min);
 
-	# avoid division by zero
+	$y_max = ceil($y_max / 10) * 10;
+	$y_min = floor($y_min / 10) * 10;
+
 	my ($domain, $range) = ($x_max - $x_min, $y_max - $y_min);
-	$domain = (24 * 60 * 60) if ($domain < (24 * 60 * 60));
+	$domain = 24 * 60 * 60 if ($domain <= 0);
 	$range = 20 if ($range < 20);
 
 	# 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 ($x_scale, $y_scale) = ($center / $domain, $middle / $range);
 
+	# make price labels along right side and lines across chart
+	my $num_labels = 5;
+	for (1..$num_labels) {
+		my $price = sprintf("%.2f", $y_max - $range * $_ / $num_labels);
+		my $y = $top + $middle * ($_ - 1) / ($num_labels - 1);
+
+		$svg->text(
+			id => "price_$_", x => $left + $center + 5, y => $y + 3,
+			class => "chart_price", "text-anchor" => "start"
+		)->cdata("\$$price");
+
+		$svg->line(
+			id => "line_$_", x1 => $left, y1 => $y,
+			x2 => $width - $right, y2 => $y,
+			class => "chart_rulers",
+		);
+	}
+
+	# make dates along the bottom
+	if ($domain == 0) {
+		$num_labels = 1;
+	}
+	for (1..$num_labels) {
+		my $time = $x_min + $_ * $domain / $num_labels;
+		my $date = strftime "%b %e %Y", localtime($time);
+		my $x = $left + ($_ - 1) / ($num_labels - 1) * $center;
+
+		# make the dates not hang off the ends of the chart
+		my $text_anchor = "middle";
+		$text_anchor = "start" if ($_ == 1);
+		$text_anchor = "end" if ($_ == $num_labels);
+
+		# print the dates along the x axis
+		$svg->text(
+			id => "date_$time", x => $x, y => $height,
+			class => "chart_date", "text-anchor" => $text_anchor
+		)->cdata($date);
+
+		# print the little tick marks down from the x axis
+		my $x_axis = $top + $middle;
+		$svg->line(
+			id => "date_marker_$_", x1 => $x, y1 => $x_axis,
+			x2 => $x, y2 => $x_axis + 5, class => "chart_rulers",
+		);
+	}
+
 	# render each retailer as a different series
-	$retailer_sth->execute($part_num);
+	$retailer_sth->execute($part_num, $manufacturer);
 	while (my ($retailer) = $retailer_sth->fetchrow_array()) {
 		my (@xs, @ys);
-		my $line_color = "#000";
+		my ($last_y, $last_price) = ("#000", 0, 0);
+
+		my $retailer_id = lc($retailer);
+		$retailer_id =~ s/ /_/;
+
+		$sql = "select url, color from retailers where name = ?";
+		my ($url, $color) = $dbh->selectrow_array($sql, undef, $retailer);
 
-		# gather all points in the series
+		# xlink:href's don't like raw ampersands
+		$url =~ s/&/&/g;
+
+		# gather all points in the retailers series
 		$point_sth->execute($part_num, $retailer);
-		while (my ($date, $price, $color) = $point_sth->fetchrow_array) {
+		while (my ($date, $price) = $point_sth->fetchrow_array) {
 			# transform and clamp real world coordinates
 			push @xs, ($date - $x_min) * $x_scale + $left;
-			push @ys, ($price - $y_min) * $y_scale + $top;
+			push @ys, $height - $bottom - ($price - $y_min) * $y_scale;
 
 			# small filled in circles to indicate data points
-			my $tag = $svg->anchor(
-				-href => $cfg->{retailers}{$retailer}{search_url} . $part_num,
+			$svg->anchor(
+				-href => $url . $part_num,
 				target => "new_window"
 			)->circle(
-				cx => $xs[-1], cy => $ys[-1],
-				r => 2,
-				style => {
-					"fill" => "#$color",
-					"stroke" => "#$color"
-				}
+				cx => $xs[-1], cy => $ys[-1], r => 2,
+				style => { "fill" => "#$color",
+					"stroke" => "#$color" }
 			);
-			$line_color = $color;
+
+			$last_y = $ys[-1];
+			$last_price = $price;
 			$points++;
 		}
-		my $points = $svg->get_path(
-			x => \@xs, y => \@ys, -type => "polyline",
-			-closed => "false"
-		);
 
-		# polyline sucks, spline would look nicer
-		my $tag = $svg->anchor(
-			-href => $cfg->{retailers}{$retailer}{search_url} . $part_num,
+		# helper to get svg style coordinates easily
+		my $points = $svg->get_path(x => \@xs, y => \@ys, -type => "path");
+
+		# path sucks, spline would look nicer
+		$svg->anchor(-href => $url . $part_num,
 			target => "new_window"
+		)->path(
+			%$points, id => "path_$retailer_id",
+			class => "chart_series",
+			style => { fill => "#$color", stroke => "#$color" }
 		);
-		$tag->polyline(
-			%$points, id => $retailer,
-			style => {
-				"fill-opacity" => 0, fill => "#$line_color",
-				stroke => "#$line_color", "stroke-width" => 2,
-			}
-		);
-		$series++;
-	}
 
-	# when graph is loaded make a sliding motion show the graph lines
-	my $mask = $svg->rectangle(
-		x => 0, y => 0, width => 1000, height => 250, rx => 0, ry => 0,
-		id => "mask", fill => "#FFF"
-	);
-	$mask->animate(
-		attributeName => "x", values => "0;1000", dur => "0.8s",
-		fill => "freeze", -method => ""
-	);
-
-	# prices along the side
-	my $num_labels = 5;
-	if ($range <= 20) {
-		$num_labels = 2;
-	}
-	for (0..$num_labels) {
-		my $price = $y_max - $range * $_ / $num_labels;
-		my $y = $top + $middle * $_ / $num_labels;
-
-		$svg->text(
-			id => $_, x => $left + $center + 20, y => $y,
-			style => "font-size: 14px; fill: #666",
-			"text-anchor" => "start"
-		)->cdata("\$$price");
-
-		$svg->line(
-			id => "line_$_", x1 => $left, y1 => $y,
-			x2 => $width - $right, y2 => $y,
-			fill => "#CCC", stroke => "#CCC",
-			"stroke-width" => 1,
+		# prepare the definition for a textPath
+		my $id = "text_path_$retailer_id";
+		$svg->defs()->path(
+			%$points, id => $id,
 		);
-	}
 
-	# dates along the bottom
-	for (0..$num_labels) {
-		my $time = $x_min + $_ * $domain / $num_labels;
-		my $date = strftime "%b %e %Y", localtime($time);
-		my $x = $left + $_ / $num_labels * $center;
+		# show retailer name along the start of the path
+		$svg->text(class => "chart_series_text", fill => "#$color"
+		)->tag("textPath", "xlink:href" => "#$id")->tag("tspan", "dy" => "-5",
+		)->cdata($retailer);
 
-		$svg->text(
-			id => "time_$time",
-			x => $x, y => $height,
-			style => "font-size: 12px; fill: #666",
-			"text-anchor" => "middle"
-		)->cdata($date);
+		# really, you only care about the latest price difference
+		# $svg->text(
+		# 	id => "price_for_$retailer_id", x => $left + $center + 5,
+		# 	y => $last_y + 5,
+		# 	style => "font-size: 12px; fill: #000",
+		# 	"text-anchor" => "start"
+		# )->cdata("\$$last_price");
 
-		$svg->line(
-			id => "date_marker_$_",
-			x1 => $x, y1 => $top + $middle,
-			x2 => $x, y2 => $top + $middle + 5,
-			fill => "#CCC",
-			stroke => "#CCC",
-			"stroke-width" => 1,
-		);
+		$series++;
 	}
 
+	# when graph is loaded make a sliding motion show the graph lines
+	# my $mask = $svg->rectangle(
+	# 	x => 0, y => 0, width => 1000, height => 250, rx => 0, ry => 0,
+	# 	id => "mask", fill => "#FFF"
+	# );
+	# $mask->animate(
+	# 	attributeName => "x", values => "0;1000", dur => "0.2s",
+	# 	fill => "freeze", -method => ""
+	# );
+
 	next if ($args{n});
 
+	# all links lower case
+	$part_num = lc($part_num);
+
 	# giant hack, if the part number has / in it, make some directories
 	if ($part_num =~ /\//) {
 		my $needed_dirs = substr($part_num, 0, rindex($part_num, '/'));
@@ -175,7 +207,13 @@ while (my ($brand, $part_num, $description) = $parts_sth->fetchrow_array()) {
 	}
 	open my $svg_fh, ">", "$svg_dir/$part_num.svg" or
 		die "couldn't open $svg_dir/$part_num: $!";
-	print $svg_fh $svg->xmlify;
+
+	# XXX: not sure how to add this programatically, hack around for now
+	my @buf = split("\n", $svg->xmlify);
+	my $css_include = "<?xml-stylesheet href=\"/pricechart.css\" type=\"text/css\"?>";
+	splice (@buf, 1, 0, ($css_include));
+
+	print $svg_fh "$_\n" for (@buf);
 	close $svg_fh;
 
 	$rendered_total++;
diff --git a/pricechart.css b/pricechart.css
@@ -50,3 +50,35 @@ p {
 	height: 1em;
 	vertical-align: middle;
 }
+
+/* horizontal rulers, plus date ticks */
+.chart_rulers {
+	stroke: #DDD;
+	stroke-width: 1;
+}
+
+/* x axis labels */
+.chart_date {
+	font-size: 0.75em;
+	fill: #666;
+}
+
+/* y axis labels */
+.chart_price {
+	font-size: 0.80em;
+	fill: black;
+}
+
+/* retailer date series */
+.chart_series {
+	fill-opacity: 0;
+	stroke-width: 2;
+}
+
+.chart_series:hover {
+	stroke-width: 4;
+}
+
+.chart_series_text {
+	font-size: 0.5em;
+}