User:BenRG/Source code

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search

This page has the (ugly, hacked together) source code used to generate some of the images I've uploaded to Commons.

Cone fundamentals[edit]

This Perl script produced File:Cone-fundamentals-with-srgb-spectrum.svg. Needs #Svg.pm and #FitCurves.

(hidden)
use strict;
use Svg;

sub MakeGrad {
	my ($grad,$a) = @_;
	my ($lo,$hi) = ($$a[0][0], $$a[$#$a][0]);
	for my $stop (@$a) {
		$grad->stop(offset => int(($$stop[0]-$lo)/($hi-$lo)*100+0.5) . '%', 'stop-color' => $$stop[1]);
	}
}

# cone fundamentals in steps of 5nm from 390nm to 710nm, normalized to max=1000
my @lfund = (0,1,2,5,9,13,18,23,28,34,40,45,50,55,65,81,99,119,140,164,192,233,289,360,444,536,629,705,771,826,881,919,940,966,981,994,1000,992,969,956,928,886,834,775,706,631,554,480,401,328,266,213,165,125,93,69,50,36,25,18,12,8,6,4,3);
my @mfund = (0,1,2,5,9,15,22,30,39,52,65,76,87,98,116,145,176,205,236,268,304,357,428,516,616,719,817,886,936,969,995,997,977,957,918,873,814,740,653,573,493,411,334,265,205,156,117,86,62,44,31,22,15,11,7,5,3,2,2,1,1,1,0,0,0);
my @sfund = (10,24,57,122,233,381,544,674,803,904,991,992,955,860,787,738,646,516,390,290,212,161,123,89,61,43,29,19,13,8,5,3,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);

my @colorgradient1 = ([390,'#000001'],[400,'#010002'],[410,'#020007'],[420,'#060013'],[440,'#170039'],[450,'#1D004E'],[455,'#1D005B'],[460,'#18006B'],[465,'#000879'],[470,'#002061'],[480,'#00334D'],[490,'#004245'],[500,'#005446'],[520,'#007C4F'],[530,'#00884D'],[535,'#008C48'],[545,'#00912F'],[550,'#159300'],[555,'#409000'],[570,'#7E8000'],[585,'#A56400'],[595,'#B44A00'],[605,'#BA2700'],[610,'#B90C00'],[615,'#B00016'],[620,'#A4001F'],[630,'#8B0021'],[640,'#72001D'],[670,'#310009'],[680,'#220005'],[700,'#0D0001'],[710,'#070001']);
my @colorgradient2 = ([390,'#8E8D8F'],[400,'#8E8D93'],[410,'#908B9C'],[420,'#9587B6'],[430,'#9C7FDA'],[440,'#9C7DE8'],[450,'#9780E9'],[460,'#8F86E5'],[470,'#8390D5'],[490,'#68A6AA'],[500,'#55B29C'],[510,'#30C292'],[520,'#00D189'],[530,'#33D985'],[540,'#5FDB82'],[555,'#96D681'],[570,'#C5C783'],[585,'#EAB186'],[595,'#FAA088'],[605,'#FF918A'],[615,'#FA888B'],[625,'#EB848C'],[655,'#B1898D'],[670,'#9D8C8D'],[690,'#928D8E'],[710,'#8F8D8E']);

my ($bgcolor,$legendcolor,$tracecolor,$spectrum) = (qw(white black white), \@colorgradient1);
#my ($bgcolor,$legendcolor,$tracecolor,$spectrum) = (qw(black white black), \@colorgradient2);
my $graph_width = 480;
my $graph_height = 300;
my $spectrum_top_padding = 10;
my $graph_ofs_x = 40;
my $graph_ofs_y = 20;
my $image_width = $graph_ofs_x + $graph_width + 20;
my $image_height = $graph_ofs_y + $graph_height + 60;

my $svg = new Svg(width => $image_width, height => $image_height, fill => 'none');
MakeGrad($svg->defs()->linearGradient(id => 'spectrum'), $spectrum);

$svg->rect(width => $image_width, height => $image_height, fill => $bgcolor);

my $graph = $svg->group(stroke => $tracecolor, 'stroke-width' => 3, 'stroke-linecap' => 'square');
$graph->translate($graph_ofs_x, $graph_ofs_y);
$graph->rect(x => 0, y => 0, width => $graph_width, height => $graph_height, stroke => 'none', fill => 'url(#spectrum)');
for my $a (\@lfund,\@mfund,\@sfund) {
	$graph->path(d => MakePath(Interpolate([0..$#$a], $a, 4), $graph_width / $#$a, ($graph_height-$spectrum_top_padding)/-1000, 0, $graph_height, 0.25));
}
$graph->line(x1 => 0, y1 => $graph_height, x2 => $graph_width, y2 => $graph_height, stroke => $bgcolor);

my $text = $svg->group('font-family' => 'Nimbus Roman No9 L, Times, serif', 'font-size' => 20, 'text-anchor' => 'middle', stroke => 'none', fill => $legendcolor);
my $ticks = do { my $w = $graph_width+19; my $h = $graph_height+19; "M30.5,10.5l$w,0l0,${h}l-$w,0l0,-$h" };
for (my $i = 390; $i <= 710; $i += 10) {
	my $h = $i % 50 ? 3 : 6;
	my $x = $graph_ofs_x + 0.5 + $graph_width * ($i-390)/(710-390);
	my $y2 = $graph_ofs_y + $graph_height + 9.5 - $h;
	$ticks .= "M$x,10.5l0,${h}M$x,${y2}l0,$h";
	if ($i % 50 == 0) {
		$text->text(x => $x, y => $graph_ofs_y + $graph_height + 30)->add($i);
	}
}
$text->text(x => $graph_ofs_x + $graph_width * 0.5, y => $graph_ofs_y + $graph_height + 50)->add('Wavelength (nm)');
$text->text()->rotate(-90)->translate(20, $graph_ofs_y + $graph_height/2)->add('Normalized cone response (linear energy)');
my $sml_text = $text->group(fill => $tracecolor);
$sml_text->text(x => 145, y => 50)->add('S');
$sml_text->text(x => 230, y => 50)->add('M');
$sml_text->text(x => 355, y => 50)->add('L');
$svg->path(stroke => $legendcolor, 'stroke-width' => 1, d => $ticks);

$svg->write('Cone-fundamentals-with-srgb-spectrum.svg');

Friedmann universes[edit]

This Perl script produced File:Friedmann_universes.svg. Needs #Svg.pm and #FitCurves.

(hidden)
use strict;
use Svg;
use Math::Trig qw(sinh cosh acos asinh acosh pi);

sub ScaleFunc {
	my ($H0, $M0, $with_lambda) = @_;
	if ($M0 == 1) {
		my $q0 = 2/(3*$H0);
		return sub { my ($q) = @_; ($q - $q0, (1.5 * $H0 * $q) ** (2/3)) };
	}
	if ($with_lambda) {
		my $L0 = 1 - $M0;
		# assume 0 < $M0 < 1
		my $a = ($M0/$L0) ** (1/3);
		my $b = 1.5 * $H0 * sqrt($L0);
		my $q0 = asinh(sqrt($L0/$M0)) / $b;
		return sub { my ($q) = @_; ($q - $q0, $a * (sinh($b * $q) ** (2/3))) }
	} else {
		# \Omega_{\Lambda_0} = 0
		my $k0 = 1 - $M0;
		if ($M0 == 0) {
			return sub { my ($q) = @_; ($q - 1/$H0, $q * $H0) }
		} else {
			my $a = $M0 / (2 * abs($k0));
			my $b = 1 / ($H0 * sqrt(abs($k0)));
			my $c = $a * $b;
			if ($M0 > 1) {
				my $d = $a * (2 / ($H0 * $M0) - acos(2/$M0 - 1) * $b);
				return sub { my ($q) = @_; ($c * ($q - sin($q)) + $d, $a * (1 - cos($q))) }
			} else {
				# 0 < M < 1
				my $d = $a * (acosh(2/$M0 - 1) * $b - 2 / ($H0 * $M0));
				return sub { my ($q) = @_; ($c * (sinh($q) - $q) + $d, $a * (cosh($q) - 1)) }
			}
		}
	}
}

sub SubscriptedText {
	my $text = shift;
	$text->add(shift);
	my $sub = 0;
	for my $t (@_) {
		$sub = !$sub;
		$text->tspan($sub ? (dy => 4, 'font-size' => 12) : (dy => -4))->add($t);
	}
}

my ($image_width,$image_height) = (620,500);
my ($origin_x, $origin_y) = (30.5,450.5);
my $pad_right = 70;
my ($tlo, $thi, $ahi) = (-15,18,2.5);

my $svg = new Svg(width => $image_width, height => $image_height);
#	$svg->rect(width => $image_width, height => $image_height, fill => 'gray');
$svg->defs()->marker(id => 'arrowhead', refX => 0, refY => 3, markerWidth => 10, markerHeight => 6, markerUnits => 'userSpaceOnUse', orient => 'auto')->path(d => 'M 0,0 L 10,3 L 0,6 z');
my $traces = $svg->group(stroke => 'black', 'stroke-width' => 2, fill => 'none');
my $axes = $svg->group(stroke => 'black', 'stroke-width' => 1, fill => 'none');
my $labels = $svg->group('font-family' => 'Nimbus Roman No9 L, Times, serif', 'font-size' => 20, 'text-anchor' => 'middle', stroke => 'none', fill => 'black');
my $H0 = 1 / 13.95;
my $M0 = 0.279;
my ($graphscalex,$graphscaley) = (($image_width-$origin_x-$pad_right)/($thi-$tlo), -$origin_y/$ahi);
my ($graphofsx,$graphofsy) = ($origin_x - $tlo * $graphscalex, $origin_y);
for my $z ([0,0,30,'none'],[$M0,0,3.17,'1,4'],[1,0,26,'2,2'],[6,0,2*pi,'1,3,4,3'],[$M0,1,27,'5,3']) {
	my ($m0,$with_lambda,$max_q,$dashes) = @$z;
	my $f = ScaleFunc($H0,$m0,$with_lambda);
	my (@x,@y);
	for my $i (0..200) {
		($x[$i],$y[$i]) = &$f($i / 200 * $max_q);
	}
	$traces->path('stroke-dasharray' => $dashes, ($m0 == 6 ? () : ('marker-end' => 'url(#arrowhead)')), d => MakePath(\@x, \@y, $graphscalex, $graphscaley, $graphofsx, $graphofsy, 1));
}
$axes->line(x1 => $origin_x, y1 => $image_height-20, x2 => $origin_x, y2 => 20, 'marker-end' => 'url(#arrowhead)');
$axes->line(x1 => 10, y1 => $origin_y, x2 => $image_width - $pad_right + 10, y2 => $origin_y, 'marker-end' => 'url(#arrowhead)');
$labels->text(x => ($origin_x + $image_width) / 2, y => $image_height-10)->add('Billions of years from now');
my $path = '';
for my $gyr (-13.7, -10, -5, 0, 5, 10, 15) {
	my $x = int($gyr * $graphscalex + $graphofsx);
	my $y = $origin_y-5;
	$path .= "M$x.5,${y}l0,10";
	$labels->text(x => $x, y => $origin_y + 20)->add($gyr);
}
$axes->path(d => $path);
$labels->circle(cx => $graphofsx, cy => $graphscaley + $graphofsy, r => 4);
$labels->text(x => $graphofsx-5, y => $graphscaley + $graphofsy, 'text-anchor' => 'end')->add('Now');
$labels->text()->rotate(-90)->translate($origin_x - 8, $origin_y / 2)->add("Average distance between galaxies");
my $trace_labels = $labels->group('font-family' => 'DejaVu Serif, serif', 'font-size' => 16);
SubscriptedText($trace_labels->text(x => 465, y => 30, 'text-anchor' => 'end'), "\x{3A9}", 'M', " = 0.3, \x{3A9}", "\x{39B}", " = 0.7");
SubscriptedText($trace_labels->text(x => 520, y => 50, 'text-anchor' => 'start'), "\x{3A9}", 'M', ' = 0');
SubscriptedText($trace_labels->text(x => 535, y => 70, 'text-anchor' => 'start'), "\x{3A9}", 'M', ' = 0.3');
SubscriptedText($trace_labels->text(x => 540, y => 95, 'text-anchor' => 'start'), "\x{3A9}", 'M', ' = 1');
SubscriptedText($trace_labels->text(x => 540, y => 400, 'text-anchor' => 'start'), "\x{3A9}", 'M', ' = 6');

$svg->write('Friedmann universes.svg');

CIE1931xy_*.svg[edit]

This Perl script produced File:CIE1931xy_blank.svg, File:CIE1931xy_sRGB.svg, File:CIE1931xy_AdobeRGB.svg, and File:CIE1931xy_CIERGB.svg. Needs #Svg.pm and #FitCurves.

(hidden)
use strict;
use Svg;
use List::Util qw(min);

my @primaries;
my $whitepoint = '';
my $filename;

if (1) {
	$filename = 'CIE1931xy_blank.svg';
} elsif (0) {
	$filename = 'CIE1931xy_sRGB.svg';
	@primaries = ([0.64,0.33],[0.30,0.60],[0.15,0.06]);
	$whitepoint = [0.3127,0.3290,'D65'];
} elsif (0) {
	$filename = 'CIE1931xy_AdobeRGB.svg';
	@primaries = ([0.64,0.33],[0.21,0.71],[0.15,0.06]);
	$whitepoint = [0.3127,0.3290,'D65'];
} elsif (0) {
	@primaries = ([0.7347,0.2653],[0.2738,0.7174],[0.1666,0.0089]);
	$whitepoint = [1/3, 1/3, 'E'];
	$filename = 'CIE1931xy_CIERGB.svg';
}

# in 5 nm steps from 360 to 830 nm, from http://www-cvrl.ucsd.edu/database/data/cmfs/ciexyz31.txt
my @xcmf = (
	0.0001299,0.0002321,0.0004149,0.0007416,0.001368,
	0.002236,0.004243,0.00765,0.01431,0.02319,
	0.04351,0.07763,0.13438,0.21477,0.2839,
	0.3285,0.34828,0.34806,0.3362,0.3187,
	0.2908,0.2511,0.19536,0.1421,0.09564,
	0.05795001,0.03201,0.0147,0.0049,0.0024,
	0.0093,0.0291,0.06327,0.1096,0.1655,
	0.2257499,0.2904,0.3597,0.4334499,0.5120501,
	0.5945,0.6784,0.7621,0.8425,0.9163,
	0.9786,1.0263,1.0567,1.0622,1.0456,
	1.0026,0.9384,0.8544499,0.7514,0.6424,
	0.5419,0.4479,0.3608,0.2835,0.2187,
	0.1649,0.1212,0.0874,0.0636,0.04677,
	0.0329,0.0227,0.01584,0.01135916,0.008110916,
	0.005790346,0.004109457,0.002899327,0.00204919,0.001439971,
	0.0009999493,0.0006900786,0.0004760213,0.0003323011,0.0002348261,
	0.0001661505,0.000117413,0.00008307527,0.00005870652,0.00004150994,
	0.00002935326,0.00002067383,0.00001455977,0.00001025398,0.000007221456,
	0.000005085868,0.000003581652,0.000002522525,0.000001776509,0.000001251141);
my @ycmf = (
	0.000003917,0.000006965,0.00001239,0.00002202,0.000039,
	0.000064,0.00012,0.000217,0.000396,0.00064,
	0.00121,0.00218,0.004,0.0073,0.0116,
	0.01684,0.023,0.0298,0.038,0.048,
	0.06,0.0739,0.09098,0.1126,0.13902,
	0.1693,0.20802,0.2586,0.323,0.4073,
	0.503,0.6082,0.71,0.7932,0.862,
	0.9148501,0.954,0.9803,0.9949501,1,
	0.995,0.9786,0.952,0.9154,0.87,
	0.8163,0.757,0.6949,0.631,0.5668,
	0.503,0.4412,0.381,0.321,0.265,
	0.217,0.175,0.1382,0.107,0.0816,
	0.061,0.04458,0.032,0.0232,0.017,
	0.01192,0.00821,0.005723,0.004102,0.002929,
	0.002091,0.001484,0.001047,0.00074,0.00052,
	0.0003611,0.0002492,0.0001719,0.00012,0.0000848,
	0.00006,0.0000424,0.00003,0.0000212,0.00001499,
	0.0000106,0.0000074657,0.0000052578,0.0000037029,0.0000026078,
	0.0000018366,0.0000012934,0.00000091093,0.00000064153,0.00000045181);
my @zcmf = (
	0.0006061,0.001086,0.001946,0.003486,0.006450001,
	0.01054999,0.02005001,0.03621,0.06785001,0.1102,
	0.2074,0.3713,0.6456,1.0390501,1.3856,
	1.62296,1.74706,1.7826,1.77211,1.7441,
	1.6692,1.5281,1.28764,1.0419,0.8129501,
	0.6162,0.46518,0.3533,0.272,0.2123,
	0.1582,0.1117,0.07824999,0.05725001,0.04216,
	0.02984,0.0203,0.0134,0.008749999,0.005749999,
	0.0039,0.002749999,0.0021,0.0018,0.001650001,
	0.0014,0.0011,0.001,0.0008,0.0006,
	0.00034,0.00024,0.00019,0.0001,0.00004999999,
	0.00003,0.00002,0.00001,0,0,
	0,0,0,0,0,
	0,0,0,0,0,
	0,0,0,0,0,
	0,0,0,0,0,
	0,0,0,0,0,
	0,0,0,0,0,
	0,0,0,0,0);

my @x = map { $xcmf[$_]/($xcmf[$_]+$ycmf[$_]+$zcmf[$_]) } 0..$#xcmf;
my @y = map { $ycmf[$_]/($xcmf[$_]+$ycmf[$_]+$zcmf[$_]) } 0..$#xcmf;

sub Normalize {
	my @a = @_;
	my $n = 0;
	for my $i (@a) { $n += $i*$i }
	$n = 1/sqrt($n); 
	for my $i (@a) { $i *= $n }
	@a
}
sub Tick {
	my $i = ($_[0] - 360) / 5;
	my ($px,$py) = Normalize($x[$i]-$x[$i-1], $y[$i]-$y[$i-1]);
	my ($qx,$qy) = Normalize($x[$i+1]-$x[$i], $y[$i+1]-$y[$i]);
	my ($tx,$ty) = Normalize($px+$qx, $py+$qy);
	return (X($x[$i]), Y($y[$i]), $ty * -$_[1], $tx * -$_[1]);
}

our $scale = 512;
our ($padleft,$padtop,$padright,$padbot) = (60,15,25,50);
our ($maxx, $maxy) = (0.8,0.9);
our ($origin_x, $origin_y) = ($padleft, $padtop + $maxy * $scale);
our ($imgwidth, $imgheight) = ($origin_x + $maxx * $scale + $padright, $origin_y + $padbot);

sub X { $origin_x + $scale * $_[0] }
sub Y { $origin_y - $scale * $_[0] }

my $svg = new Svg(width => $imgwidth, height => $imgheight);
#$svg->rect(width => $imgwidth, height => $imgheight, fill => 'gray');

# semitransparent grid
my $grid = new Svg::PathString;
for my $xx (1..$maxx*20) {
	my $x = $xx * 0.05;
	my $y = min($maxy, 1-$x);
	$grid->M(halfpixel(X($x)), halfpixel(Y(0)))->l(0, -$scale*$y);
}
for my $yy (1..$maxy*20) {
	my $y = $yy * 0.05;
	my $x = min($maxx, 1-$y);
	$grid->M(halfpixel(X(0)), halfpixel(Y($y)))->l($scale*$x, 0);
}
$grid->M(halfpixel(X(1-$maxy)), halfpixel(Y($maxy)))->L(halfpixel(X($maxx)), halfpixel(Y(1-$maxx)));
$svg->path(opacity => 1/8, stroke => 'black', 'stroke-width' => 1, fill => 'none', d => $grid->get());

$svg->defs->path(id => 'border', d => MakePath(\@x, \@y, $scale, -$scale, $origin_x, $origin_y, 1));
$svg->clipPath(id => 'clipborder')->use('border');

# black stroke on the monochromatic locus (the inner half of the stroke gets overwritten by the image)
$svg->use('border', stroke => 'black', 'stroke-width' => 4, fill => 'none');

# frequency tick marks on the monochromatic locus
my $ticks = '';
for (my $i = 410; $i <= 695; $i += 5) {
	my ($x,$y,$tx,$ty) = Tick($i, $i % 20 ? 6 : 10);
	$ticks .= 'M' . tenth($x) . ',' . tenth($y) . 'l' . tenth($tx) . ',' . tenth($ty);
}
$svg->path(d => $ticks, stroke => 'black', 'stroke-width' => 1);

# color PNG
my $pnggroup = $svg->group('clip-path' => 'url(#clipborder)');
$pnggroup->filter(id => 'blur')->feGaussianBlur(stdDeviation => 0.5);
$pnggroup->image('chromaticity1.png', width => 27, height => 28, filter => 'url(#blur)')->scale(1/32 * $scale)->translate($origin_x - 3/64 * $scale, $origin_y - 27.5/32 * $scale);

# coordinate axes
my $axes = new Svg::PathString;
$axes->M(round(X(0)), round(Y($maxy)))->l(0,round($scale*$maxy))->l(round($scale*$maxx),0);
for my $xx (0..$maxx*20) {
	$axes->M(round(X($xx*0.05)), round(Y(0)))->l(0,4);
}
for my $yy (0..$maxy*20) {
	$axes->M(round(X(0)), round(Y($yy*0.05)))->l(-4,0);
}
$svg->path(stroke => 'black', 'stroke-width' => 2, 'stroke-linecap' => 'square', fill => 'none', d => $axes->get());

my $alltext = $svg->group('font-family' => 'Nimbus Roman No9 L, Times, serif', 'font-size' => 19, stroke => 'none');
my $halfdigit = 6; # half the height of a digit, since Opera doesn't support dominant-baseline

# labels for frequency tick marks
my $freqs = $alltext->group(fill => 'blue');
for (my $i = 460; $i <= 620; $i += 20) {
	my ($x,$y,$tx,$ty) = Tick($i,12);
	my $xanchor = $ty < -2*abs($tx) ? 'middle' : $tx > 0 ? 'start' : 'end';
	my $yshift = $tx < -2*abs($ty) || $ty > 0 ? $halfdigit : 0;
	$freqs->text(x => tenth($x+$tx), y => tenth($y+$ty+$yshift), 'text-anchor' => $xanchor)->add($i);
}

# x,y axis labels
my $coords = $alltext->group(fill => 'black');
my $xcoords = $coords->group('text-anchor' => 'middle');
$xcoords->text(x => X($maxx/2), y => $origin_y + 40, 'font-style' => 'italic')->add('x');
for my $x (0..$maxx*10) {
	$xcoords->text(x => X($x * 0.1), y => $origin_y + 20)->add("0.$x");
}
my $ycoords = $coords->group('text-anchor' => 'end');
$ycoords->text(x => $origin_x - 40, y => Y($maxy/2)+$halfdigit, 'font-style' => 'italic')->add('y');
for my $y (0..$maxy*10) {
	$ycoords->text(x => $origin_x - 8, y => Y($y * 0.1)+$halfdigit)->add("0.$y");
}

# gamut polygon and whitepoint
if (@primaries || $whitepoint) {
	my $primary_polygon = $svg->group(stroke => 'black', 'stroke-width' => 2, fill => 'none');
	my $primary_polygon_circle_radius = 5;
	for my $p (0..$#primaries) {
		my ($a,$b) = @primaries[$p, $p == $#primaries ? 0 : $p+1];
		$primary_polygon->circle(cx => X($$a[0]), cy => Y($$a[1]), r => $primary_polygon_circle_radius);
		my ($dx,$dy) = ($scale * ($$a[0]-$$b[0]), -$scale * ($$a[1]-$$b[1]));
		my $nudge = $primary_polygon_circle_radius / sqrt($dx*$dx + $dy*$dy);
		my ($nudgex,$nudgey) = ($nudge * $dx, $nudge * $dy);
		$primary_polygon->line(x1 => round(X($$a[0])-$nudgex), y1 => round(Y($$a[1])-$nudgey), x2 => round(X($$b[0])+$nudgex), y2 => round(Y($$b[1])+$nudgey));
	}
	if ($whitepoint) {
		$primary_polygon->circle(cx => X($$whitepoint[0]), cy => Y($$whitepoint[1]), r => $primary_polygon_circle_radius);
		if (defined $$whitepoint[2]) {
			$alltext->text(x => X($$whitepoint[0]) + 8, y => Y($$whitepoint[1]) + 16, fill => 'black')->add($$whitepoint[2]);
		}
	}
}

$svg->write($filename);

The file chromaticity1.png was produced by this C++ code. It writes a PPM which you'll have to convert to PNG. Note that (to avoid edge artifacts) the script above ended up using a 27×28 image which consists of the 26×28 output of this program with the left column duplicated.

(hidden)
#include <stdio.h>
#include <math.h>

const int width = 26, height = 28;
const double step = 1/32.0;
const double norm = 5;
//const int width = 384, height = 448;
//const double step = 1/512.0;
//const double norm = 2;

struct RGB { double r,g,b; };

#ifdef DITHER
bool parity;
#endif

unsigned lin2gamma(double x) {
#ifdef DITHER
	unsigned n =
		x < 0 ? 0
		: x < 0.0031308 ? unsigned(0.5 + 510 * 12.92 * x)
		: x < 1 ? unsigned(0.5 - 510 * 0.055 + 510 * 1.055 * pow(x, 1/2.4))
		: 510;
	return !(n&1) ? n >> 1 : n&2 ? (n>>1)+!parity : (n>>1)+parity;
#else
	const int max = 255;
	return
		x < 0 ? 0
		: x < 0.0031308 ? unsigned(0.5 + max * 12.92 * x)
		: x < 1 ? unsigned(0.5 - max * 0.055 + max * 1.055 * pow(x, 1/2.4))
		: max;
#endif
}

double lin2gam(double x) {
	return x < 0 ? 0 : x < 0.0031308 ? 12.92 * x : x < 1.0 ? -0.055 + 1.055 * pow(x, 1/2.4) : 1.0;
}

void normalize(double* r, double* g, double* b) {
	double scale = pow(pow(*r, norm) + pow(*g, norm) + pow(*b, norm), -1.0 / norm);
	*r *= scale; *g *= scale; *b *= scale;
}

void putcolor(double x, double y, bool show_rgb_triangle, FILE* f) {
/*
	if (y > 0.835 || x + y > 1.002 || x - 2.2 * y > 0.170 || x + 0.186 * y < 0.098) {
		putc(128, f);
		putc(128, f);
		putc(128, f);
		return;
	}
*/
	/* "projective linear" RGB */
	double r = - 0.4986 + 3.7396 * x - 1.0388 * y;
	double g = + 0.0416 - 1.0108 * x + 1.8344 * y;
	double b = + 1.0570 - 1.0014 * x - 1.2610 * y;
	double min = r;
	if (g < min) min = g;
	if (b < min) min = b;

	if (min >= 0) {
		normalize(&r, &g, &b);
	} else {
		if (show_rgb_triangle) {
			r = (r + 0.9255) / 3.5;
			g = (g + 0.9255) / 3.5;
			b = (b + 0.9255) / 3.5;
		} else {
			r -= min; g -= min; b -= min;
			normalize(&r, &g, &b);
		}
	}
	putc(lin2gamma(r), f);
	putc(lin2gamma(g), f);
	putc(lin2gamma(b), f);
}

void make_image(bool show_rgb_triangle, const char* output_name) {
	FILE* f = fopen(output_name, "wb");
	fprintf(f, "P6\n%d %d\n255\n", width, height);

	for (int y = height - 1; y >= 0; --y) {
		for (int x = 0; x < width; ++x) {
#ifdef DITHER
			parity = (x ^ y) & 1;
#endif
			putcolor(x * step, y * step, show_rgb_triangle, f);
		}
	}

	fclose(f);
}

int main() {
	make_image(false, "chromaticity1.ppm");
	make_image(true,  "chromaticity2.ppm");
}

Isotopes and half-life[edit]

This Perl script produced File:Isotopes and half-life.svg. Needs #Svg.pm.

(hidden)
use strict;
use Svg;
use List::Util qw(min max);

sub ColorRange {
	my $c1 = shift;
	my @out = ($c1);
	while (@_) {
		my $n = shift; my $c2 = shift;
		for my $i (1..$n+1) {
			push @out, substr($c1,0,-6);
			for my $j (-6,-4,-2) {
				$out[$#out] .= sprintf '%02x', round((hex(substr($c1,$j,2))*($n+1-$i) + hex(substr($c2,$j,2))*$i) / ($n+1));
			}
		}
		$c1 = $c2;
	}
	@out;
}

# original: my @colors = ColorRange('#ffffff', 14, '#efffff', 14, '#00ffff', 7, '#ffff00', 7, '#ff0000', 9, '#870000', 6, '#800000');
my @colors = ColorRange('#ffffff', 17, '#3333ff', 7, '#00cccc', 4, '#22ee22', 4, '#eeee00', 9, '#ee0000', 15, '#800000', 0, '#300000');

sub HLColor {
	my ($hl) = @_;
	return 0 if $hl <= 0;
	return $#colors if $hl == 'inf';
	max(1, min($#colors-1, 18 + 2 * log($hl) / log(10)));
}

# Obtained from http://www.nndc.bnl.gov/nudat2/indx_sigma.jsp; the line at the bottom said "Nuclear Wallet Cards database version of 6/2/2009"
my @halflives = (
	[1, 613.8],
	[0, 'inf', 'inf', 3.888E8, 9.907E-23, 7.995E-23, 2.848E-22, 9.152E-15],
	[1, 'inf', 'inf', 7.595E-22, 0.807, 3.038E-21, 0.119, -1, 1.519E-21],
	[0, -1, 7.557E-23, 3.038E-22, 'inf', 'inf', 0.84, 0.178, -1, 0.00859, 1E-8],
	[1, -1, 4.953E-21, 4598208, 8.181E-17, 'inf', 4.765E13, 13.81, 0.0215, 2.7E-21, 0.00435, 2.0E-7, 2.0E-7],
	[1, -1, 3.255E-22, 0.77, 8.439E-19, 'inf', 'inf', 0.0202, 0.0173, 0.0125, 0.00993, 1.9E-10, 0.00508, 2.6E-8, 0.00292],
	[2, 1.981E-21, 0.127, 19.29, 1220.04, 'inf', 'inf', 1.799E11, 2.449, 0.747, 0.193, 0.092, 0.049, 0.014, 3.0E-8, 0.0061],
	[3, -1, 2.884E-22, 0.011, 597.9, 'inf', 'inf', 7.13, 4.173, 0.624, 0.271, 0.13, 0.085, 0.024, 0.0145, 5.2E-8, 2.6E-7],
	[4, 1.139E-21, 0.00858, 70.606, 122.24, 'inf', 'inf', 'inf', 26.88, 13.51, 3.42, 2.25, 0.082, 0.065, 5.0E-8, 4E-8, 2.6E-7, 1.0E-7],
	[5, -1, 4.557E-22, 1.139E-20, 64.49, 6586.2, 'inf', 11.07, 4.158, 4.23, 2.23, 0.39, 0.05, 0.0096, 0.005, 4E-8, 0.0025, 2.6E-7, 2.5E-7],
	[6, 3.735E-21, 0.109, 1.672, 17.22, 'inf', 'inf', 'inf', 37.24, 202.8, 0.602, 0.192, 0.032, 0.0189, 0.0148, 0.0073, 0.0034, 0.0035, 1.8E-7, 6.0E-8],
	[7, 1.3E-21, 4E-8, 0.448, 22.49, 8.213E7, 'inf', 53989.2, 59.1, 1.077, 0.301, 0.0305, 0.0449, 0.048, 0.017, 0.0132, 0.008, 0.0055, 0.0015, 1.8E-7, 6.0E-8],
	[7, -1, 0.0908, 0.122, 3.876, 11.317, 'inf', 'inf', 'inf', 567.48, 75294, 1.3, 0.335, 0.232, 0.086, 0.0905, 0.02, 0.07, 0.0039, 2.6E-7, 2.6E-7, 1.8E-7, 1.7E-7],
	[8, 3.5E-8, 0.059, 0.47, 2.053, 7.183, 2.263E13, 'inf', 134.484, 393.6, 3.6, 0.644, 0.033, 0.0417, 0.042, 0.0386, 0.09, 0.0107, 0.0076, 7.6E-6, 2.6E-7, 2.6E-7, 1.7E-7, 1.7E-7],
	[8, 0.029, 0.0423, 0.14, 0.22, 2.234, 4.16, 'inf', 'inf', 'inf', 9438, 4.828E9, 6.11, 2.77, 0.78, 0.45, 0.09, 1E-6, 0.0475, 0.033, 0.02, 0.0125, 6.0E-8, 3.6E-7],
	[9, -1, 3.0E-8, 0.0437, 0.26, 0.27, 4.142, 149.88, 'inf', 1232236.8, 2189376, 12.43, 47.3, 5.6, 2.31, 0.64, 0.28, 0.125, 0.1, 0.0485, 0.0365, 0.0185, 2.0E-7, 2.0E-7],
	[10, 0.01, 0.0155, 0.125, 0.187, 1.178, 2.572, 'inf', 'inf', 'inf', 7560864, 'inf', 303, 10218, 11.5, 8.8, 1.99, 1.013, 0.28, 0.1, 0.068, 0.05, -1, 2.0E-7, 2.0E-7],
	[11, -1, 2E-8, 3.0E-8, 0.15, 0.298, 2.511, 1920, 'inf', 9.499E12, 'inf', 2234.4, 3372, 81, 38.4, 6.8, 3.13, 0.56, 0.413, 0.232, 0.101, 2.0E-7, 1.7E-7, 0.02, 2.0E-7],
	[12, 2E-8, 0.0144, 0.098, 0.173, 0.845, 1.775, 'inf', 3019680.0, 'inf', 8.489E9, 'inf', 6576.6, 1.0382E9, 322.2, 712.2, 21.48, 8.4, 1.23, 0.475, 0.17, 1.7E-7, 2.0E-7, 0.01, 0.003],
	[13, -1, 2.5E-8, 2.5E-8, 0.178, 0.342, 1.226, 458.16, 'inf', 3.938E16, 'inf', 44355.6, 80280, 1327.8, 1068.6, 105, 17.5, 6.8, 1.26, 0.472, 0.365, 0.105, 0.03, 0.01, 0.003],
	[14, 3.5E-8, 0.0257, 0.102, 0.181, 0.44, 0.86, 9.467E28, 3.219E12, 'inf', 'inf', 'inf', 1.405E7, 8.836E22, 391910.4, 5.996E26, 523.08, 13.9, 10, 4.6, 0.09, 3.0E-7, 0.03, 0.01, 0.005],
	[15, -1, -1, -1, 3.0E-7, 0.182, 0.596, 61.7, 14007.6, 210996, 'inf', 7239456.0, 289370.88, 157212, 3430.8, 102.5, 12.4, 8.2, 3, 0.36, 0.105, 0.06, 0.013, 0.012, 0.01, 0.003],
	[16, -1, 0.031, 0.0533, 0.0804, 0.199, 0.509, 1.893E9, 11088, 'inf', 'inf', 'inf', 'inf', 'inf', 345.6, 102, 32.7, 1.5, 1.3, 0.2, 0.06, 0.059, 0.03, 0.022, 3.0E-7, 0.01, 0.003],
	[17, -1, -1, 5.5E-8, 0.8, 0.15, 0.547, 0.423, 1956, 1380110.4, 2.851E7, 4.418E24, 'inf', 224.58, 96, 49.8, 6.54, 0.216, 0.35, 0.185, 0.075, 0.122, 0.047, 1.5E-7, 0.017, 0.019, 0.01],
	[18, 0.013, 0.0216, 0.053, 0.0609, 0.26, 0.5, 77616, 2538, 4.102E25, 2393496, 'inf', 'inf', 'inf', 209.82, 356.4, 21.1, 7, 0.46, 0.57, 0.27, 0.19, 0.129, 0.043, 0.027, 0.01, 0.05],
	[19, 1.05E-7, -1, 0.034, 0.1, 0.158, 0.382, 105, 2772, 483062.4, 1.18E14, 2.697E7, 'inf', 9284.04, 85.4, 65.2, 4.59, 51, 0.67, 0.88, 0.29, 0.09, 0.092, 0.064, 0.047, -1, 0.014],
	[19, 0.00189, 0.012, 0.0218, 0.044, 0.0647, 0.155, 0.305, 29790, 510.6, 'inf', 8.637E7, 'inf', 'inf', 'inf', 3844368, 4.734E13, 358.8, 68, 6.1, 2, 1.3, 0.44, 0.6, 0.187, 0.109, 0.094, 1.5E-7, 1.5E-7],
	[23, 0.044, 2.0E-7, 0.115, 0.247, 88.8, 63108.0, 6672931.2, 2.348E7, 6122304, 'inf', 1.663E8, 5940, 834.6, 27.4, 0.3, 1.2, 0.18, 0.425, 1.6, 0.22, 0.5, 0.079, 0.062, 0.041, 1.5E-7, 1.5E-7],
	[20, 5E-7, 0.0075, 0.012, 2.0E-7, 0.038, 0.045, 0.104, 0.205, 524880, 128160, 'inf', 2.398E12, 'inf', 'inf', 'inf', 3.159E9, 'inf', 9061.92, 196560, 21, 29, 11.4, 6, 2.56, 1.57, 0.84, 0.68, 0.6, 0.238, 1.5E-7, 1.5E-7],
	[23, -1, 3.0E-7, 7.5E-8, 0.04, 0.094, 0.196, 3.204, 81.5, 1422, 11998.8, 580.38, 'inf', 45723.6, 'inf', 307.2, 222588, 225, 171, 44.5, 19.5, 6.6, 4.2, 1.594, 1.224, 0.641, 0.469, 0.342, 0.188, 3.0E-7],
	[24, -1, 0.02, 5E-7, 0.038, 0.084, 0.182, 142.8, 89.1, 33069.6, 2308.2, 'inf', 2.105E7, 'inf', 'inf', 'inf', 49536, 4.102E23, 14256, 167400, 23.5, 95.6, 10.2, 5.7, 2.08, 1.47, 0.995, 0.54, 0.29, 1.5E-7, 1.5E-7],
	[25, -1, -1, -1, -1, 0.07, 0.168, 0.116, 32.4, 157.62, 912, 34164, 281810.88, 4062.6, 'inf', 1268.4, 'inf', 50742, 17496, 487.2, 126, 32.6, 13.2, 5.09, 2.847, 1.676, 1.217, 0.599, 0.308, 0.085, 1.5E-7, 1.5E-7],
	[26, -1, -1, 0.03, 0.039, 1.5E-7, 0.142, 63.7, 30.9, 8135.1, 1134, 2.341E7, 140580, 'inf', 987552, 'inf', 'inf', 'inf', 4966.8, 'inf', 40680, 5280, 39, 29.5, 7.6, 4.55, 1.85, 0.947, 0.535, 1.5E-7, 0.14, 3.0E-7, -1],
	[27, -1, -1, -1, -1, 0.018, 0.128, 0.0958, 42.5, 151.6, 912, 3156, 235008, 93600, 6937920, 1535328, 'inf', 94538.88, 139788, 5442, 540.6, 15.2, 33.3, 19.1, 13.4, 3.24, 2.021, 0.945, 0.56, 3.0E-7, 3.0E-7, 1.5E-7, 1.5E-7, 3.0E-7],
	[30, 1.8E-7, 0.05, 0.033, 0.136, 35.5, 27.4, 2466, 284.4, 725760, 25740, 'inf', 1.035E7, 'inf', 'inf', 'inf', 9.309E12, 'inf', 3436.8, 'inf', 1338, 186, 31.7, 15.3, 5.5, 1.53, 0.41, 3.0E-7, 0.27, 3.0E-7, 1.5E-7, 1.5E-7],
	[32, -1, 1.2E-6, 2.4E-8, 2.2, 21.4, 78.6, 204, 2760, 5802, 58320, 205329.6, 387.6, 'inf', 15913.8, 'inf', 127015.2, 8640, 1908, 174, 55.1, 55.65, 16.29, 4.4, 1.91, 0.541, 0.343, 0.102, 0.07, 1.5E-7, 1.5E-7, 1.5E-7],
	[33, 0.032, 0.052, 0.1, 17.1, 27.3, 690, 257.4, 53280, 4464, 7.258E27, 126144, 'inf', 7.227E12, 'inf', 'inf', 'inf', 3.384E8, 'inf', 4578, 10224, 189, 32.32, 8.57, 1.84, 1.286, 0.212, 0.114, 0.08, 0.063, 0.046, 0.04, 3.0E-7],
	[34, -1, 1.2E-6, 3.0E-8, 0.0649, 19, 36.5, 226.2, 1059.6, 1374, 33.4, 16452, 23299.2, 7447680, 2859840, 'inf', 1610668.8, 1.518E18, 1066.38, 909, 258, 58.4, 4.492, 5.84, 2.702, 0.378, 0.203, 0.17, 0.114, 0.0503, 0.051, 0.032],
	[35, 0.025, 1.2E-6, 0.088, 7.89, 9, 150, 135, 6378, 1338, 2207520, 116675.1, 'inf', 5602176, 'inf', 'inf', 'inf', 4365792, 9.12E8, 34668, 9576, 445.38, 75.3, 23.9, 1.07, 0.429, 0.653, 0.269, 0.202, 0.118, 0.069, 1.5E-7, -1, -1],
	[37, 2.0E-7, 0.057, 5.7, 14.8, 30.1, 70.4, 8.3, 424.8, 2370, 17496, 53064, 287280, 9212486.4, 'inf', 230590.8, 5055264, 12744, 36648, 1122, 618, 9.6, 3.75, 2, 1.47, 0.94, 0.45, 0.36, 0.23, 0.18, -1, 1.5E-7, 0.03, 0.02],
	[38, 2.0E-7, 0.056, 4.6, 5.5, 32, 41.6, 1554, 471.6, 59400, 6048, 7205760.0, 282276, 'inf', 'inf', 'inf', 4.828E13, 'inf', 5532364.8, 6.312E26, 60278.4, 30.7, 2.1, 7.1, 2.3, 2.9, 1.3, 1.2, 0.6, -1, 0.15, 0.08, -1, 1.5E-7],
	[40, 0.8, 0.05, 4.1, 9.5, 20.9, 88, 225, 873, 7307.1, 52560, 2.146E10, 1.095E15, 'inf', 6.406E11, 3023222.4, 84060, 4326, 3078, 156, 2.99, 7.1, 4.3, 1.5, 4.9, 2.95, 0.93, 0.33, 0.193, 0.19, 0.17, 0.08, 1.5E-7, 0.03],
	[41, 0.006, 3.7, 3.2, 19.6, 14.02, 480, 126.6, 20016, 929.4, 'inf', 1.262E11, 'inf', 'inf', 'inf', 'inf', 'inf', 237384, 2.304E26, 876.6, 678, 67.5, 60, 35.6, 8.73, 3.5, 1.09, 0.53, 0.27, 0.2, 1.5E-7, 0.1, 0.08, 0.06],
	[42, 0.5, 0.054, 2.2, 6.4, 12.9, 49.2, 198, 255, 9900, 17580, 5270400, 369792, 1.329E14, 1.325E14, 6.662E12, 15.46, 853.2, 261, 54.2, 1098, 456, 35.6, 21.2, 5.17, 0.86, 0.92, 0.29, 0.29, 0.17, 0.15, 0.073, 0.09, 0.04, -1],
	[43, 1.5E-6, 1.2, 1.5, 11.7, 7.9, 219, 59.7, 3108, 5914.8, 'inf', 250560, 'inf', 'inf', 'inf', 'inf', 'inf', 3392064, 'inf', 15984.0, 3.212E7, 225, 273, 34.5, 11.6, 2.12, 1.75, 0.8, 0.52, 0.74, 0.4, 0.3, 0.123, 1.5E-7, 1.5E-7],
	[44, 1.5E-6, 1, 1.47, 4.66, 11.9, 70.6, 301.2, 594, 2772, 523.2, 1391040.0, 74880, 1.0414E8, 9.152E7, 'inf', 260.4, 127296, 7860, 1302, 360, 80, 28.5, 11, 6.73, 2.8, 1.85, 0.99, 0.68, 0.44, 0.266, 1.5E-7, 0.136, 0.151, 3.0E-7],
	[45, 1E-6, 0.7, 9.3, 9, 13.3, 122, 186, 1062, 1284, 313632, 30492.0, 'inf', 1468022.4, 'inf', 'inf', 'inf', 2.0512E14, 'inf', 49324.32, 'inf', 19800, 75708, 100, 145.2, 50, 11.8, 4.3, 1.9, 0.92, 0.5, 1.5E-7, 0.175, 1.5E-7, 0.038],
	[46, 1.5E-6, 0.55, 2, 6.9, 25.9, 47.5, 124, 134.4, 666, 774, 3942, 4152, 3567456, 715392, 'inf', 1.319E10, 'inf', 2.158E7, 643680, 11268, 19332, 4.6, 1200, 160.8, 72.8, 3.76, 6, 1.23, 0.79, 0.55, 0.3, 0.172, 0.166, 0.107, 0.079, 0.058, 0.16, 0.05],
	[47, 0.005, 1, 2.8, 9.2, 16, 49.1, 81.6, 330, 438, 3462, 3330, 'inf', 23400, 'inf', 3.986E7, 'inf', 'inf', 'inf', 2.43E23, 'inf', 3849984, 9.783E26, 12096, 3018, 161.4, 50.8, 13.5, 5.24, 2.1, 1.25, 0.65, 0.515, 0.37, 0.28, 0.27, 0.162, 0.068, 0.097],
	[48, 0.005, 1.2, 3, 5.9, 15.1, 23.3, 65, 108, 304.2, 372, 1944, 3480, 15001.2, 17640, 242326.08, 1233.6, 'inf', 4277664, 1.392E22, 3257.4, 6972, 267, 1080, 47.3, 232.8, 10.8, 47.4, 3.7, 12.2, 1.64, 3.67, 0.84, 1.23, 0.54, 0.35, 0.207, 0.165, 0.14, 0.092],
	[49, 0.005, 1, 1.7, 4.5, 7, 20.8, 34, 115, 174, 618, 1080, 14796.0, 2118, 'inf', 9943776, 'inf', 'inf', 'inf', 'inf', 'inf', 'inf', 'inf', 1.385E9, 'inf', 1.116E7, 'inf', 832896, 7.258E12, 7560, 3544.2, 414, 223.2, 58.4, 39.7, 1.45, 1.05, 0.53, 0.25, 0.19],
	[52, 1.5E-6, 0.44, 1.22, 0.6, 4, 7.4, 17, 23, 75, 51.4, 400.2, 209.4, 1926, 3618, 10080, 18000, 137484, 497664, 'inf', 235336.32, 'inf', 5201280, 8.705E7, 1067040, 332640, 32436, 15840.0, 2370, 1381.8, 245.1, 150, 10.07, 1.679, 0.923, 0.45, 3.0E-7, 1.5E-7],
	[53, 6.2E-7, 7E-5, 0.0031, 2.1, 4.6, 18.6, 19.3, 120, 102, 912, 402, 8964, 3720, 518400, 406080, 'inf', 1.331E7, 'inf', 2.903E24, 'inf', 'inf', 'inf', 9417600, 2.777E26, 2903040, 1.578E31, 119700, 276825.6, 3324, 2508, 19, 17.63, 2.49, 1.4, 1.5E-7, -1, 1.5E-7, 1.5E-7],
	[55, 0.036, 1.03E-4, 0.65, 2.5, 3.42, 6.6, 6.2, 78, 2.91, 133.2, 822, 1146, 4896, 7632, 217.8, 47604.6, 360806.4, 5132160, 1117152, 'inf', 1499.4, 4.955E14, 44496, 693377.28, 8262, 74880, 3150, 23688, 83.4, 24.5, 6.23, 2.28, 0.86, 0.43, 0.2, 1.5E-7, 3.0E-7],
	[55, 0.013, 0.093, 0.74, 2.7, 2.74, 10, 18, 59, 61, 228, 348, 2400, 2406, 72360, 7488, 5.0492E21, 60839.1, 'inf', 3144960, 'inf', 'inf', 'inf', 'inf', 'inf', 452995.2, 1.83E30, 32904, 7.574E28, 229.08, 844.8, 39.68, 13.6, 1.73, 1.25, 0.511, 0.388, 0.188, 0.146, 0.1],
	[57, 5E-4, 1.67E-5, 0.57, 1.4, 3.85, 8.4, 17, 43, 61.3, 155, 222, 352.8, 30.9, 2802, 98.4, 22500, 219.6, 115416.0, 1752.6, 837129.6, 559872, 'inf', 6.517E7, 7.258E13, 1126656, 9.493E8, 2004.6, 556.2, 63.7, 24.84, 1.684, 1.791, 1, 0.587, 0.321, 0.235, 0.146, 0.05, 0.05, 0.05],
	[58, 0.43, 0.45, 1.3, 1.75, 5.5, 5.4, 24, 29.7, 117, 162, 660, 210, 6000, 762, 209952, 8028, 'inf', 993600, 9.467E28, 3.319E8, 'inf', 'inf', 'inf', 'inf', 'inf', 4983.6, 1101833.28, 1096.2, 636, 14.5, 11.5, 4.31, 2.22, 0.893, 0.612, 0.344, 0.3, 3.0E-7, 0.1, 0.08],
	[60, 0.0235, 1, 2, 2.8, 5.3, 8.6, 17, 29.21, 64.8, 54, 306, 310.8, 696, 522, 3540, 17280, 14083.2, 387, 70200, 592.2, 1.893E12, 3.219E18, 'inf', 145026.72, 14112, 5466, 852, 40.8, 24.8, 10, 4.015, 1.26, 1.05, 0.51, 3.0E-7, 1.5E-7, 1.0E-7, 0.1, 0.06],
	[61, 0.2, 0.25, 1.1, -1, 3.8, 6, 10.2, 51, 31, 235.8, 210, 1374, 618, 12636, 17640, 273024, 63720, 2.209E21, 123840, 2.84E21, 1.189E7, 'inf', 2808691.2, 1.578E24, 118940.4, 2.462E7, 180.6, 811.2, 56.4, 56, 5.3, 4, 1.76, 1.4, 1.0E-7, 1.5E-7, 3.0E-7, 0.15, 0.05],
	[62, 1.4, 0.5, 0.8, 1.2, 3.3, 3.14, 4.2, 2.84, 32, 40, 90.6, 96, 390, 1020, 1440, 786, 4608, 7632, 15876, 203.4, 'inf', 68832, 1172448, 1036.8, 21542.4, 1449, 804, 137.4, 135.6, 6.19, 18.9, 3.63, 4.28, 2.3, 3.0E-7, 3.0E-7, 0.3, 0.2, 0.1],
	[64, 0.5, 0.6, 2.0E-7, 1.8, 5, 4.9, 21, 25.4, 94, 70, 510, 744, 3039, 2310, 18144, 19800, 291168, 8964, 'inf', 'inf', 7.227E22, 'inf', 'inf', 948672, 'inf', 6220.8, 2.493E26, 746.4, 684, 31.6, 25.9, 8.9, 5.49, 1.0E-7, 5.0E-8, 0.7, 0.3, 0.2],
	[65, 0.5, 1, 1, 2.4, 2.6, 6.3, 6.2, 15, 22, 49, 107, 144, 194.4, 249.0, 357, 1254, 40.5, 2.29E7, 3.136E7, 5.586E8, 1.745E8, 8.279E7, 3567456, 191088, 9648, 102240, 828, 315, 160.8, 41.5, 26.7, 10.56, 4.8, 1.47, 2, 0.7, 0.5, 0.2],
	[66, 0.5, 0.55, 1, 1.2, 4, 3.7, 9.5, 10.3, 47, 45, 186, 154.2, 889.2, 1356, 4349.4, 525, 'inf', 2.938E7, 3.25E15, 3.345E18, 2.209E23, 'inf', 'inf', 2.84E9, 'inf', 166622.4, 'inf', 1338, 33840, 481.8, 318, 11.37, 9.6, 4.8, 2.4, 1, 0.5, 0.2],
	[67, 9.0E-4, 0.0178, 0.1, 1, 0.5, 1.5, 3.8, 11, 12.1, 17.9, 1.51, 40.7, 73.38, 155.4, 10.2, 512352, 398304, 2082240.0, 4708800, 8043839.1, 1.164E9, 5.365E25, 4.272E8, 'inf', 2.712E8, 1.5E8, 1312416, 54648, 2754, 1086, 38, 26, 10.6, 7.8, 4.2, 2.3, 0.4, 0.2],
	[70, 0.4, 1.1, 2.0E-7, 2.2, 4.7, 5.8, 15.8, 24.5, 70.2, 110, 268.2, 1380, 4170528.0, 137016, 2.237E9, 801792, 5.649E13, 1.0705E7, 3.408E21, 2.0771E7, 'inf', 'inf', 'inf', 'inf', 'inf', 66524.4, 9.783E26, 219.6, 504, 68, 45, 10.3, 4.8, 3, 0.3, 1],
	[70, 9.4E-4, 0.2, 0.6, 2.0E-7, 1.6, 2.4, 7.9, 0.597, 21, 4.25, 30.9, 23, 6120, 3600, 14824.8, 12528, 63392.4, 63000, 202176, 81720, 459648, 462239.1, 2.241E9, 5.68E9, 'inf', 6246720, 596678.4, 456, 1170, 180, 126.6, 25.1, 19.4, 8.2, 2, 3, 0.5],
	[72, 0.2, 0.6, 7E-6, 0.9, 2.3, 3.2, 9.1, 14.1, 29, 55.7, 198, 252, 430.2, 1074, 8568, 23040, 9.467E13, 35640, 'inf', 29304.0, 'inf', 1.248E7, 'inf', 'inf', 'inf', 'inf', 'inf', 8402.4, 293760, 372, 522, 39, 30, 6, 3, 2],
	[73, 0.006, 0.0041, 0.4, 2.0E-7, 0.7, 2.4, 3.6, 5.8, 9.59, 56, 72, 47.2, 161.8, 558, 705.6, 2880, 3360, 756, 1680, 1982.1, 18072, 8928, 4020, 1.442E11, 2250, 'inf', 3.787E10, 10810.8, 179.4, 283.2, 165.6, 53, 25, 10, 8, 5],
	[75, 0.2, 2.0E-7, 0.9, 1.7, 2.5, 4.6, 8.9, 18.5, 23.5, 10.3, 37.1, 223.8, 318, 1170, 1119, 8244, 2160, 102888, 11556, 'inf', 4500, 'inf', 37296, 'inf', 'inf', 'inf', 811468.8, 'inf', 27057.6, 177480, 84, 192, 72, 20, 3],
	[75, 1.9E-6, 3.17E-6, 0.2, 0.58, 0.7, 0.9, 2.2, 6.6, 8, 2.5, 8.1, 45, 83.8, 217.8, 238.8, 547.8, 564, 1812, 1302, 6516, 306, 108216, 27720, 799200, 8043839.1, 'inf', 1.111E7, 6.0591E7, 228960, 29664, 324, 912, 114, 90, 30, 20],
	[78, 0.25, 0.7, 2.0E-7, 1.6, 3.04, 4.2, 0.409, 1.793, 26.1, 38.6, 89.4, 100.2, 288, 252, 1132.2, 663, 4548, 594, 204120, 1050, 'inf', 2766355.2, 'inf', 'inf', 'inf', 'inf', 'inf', 361583.1, 'inf', 6879.6, 4440, 480, 144, 60],
	[79, 0.043, 0.0806, 0.7, 0.9, 2, 0.138, 0.494, 6.8, 10.6, 12.1, 40, 77, 114, 238.2, 188.4, 644.4, 159, 3090, 402, 122616.0, 173836.8, 711936, 578880, 4.323E7, 1.0446E8, 'inf', 1.187E18, 1.386E7, 1704, 16524, 342, 210, 120, 58, 20],
	[79, -1, -1, 6.0E-8, 2, 0.89, 0.023, 0.11, 2.85, 5.6, 13.6, 18.2, 39.4, 40, 111, 76, 406.2, 122.1, 1557, 194.4, 57636.0, 43560, 5.901E7, 84960, 6.312E22, 6048000, 'inf', 'inf', 'inf', 'inf', 'inf', 3662496, 2.809E14, 3841.2, 14832, 210, 156, 30, 20],
	[82, 0.0029, 0.36, 0.0101, 0.055, 0.83, 1.7, 2.89, 3.57, 10.6, 14.2, 31, 34.4, 80, 120, 294, 405.6, 1398, 2208, 11304, 4104, 37800, 29124, 203616, 8496, 5.743E7, 3.787E22, 'inf', 9886752, 440639.1, 31319.1, 2964, 630, 120, 20, 3, 0.3],
	[84, 0.00125, 0.0073, 0.091, 0.409, 1.36, 2.8, 6.3, 5.1, 19.2, 19.9, 53, 74, 145.2, 142.8, 396, 456, 1992.0, 2112, 9000, 7920, 1866240.0, 2223, 5.68E25, 1.0472E7, 2.619E26, 4.102E26, 9.152E26, 6488639.1, 8.521E26, 85392, 6028992, 642, 1800, 3.0E-7, 3.0E-7],
	[84, 1.1E-5, 8.2E-4, 0.0156, 0.107, 0.39, 0.53, 2.1, 2.25, 5.9, 4.4, 15.1, 9.2, 15.2, 55, 118.8, 144, 353.4, 318, 840, 792, 1170, 146.4, 71640, 230400, 6048000, 1.46E7, 'inf', 6.312E12, 1.3E18, 61210.8, 87480, 11520, 588, 16, -1, 3.8E-5],
	[86, 0.0021, 0.0055, 0.021, 0.071, 0.199, 0.81, 2.1, 3.43, 7.37, 8.3, 19.2, 22.4, 44, 84, 216, 180, 300, 390, 1290, 6300, 79560, 46800, 1.767E21, 8087039.1, 6.312E22, 'inf', 'inf', 'inf', 'inf', 1330560, 'inf', 108396, 1.893E8, 540, 2094, 168, -1, 5, 6],
	[87, 9.4E-5, 1E-6, 0.0151, 0.0352, 1.61E-4, 0.353, 0.87, 3.2, 4.4, 9, 7.9, 9, 8.7, 30, 12, 79, 90, 294, 900, 3420, 11124, 51840, 59904, 37800, 149400, 1140480, 1017792, 'inf', 7.605E9, 'inf', 1.477E7, 13680, 5040, 534, 8, 6, -1, -1, 11],
	[88, 3E-4, 9.0E-4, 0.0021, 0.007, 0.0138, 0.051, 0.096, 0.382, 0.889, 2.53, 6.33, 10.6, 21.1, 21.2, 56, 52, 180, 390, 1038, 4254, 7488, 8460, 881279.1, 39132, 2.0512E19, 244512, 'inf', 1.578E9, 'inf', 'inf', 'inf', 71609.4, 'inf', 1848, 45360, 150, 158400, 10],
	[90, 1.5E-4, 2.86E-4, 0.00102, 0.0063, 0.025, 0.139, 0.156, 1.36, 1.462, 2.6, 3.3, 8.1, 13.7, 15.6, 42.8, 47.6, 408, 642, 504, 530.4, 1722, 2568, 11448, 17784, 63539.1, 136872, 1.608E7, 532820.16, 'inf', 232862.688, 271209.6, 67320, 1560, 28.4, 60, 39.8, 31],
	[91, 5.9E-5, 2.9E-4, 6E-4, 0.0021, 0.0108, 0.0203, 0.127, 0.269, 1.08, 2.58, 3.6, 10.83, 9.4, 30.9, 49.1, 82.8, 144, 195, 516, 1200, 3048, 17460, 42480, 1.401E10, 149760, 'inf', 230904, 'inf', 'inf', 'inf', 'inf', 'inf', 4025721.6, 'inf', 308.4, 499.2, 174, 2460, 35, 3.0E-7],
	[95, 0.0052, 0.018, 0.06, 0.42, 1.5, 3.2, 3.1, 6.9, 11, 19.5, 27.5, 51, 71, 138, 222, 313.2, 648, 1296, 1980, 4176, 6624, 10224, 19080, 26712, 93960, 262837.44, 1063584, 'inf', 1.193E8, 'inf', 252.12, 286.2, 183.18, 132, 78, 3.0E-7, 3.0E-7],
	[96, 2.3E-4, 0.003, 0.0045, 0.045, 0.055, 0.535, 0.49, 6.3, 4.82, 18.3, 25.1, 51, 71, 130.8, 210, 348, 642, 900, 2220, 2574, 8640, 5400, 77400, 33588, 1.657E12, 186912, 4.418E24, 5.459E14, 'inf', 'inf', 'inf', 11710.8, 7.00579E8, 2166, 38304, 612, 1608, 36],
	[101, 0.013, 5.8E-5, 0.015, 0.032, 0.265, 0.674, 6.3, 12.4, 39.6, 63.6, 125, 183, 308, 559.8, 696, 1620, 2184, 6180, 6156, 42336, 40392, 1322784, 539395.2, 1.0382E9, 1.161E13, 'inf', 9.594E13, 128.4, 3633, 2735.4, 1194, 456, 396, 98.5, 33],
	[104, 4E-4, 0.0035, 0.00246, 0.093, 0.0332, 0.37, 0.392, 4.64, 5.8, 84, 106.2, 328.2, 690.6, 936, 2676, 2202, 12708, 6264, 760320.0, 20880, 9.145E7, 3.219E9, 1.196E7, 25.2, 45.1, 3.72E-6, 1.643E-4, 0.00178, 0.145, 1.53, 185.88, 3.0E-7, 3.0E-7],
	[106, 0.0021, -1, 0.028, 0.25, 0.328, 0.388, 2, 4.2, 7.03, 47, 85.2, 184, 444, 552, 1614, 1836, 6480, 5868, 19476, 29160, 25970.4, 0.314, 1.25E-7, 7.6E-7, 1E-4, 3E-4, 0.0323, 1.5, 56, 222.6, 138, 54, 50],
	[107, 0.00115, 7.8E-4, 0.006, 0.0044, 0.065, 0.065, 0.59, 1.03, 7, 9.7, 44, 70.2, 170, 340.2, 555, 1461, 1710, 8640, 52560, 1434, 0.0195, 2.7E-7, 2.3E-6, 4.5E-5, 5.4E-4, 0.035, 3.96, 55.6, 1500, 330350.4, 1458, 6420, 279.6, 444, 20.8, 65],
	[112, 0.012, 0.049, 0.062, 0.3, 0.55, 2.6, 3.8, 16, 14.8, 59.1, 50, 190.8, 186, 1200, 34.82, 0.005, 8.6E-8, 7E-7, 1.9E-5, 0.022, 0.02, 27.4, 294, 852, 1320, 199.8, 237, 49, 148.2, 38, 50.2, 19.1, 17.6, 5.5],
	[113, 0.0016, 0.016, 0.031, 0.059, 0.21, 0.24, 1.3, 1.3, 4.6, 3.7, 13, 13, 163.8, 2.46, 0.00155, 1.82E-7, 1.6E-6, 2.52E-5, 0.01, 0.018, 28, 38, 987552, 313796.16, 1287360, 5.0492E10, 2532, 1.815E8, 240, 5580, 103, 252, 30, 30],
	[117, 0.033, 0.027, 0.095, 0.1, 0.35, 0.21, 0.93, 0.738, 8.2, 0.17, 4.41E-4, 6.9E-8, 1.08E-6, 1.18E-5, 0.0264, 0.052, 63, 126, 10008, 864000, 105732, 6.871E8, 22140, 3762, 122, 450, 119, 145, 44, 60, 120],
	[118, -1, 0.0038, 0.009, 0.04, 0.03, 0.144, 0.1, 1.2, 0.026, 2.41E-4, 1.17E-7, 1.05E-6, 9.7E-6, 0.00168, 0.00224, 0.6, 0.81, 523.2, 1834.2, 1613952, 6.0326E7, 2.487E11, 2.379E12, 91872, 4.418E17, 1309.8, 2082240.0, 432, 2238, 282, 564],
	[121, 0.0051, 0.0053, 0.017, 0.014, 0.15, 0.0036, 1.13E-4, 5.3E-8, 7.8E-7, 5.9E-6, 0.0033, 0.0051, 0.85, 1.7, 108, 2298, 79200, 129600, 1503359.1, 1.0338E12, 114048, 2330640, 24120, 1466.4, 546, 522, 136.2, 6480, 120],
	[125, 0.016, 5.6E-4, 4.2E-5, 6.0E-8, 7.0E-7, 1E-6, 1.8E-5, 9.0E-4, 0.084, 0.35, 66, 546, 3480, 1797120, 362880, 2.174E9, 5.024E12, 7.747E12, 2.222E16, 7.391E14, 583200, 1.41E17, 1407, 50760, 300, 1008],
	[132, 2E-6, 0.035, 0.51, 61.4, 240, 276, 2928, 882, 2172, 380160.0, 3.422E7, 4.828E12, 6.766E13, 182908.8, 203558.4, 3714, 834, 330, 111, 137.4],
	[134, 1.1, 90, 102, 516, 2027.1, 1254, 31680.0, 1518, 9.0192E7, 3905280.0, 2.768E9, 7.609E11, 2.0705E11, 4.51E8, 1.183E13, 17841.6, 2.525E15, 37800, 936576, 196128],
	[135, 17, 10, 79, 192, 139.2, 618, 216, 4416, 5880, 42840, 182880, 1.365E10, 4.45E9, 2.326E11, 36360, 7379.1, 2340, 1380, 600, 120],
	[136, 60, -1, 51, 300, -1, 1200, 8640, 10440, 2332800, 2833919.1, 1.407E7, 9.183E8, 5.712E8, 2.682E11, 1.502E11, 4.923E14, 1.0982E13, 3849.0, 2.619E11, 1008, 172800],
	[137, 140, 20, -1, 60, 144, -1, 288, 276, 420, 16200, 15659.1, 426816.0, 155520, 4.355E10, 2.84E8, 2.851E7, 11563.2, 3336, -1, 600, 120],
	[139, 2.1, 0.021, 39, 57.6, 226.8, 222, 642, 1164, 2700, 128520.0, 11196, 2.881E7, 1.108E10, 4.128E8, 2.834E10, 8.347E7, 1538784, 5227200, 5100, 738],
	[141, 1, 8, 13.5, 21, 37, 66, 462, 5.4E7, 1620, 6132, 30960, 118800, 4.0755E7, 1768608, 2.382E7, 3438719.1, 27360, 665280, 180],
	[142, 8E-4, 0.18, 0.0033, 4.2, 1.1, 29, 36, 156, 1800, 19080, 91404, 259200, 11664, 72252, 9456, 8683200, 3.7E-4, 1.5, 0.004],
	[144, 0.35, 1, 1.12, 7, 24, 52, 240, 138, 360, 1680, 1620, 4620, 19872, 4449600, 5760, 2747520, 2400, 180],
	[146, 2E-6, -1, 4.6E-5, 1.02, 2.44, 97.2, 51, 186, 2.91, 25, 0.0012, 3480, 0.106, -1, 0.005, 1200, 60],
	[148, -1, 0.36, 1.49, 13, 22, 27, 0.646, 4.1, 6.2, 180, 2340, 14400, 18000, 36000, 36000, 3600],
	[149, 4.8E-5, 2.3E-5, 1.68, 0.0064, 4.7, 0.012, 3.2, 0.021, 65, 2.3, 600, 3600, 46800, 36000, -1, 21600],
	[150, 1.6, 1.6, 1.5, 20, 0.51, 1.52, 1.8, 35, 27, 180, 900, 1200, 4380, 115200, 10800],
	[152, 0.0029, 0.48, 0.0036, 0.23, 0.0069, 1, 0.037, 8, 21, -1, 30, -1, 600, 144, 3600, 60],
	[153, 3E-4, 0.012, 0.102, 2E-4, 0.44, 0.9, 1.7, 17, -1, -1, -1, -1, 10, 5400, 5400, 2400],
	[155, -1, 8E-4, 0.002, 0.0023, 0.052, -1, 9.7, 3.6, 40, 40, -1, 60, 0.15, 3600],
	[156, 120, 0.0017, 0.01, 0.021, -1, 0.005, 5, 10, 20, 0.45, 0.0097, 0.72, -1, -1, 360],
	[157, 2.8E-6, 9.1E-5, 1.79E-4, 0.006, 0.069, 1, 1.7E-4, 2, 2, 5, 5, 10, 0.18, -1, 9.6],
	[161, 0.0038, 0.005, 0.0064, 0.01, 0.1, 1, 0.0042, 0.17, 3.6, 60, 240, 600],
	[165, 6.9E-4, 0.01, 0.1, 1, -1, 5E-4, 4, 0.101, 34],
	[165, 2.4E-4, -1, -1, -1, -1, 0.1, 0.48, 120, 300, 1200],
	[172, 0.16, 0.51, 0.8, 2.7],
	[172, 0.032, 0.087, 10, 10, 60],
	[174, 0.015, 0.0063, 0.018, 0.053],
	[174, 0.01, 0.05],
	[176, 0.0018],
);

my ($imgwidth, $imgheight) = (640,768);
my ($origin_x, $origin_y) = (40,728);
my ($maxz, $maxn) = ($#halflives + 1, 178);
my ($legend_x, $legend_width) = (530,25);
my $scale = 4;
my ($plotwidth,$plotheight) = (round($scale * $maxz), round($scale * $maxn));

my $svg = new Svg(width => $imgwidth, height => $imgheight);
#$svg->rect(width => $imgwidth, height => $imgheight, fill => 'gray');

# grid
my $grid = new Svg::PathString;
for (my $x = 10; $x < $maxz; $x += 10) {
	$grid->M($origin_x + $scale * ($x+0.5) - 0.5, $origin_y + 0.5)->v(-$plotheight);
}
for (my $y = 10; $y < $maxn; $y += 10) {
	$grid->M($origin_x - 0.5, $origin_y - $scale * ($y+0.5) + 0.5)->h($plotwidth);
}
$svg->path(d => $grid->get(), stroke => 'lightgray', 'stroke-width' => 1, fill => 'none');

# Z=N line
$svg->line(x1 => $origin_x - 20.5, y1 => $origin_y + 20.5, x2 => $origin_x + $plotwidth + 0.5, y2 => $origin_y - $plotwidth - 0.5, stroke => 'black', 'stroke-width' => 0.707);
#$svg->line(x1 => $origin_x, y1 => $origin_y, x2 => $origin_x + $plotwidth, y2 => $origin_y - $plotwidth, stroke => 'black', 'stroke-width' => 1);

# the little squares
my $pixels = $svg->group(stroke => 'none')->scale($scale,-$scale)->translate($origin_x + 0.05, $origin_y - 0.05);

my @hist = (0) x @colors;
if (1) {
	# path-based; makes a smaller file, maybe slower?
	my @paths;
	for my $z (0..$#halflives) {
		my $elt = $halflives[$z];
		my $n = $$elt[0];
		for (my $i = 1; $i < @$elt; ++$i, ++$n) {
			my $c = int(HLColor($$elt[$i]));
			next if $c == 0;
			++$hist[$c] unless $$elt[$i] == 'inf';
			$paths[$c] //= [new Svg::PathString(), 0, 0];
			$paths[$c][0]->m($z-$paths[$c][1], ($n-$paths[$c][2]) * 10 - 9)->v('9');
			@{$paths[$c]}[1,2] = ($z,$n);
		}
	}
	for my $c (1..$#colors) {
		next if !defined $paths[$c];
		my $path = $paths[$c][0]->get();
		$path =~ s/^m/M/;
		$pixels->path(stroke => $colors[$c], 'stroke-width' => 0.9, fill => 'none', d => $path)->translate(0.45,9)->scale(1,0.1);
	}
} else {
	# rect-based
	my @ids = (undef, 0..9, 'A'..'Z', 'a'..'z', '_');
	for my $i (1..$#colors) {
		$pixels->defs->rect(id => $ids[$i], width => 0.9, height => 0.9, fill => $colors[$i]);
	}
	for my $z (0..$#halflives) {
		my $elt = $halflives[$z];
		my $n = $$elt[0];
		my $col = $pixels->group()->translate($z,$n);
		for my $i (1 .. $#$elt) {
			my $c = HLColor($$elt[$i]);
			$col->use($ids[$c], y => $i-1) unless $c == 0;
		}
	}
}

# frame box
$svg->rect(x => $origin_x - 0.5, y => $origin_y - $plotheight - 0.5, width => $plotwidth+1, height => $plotheight+1, stroke => 'black', 'stroke-width' => 1, fill => 'none');

# color legend
my $legend = $svg->group(stroke => 'none');
my $y1 = round($origin_y - $plotheight);
for (my $c = $#colors; $c > 0; --$c) {
	my $y2 = round($origin_y - 1 - ($plotheight-1) * $c / @colors);
	$legend->rect(fill => $colors[$c], x => $legend_x, y => $y1, width => $legend_width, height => $y2-$y1);
	$y1 = $y2;
}
$legend->rect(x => $legend_x - 0.5, y => $origin_y - $plotheight - 0.5, width => $legend_width + 1, height => $plotheight + 1, stroke => 'black', 'stroke-width' => 1, fill => 'none');
$legend->line(x1 => $legend_x - 0.5, y1 => $y1 + 0.5, x2 => $legend_x + $legend_width + 0.5, y2 => $y1 + 0.5, stroke => 'black', 'stroke-width' => 1);
#for my $c (1..$#colors) {
#	my $y = round($origin_y - $plotheight * ($c+0.5) / @colors);
#	$legend->line(x1 => $legend_x + $legend_width + 0.5, y1 => $y, x2 => $legend_x + $legend_width + 0.5 + 3 * $hist[$c], y2 => $y, stroke => 'red', 'stroke-width' => 1);
#}

# text labels
my $labels = $svg->group('font-family' => 'Nimbus Roman No9 L, Times, serif', 'font-size' => 19, stroke => 'none', fill => 'black');
for (my $x = 20; $x < $maxz; $x += 20) {
	$labels->text(x => $origin_x + $scale * ($x+0.5), y => $origin_y + 20, 'text-anchor' => 'middle')->add($x);
}
$labels->text(x => $origin_x, y => $origin_y + 20)->add('Z');
for (my $y = 20; $y < $maxn; $y += 20) {
	$labels->text(x => $origin_x - 5, y => $origin_y - $scale * ($y+0.5) + 6, 'text-anchor' => 'end')->add($y);
}
$labels->text(x => $origin_x - 8, y => $origin_y, 'text-anchor' => 'end')->add('N');
$labels->text(x => $origin_x + $plotwidth*0.7 + 20, y => $origin_y - $plotwidth*0.7)->add('Z = N');
my $ticks = new Svg::PathString;
for my $yr (0,1) {
	for my $n ($yr ? (0..14) : (-8..6)) {
		next if $n & 1;
		my $c = HLColor(10 ** $n * ($yr ? 31557600 : 1));
		my $y = round($origin_y - $plotheight * $c / @colors);
		$ticks->M($legend_x + $legend_width, $y - 0.5)->h(5);
		my $text = $labels->text(x => $legend_x + $legend_width + 10, y => $y + 4, 'text-anchor' => 'start');
		if ($n >= 0 && $n <= 2) {
			$text->add(10**$n);
		} else {
			$text->add('10');
			$n =~ s/^-/\x{2212}/;
			$text->tspan(dy => -6, 'font-size' => 13)->add($n);
			$text = $text->tspan(dy => 6);
		}
		$text->add($yr ? ' yr' : ' sec');
	}
}
$labels->text(x => $legend_x + $legend_width + 5, y => $origin_y, 'text-anchor' => 'start')->add('unstable');
$labels->text(x => $legend_x + $legend_width + 5, y => $origin_y - $plotheight + 12, 'text-anchor' => 'start')->add('stable');
$ticks->M($legend_x + 0.5, halfpixel($origin_y - $plotheight / @colors))->h($legend_width);
$legend->path(stroke => 'black', 'stroke-width' => 1, fill => 'none', d => $ticks->get());

$svg->write('Isotopes and half-life.svg');

Svg.pm[edit]

The SVG-generating Perl scripts use this. It needs to be in Perl's library search path (or you could set PERL5LIB=.).

Svg.pm
package Svg;
use strict;
use XML::Writer;
use IO::File;
use IPC::Open2;
use POSIX qw(floor);

{
	package Svg::Elt;

	sub new { my $class = shift; bless [@_], $class }

	sub add {
		my $self = shift;
		push @$self, @_;
		return $_[$#_];
	}

	sub write {
		my ($self,$writer) = @_;
		my ($tag,$attr,@sub) = @$self;
		if (!@sub) {
			$writer->emptyTag($tag, @$attr);
		} else {
			$writer->startTag($tag, @$attr);
			for my $x (@sub) {
				if (ref $x) { $x->write($writer) } else { $writer->characters($x) }
			}
			$writer->endTag($tag);
		}
	}

	sub transform {
		my ($self,$xform) = @_;
		my $a = $$self[1];
		for (my $i = 0; $i < @$a; $i += 2) {
			if ($$a[$i] eq 'transform') {
				$$a[$i+1] = "$xform $$a[$i+1]";
				return $self;
			}
		}
		push @$a, 'transform', $xform;
		return $self;
	}
	sub translate {
		my ($self,$x,$y) = @_;
		$self->transform("translate($x,$y)");
	}
	sub scale {
		my ($self,$x,$y) = @_;
		$self->transform(defined($y) ? "scale($x,$y)" : "scale($x)");
	}
	sub rotate {
		my ($self,$a) = @_;
		$self->transform("rotate($a)");
	}
}
{
	package Svg::Gradient;
	our @ISA = ('Svg::Elt');
	sub stop { my ($self,@attr) = @_; $self->add(new Svg::Elt('stop', \@attr)) }
}
{
	package Svg::Filter;
	our @ISA = ('Svg::Elt');
	sub feGaussianBlur { my ($self,@attr) = @_; $self->add(new Svg::Elt('feGaussianBlur', \@attr)) }
}
{
	package Svg::Text;
	our @ISA = ('Svg::Elt');
	sub tspan { my ($self,@attr) = @_; $self->add(new Svg::Elt('tspan', \@attr)) }
	sub tref { my ($self,$id,@attr) = @_; push @attr, 'xlink:href' => "#$id"; $self->add(new Svg::Elt('tref', \@attr)) }
}
{
	package Svg::Group;
	our @ISA = ('Svg::Elt');
	use MIME::Base64;

	sub group    { my ($self,@attr) = @_; $self->add(new Svg::Group('g', \@attr)) }
	sub marker   { my ($self,@attr) = @_; $self->add(new Svg::Group('marker', \@attr)) }
	sub clipPath { my ($self,@attr) = @_; $self->add(new Svg::Group('clipPath', \@attr)) }
	sub line     { my ($self,@attr) = @_; $self->add(new Svg::Elt('line', \@attr)) }
	sub rect     { my ($self,@attr) = @_; $self->add(new Svg::Elt('rect', \@attr)) }
	sub circle   { my ($self,@attr) = @_; $self->add(new Svg::Elt('circle', \@attr)) }
	sub path     { my ($self,@attr) = @_; $self->add(new Svg::Elt('path', \@attr)) }
	sub text     { my ($self,@attr) = @_; $self->add(new Svg::Text('text', \@attr)) }
	sub filter   { my ($self,@attr) = @_; $self->add(new Svg::Filter('filter', \@attr)) }
	sub linearGradient { my ($self,@attr) = @_; $self->add(new Svg::Gradient('linearGradient', \@attr)) }
	sub use { my ($self,$id,@args) = @_; push @args, 'xlink:href' => "#$id"; $self->add(new Svg::Elt('use', \@args)) }
	sub defs {
		my ($self) = @_;
		if (@$self <= 2 || !ref $$self[2] || $$self[2][0] ne 'defs') {
			splice @$self, 2, 0, new Svg::Group('defs', []);
		}
		$$self[2];
	}
	sub image {
		my ($self,$imgname,@attr) = @_;
		my $mimetype = $imgname =~ /\.jpe?g$/i ? 'image/jpeg' : $imgname =~ /\.png$/i ? 'image/png' : die;
		my $data = do { local $/; local *F; open F, '<', $imgname or die; binmode F; <F> };
		push @attr, 'xlink:href' => "data:$mimetype;base64," . encode_base64($data,'');
		$self->add(new Svg::Elt('image', \@attr));
	}
}

{
	# just a trivial string-builder class
	package Svg::PathString;
	sub new { my $a = ''; bless \$a };
	sub add {
		my $self = shift;
		for my $_ (@_) {
			my $x = $_;
			$x =~ s/^(-?)0\.(\d)/$1.$2/;
			$$self .= ' ' if $x =~ /^[\d.]/ && $$self =~ /[\d.]$/;
			$$self .= $x;
		}
		$self;
	}
	our $AUTOLOAD;
	sub AUTOLOAD {
		$AUTOLOAD =~ /::(\w)$/ or return ();
		my $self = shift;
		$self->add($1,@_);
	}
	sub get { ${$_[0]} }
}

our @ISA = ('Svg::Group', 'Exporter');

our @EXPORT = qw(Interpolate MakePath round tenth halfpixel);

sub new {
	my $class = shift;
	bless ['svg', [version => '1.1', 'xml:space' => 'preserve', xmlns => 'http://www.w3.org/2000/svg', 'xmlns:xlink' => 'http://www.w3.org/1999/xlink', @_]], $class;
}
sub write {
	my ($self,@args) = @_;
	$args[0] = new IO::File $args[0],'w' if !ref $args[0];
	my $writer = new XML::Writer(ENCODING => 'UTF-8', OUTPUT => @args);
	$writer->xmlDecl(undef, 'no');
	$writer->doctype('svg', '-//W3C//DTD SVG 1.1//EN', 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd');
	Svg::Elt::write($self,$writer);
	$writer->end();
}

sub Interpolate {
	my ($x,$y,$k) = @_;
	my $n = @$x;
	return ($x,$y) if ($n < 2);
	my @tan;
	my $i;
	for $i (1..$n-1) { $tan[$i] = ($$y[$i] - $$y[$i-1]) / ($$x[$i] - $$x[$i-1]) }
	$tan[0] = $tan[1];
	for $i (1..$n-2) { $tan[$i] = ($tan[$i] + $tan[$i+1]) * 0.5 }
	my (@x,@y);
	for $i (0..$n-1) { $x[$i*$k] = $$x[$i]; $y[$i*$k] = $$y[$i] }
	for $i (0..$n-2) {
		for my $j (1..$k-1) {
			my $t = $j/$k;
			my $u = $t * $t * (3 - 2 * $t);
			$x[$i*$k + $j] = $$x[$i] * (1 - $t) + $$x[$i+1] * $t;
			$y[$i*$k + $j] = $$y[$i] * (1 - $u) + $$y[$i+1] * $u + $tan[$i] * $t * ($t-1) * ($t-1) + $tan[$i+1] * $t * $t * ($t-1)
		}
	}
	return (\@x,\@y);
}

sub MakePath {
	my ($x,$y,$xscale,$yscale,$xofs,$yofs,$error) = @_;
	if (defined $error) {
		my ($in,$out);
		open2($in, $out, 'FitCurves', $error) or die;
		for my $i (0..$#$x) {
			print $out $$x[$i]*$xscale+$xofs, ' ', $$y[$i]*$yscale+$yofs, "\n";
		}
		close $out;
		my $result = do { local $/; <$in> };
		close $in;
		return $result;
	} else {
		my $_;
		return 'M' . join('L', map { ($$x[$_]*$xscale+$xofs) . ',' . ($$y[$_]*$yscale+$yofs) } 0..$#$x)
	}
}

sub round { floor($_[0] + 0.5) }
sub tenth { 0.1 * floor($_[0] * 10 + 0.5) }
sub halfpixel { floor($_[0]) + 0.5 }

1;

FitCurves[edit]

Svg.pm invokes FitCurves, which is a slight modification of a program from Graphics Gems. Download the original here (tar.gz) or here (zip) and then replace DrawBezierCurve and main in FitCurves.c with these functions:

(hidden)
DrawBezierCurve(n, curve)
int n;
BezierCurve curve;
{
	int i;
	for (i = 1; i <= n; ++i)
		printf("%s%d,%d", i==1 ? "C" : " ", (int)floor(curve[i].x+0.5), (int)floor(curve[i].y+0.5));
}

void usage(void)
{
    printf("usage: FitCurves max_squared_error\n  takes coordinate pairs on stdin, returns path on stdout\n");
    exit(1);
}

int main(int argc, char** argv)
{
    static Point2 d[MAXPOINTS];
    int n;
    double error = 1;		/*  Squared error */
    if (argc != 2) usage();
    sscanf(argv[1], "%lf", &error);
    for (n = 0; n < MAXPOINTS; ++n) {
        Point2 p;
        int k = scanf("%lf%lf", &d[n].x, &d[n].y);
        if (k < 0) break;
        else if (k != 2) usage();
    }
    printf("M%d,%d", (int)floor(d[0].x+0.5), (int)floor(d[0].y+0.5));
    FitCurve(d, n, error);
    return 0;
}