view release on metacpan or search on metacpan
examples/boxes.pl
examples/empty.t
examples/git.pl
examples/gray.pl
examples/interp.pl
examples/random.pl
examples/smallinterp.pl
lib/ANSI/Heatmap.pm
LICENSE
Makefile.PL
MANIFEST This list of files
README.pod
t/basic.t
META.yml Module YAML meta-data (added by MakeMaker)
META.json Module JSON meta-data (added by MakeMaker)
{
"abstract" : "Render heatmaps to your terminal",
"author" : [
"Richard Harris <richardjharris@gmail.com>"
],
"dynamic_config" : 1,
"generated_by" : "ExtUtils::MakeMaker version 6.6302, CPAN::Meta::Converter version 2.113640",
"license" : [
"perl_5"
],
"meta-spec" : {
"url" : "http://search.cpan.org/perldoc?CPAN::Meta::Spec",
"version" : "2"
},
"name" : "ANSI-Heatmap",
"no_index" : {
"directory" : [
"t",
"inc"
]
},
"prereqs" : {
"build" : {
"requires" : {
"ExtUtils::MakeMaker" : 0
"runtime" : {
"requires" : {
"Class::Accessor::Fast" : 0,
"Test::More" : 0
}
}
},
"release_status" : "stable",
"resources" : {
"bugtracker" : {
"web" : "https://github.com/richardjharris/ANSI-Heatmap/issues"
},
"repository" : {
"url" : "https://github.com/richardjharris/ANSI-Heatmap"
}
},
"version" : "0.3"
}
---
abstract: 'Render heatmaps to your terminal'
author:
- 'Richard Harris <richardjharris@gmail.com>'
build_requires:
ExtUtils::MakeMaker: 0
configure_requires:
ExtUtils::MakeMaker: 0
dynamic_config: 1
generated_by: 'ExtUtils::MakeMaker version 6.6302, CPAN::Meta::Converter version 2.113640'
license: perl
meta-spec:
url: http://module-build.sourceforge.net/META-spec-v1.4.html
version: 1.4
name: ANSI-Heatmap
no_index:
directory:
- t
- inc
requires:
Class::Accessor::Fast: 0
Test::More: 0
resources:
bugtracker: https://github.com/richardjharris/ANSI-Heatmap/issues
repository: https://github.com/richardjharris/ANSI-Heatmap
version: 0.3
Makefile.PL view on Meta::CPAN
use strict;
use warnings;
use ExtUtils::MakeMaker;
WriteMakefile1(
META_MERGE => {
resources => {
repository => q{https://github.com/richardjharris/ANSI-Heatmap},
bugtracker => q{https://github.com/richardjharris/ANSI-Heatmap/issues},
},
},
NAME => q{ANSI::Heatmap},
AUTHOR => q{Richard Harris <richardjharris@gmail.com>},
ABSTRACT => q{Render heatmaps to your terminal},
VERSION_FROM => q{./lib/ANSI/Heatmap.pm},
LICENSE => q{perl},
PREREQ_PM => {
q{Class::Accessor::Fast} => 0,
q{Test::More} => 0,
},
MAN3PODS => {
'lib/ANSI/Heatmap.pm' => 'blib/man3/ANSI::Heatmap.3',
},
dist => { COMPRESS => q{gzip -9f}, SUFFIX => q{gz}, },
clean => { FILES => q{ANSI-Heatmap-*} },
);
sub WriteMakefile1 {
my %params = @_;
my $eumm_version = $ExtUtils::MakeMaker::VERSION;
$eumm_version = eval $eumm_version;
die "EXTRA_META is deprecated" if exists $params{EXTRA_META};
die "License not specified" if not exists $params{LICENSE};
if ($params{AUTHOR} and ref($params{AUTHOR}) eq q{ARRAY}
and $eumm_version < 6.5705) {
=head1 NAME
ANSI::Heatmap - render heatmaps to your terminal
=head1 SYNOPSIS
my $map = ANSI::Heatmap->new(
half => 1,
min_x => 0, max_x => 49,
min_y => 0, max_y => 49,
swatch => 'blue-red',
);
for (1..2000) {
my $x = int(rand(50));
my $y = int(rand(50));
$map->inc($x, $y);
}
print $map;
$map->interpolate(1);
$map->width(25);
$map->height(25);
$map->swatch('grayscale');
print $map;
my $data = $map->data;
# Mess with the data...
print $map->render($data);
# Custom swatch
$map->swatch([0x10 .. 0x15]);
=head1 DESCRIPTION
Produce cutting-edge ANSI heatmaps using 256 colours and weird Unicode
characters! Perfect for 3D (2D + intensity) data.
=head1 METHODS
=head2 new ( [ARGS] )
C<ARGS> may be a hash or hashref accepting the following keys, which
also have getter/setter methods:
=over 4
=item min_x, max_x ( INT )
Specify the smallest and largest X-axis value to include. If not
provided, defaults to the smallest/largest values passed to C<set>
or C<inc>. Can be used to crop the map or ensure it keeps a fixed
size even if some values are unset.
To make automatic again, set to C<undef>.
=item min_y, max_y ( INT )
Ditto for the Y-axis.
=item min_z, max_z ( FLOAT )
Ditto for intensity; useful for keeping a fixed intensity across
multiple heatmaps.
The default C<min_z> value is 0, unless negative intensities are
used.
=item swatch ( STR | ARRAYREF )
Set the colour swatch; see C<swatch> below.
=item half ( BOOL )
A boolean indicating if the map should be rendered in half-height
mode using special characters. On most terminals, this means the
X and Y axis will be scaled identically.
Off by default.
=item width, height ( INT )
Specify the width/height of the map in characters. Defaults to
using the min_I<axis> and max_I<axis> values to determine the
width/height.
=item interpolate ( BOOL )
If width/height is not a nice multiple of the input data and
this flag is set, perform bilinear interpolation (instead of
nearest neighbour). This is a trade off; interpolated data is
blurrier, but retains a linear relationship with the original
data. Off by default.
=back
=head2 set ( X, Y, Z )
Set the heatmap intensity for the given X and Y co-ordinate.
Currently, only integer values for X and Y are supported.
=head2 get ( X, Y )
Return the heatmap intensity for the given X and Y co-ordinate,
or 0 if unset.
=head2 inc ( X, Y )
Increase the intensity at the given co-ordinate by 1.
=head2 to_string
Return a string containing the ANSI heatmap. If C<half> is set,
this string contains wide characters, so you may need to:
binmode STDOUT, ':utf8';
or
use open OUT => ':utf8';
before printing anything (in this case) to STDOUT.
=head2 data
Returns the heatmap data, cropped, scaled and normalised with
intensity values between 0 and 1.
Expressed as an arrayref of arrayrefs indexed by Y and then
X co-ordinate.
=head2 render ( DATA )
Manually render heatmap data as returned by C<data>. Useful
if you want to do any custom processing.
=head2 swatch ( [ARRAYREF | STRING] )
Set the colour swatch that decided how the heatmap will look.
A string alias can be provided, or an arrayref of numeric values
from 0..255 declaring the colour indexes to use from least
intensive to most.
With no arguments, returns thw swatch as an arrayref.
Defaults to a traditional 'thermography' blue -> red swatch
('blue-red'). Another valid option is 'grayscale'.
=head2 swatch_names
examples/boxes.pl view on Meta::CPAN
use strict;
use warnings;
use ANSI::Heatmap;
binmode STDOUT, ':utf8';
for my $half (0,1) {
for my $box ([2,2], [3, 5], [9,9], [10,10], [11,11], [21,19]) {
my ($x, $y) = @$box;
my $map = ANSI::Heatmap->new(
half => $half,
min_x => 1,
min_y => 1,
max_x => $x,
max_y => $y,
swatch => 'grayscale',
);
my @white = (
(map { [1,$_], [$x,$_] } (1..$y)),
(map { [$_, 1], [$_, $y] } (1..$x)),
);
for my $c (@white) {
$map->set(@$c, 100);
}
print "$x x $y\n";
print $map, "\n";
}
}
examples/empty.t view on Meta::CPAN
use strict;
use warnings;
use ANSI::Heatmap;
my $map = ANSI::Heatmap->new( max_x => 10, max_y => 5 );
print $map;
examples/git.pl view on Meta::CPAN
#!/usr/bin/perl
use strict;
use warnings;
# Git commits by week by author
use ANSI::Heatmap;
use List::Util qw(max min);
my $LIMIT = 20;
my $PER_ROW = 4;
my $i;
my %day2idx = map { $_ => $i++ } qw(Mon Tue Wed Thu Fri Sat Sun);
my %map;
my %commit_count;
my $repo = shift or die "usage: $0 /path/to/git-repo";
chdir $repo;
open my $log, '-|', 'git', 'log', '--no-merges', '--remove-empty', "--format=%cN\t%cD";
while (<$log>) {
/\A([^\t]+)\t(\w+), \d+ \w+ \d+ (\d+):\d+:\d+ [-+]\d+\Z/ or die "invalid line '$_'";
my ($name, $day, $hour) = ($1, $2, $3);
$day = $day2idx{$day}; defined $day or die "Invalid day";
$commit_count{$name}++;
if (!$map{$name}) {
$map{$name} = ANSI::Heatmap->new(
min_x => 0,
max_x => 23,
min_y => 0,
max_y => 6,
half => 1,
);
}
$map{$name}->inc($hour, $day);
}
my @order = sort { $commit_count{$b} <=> $commit_count{$a} } keys %commit_count;
@order = splice @order, 0, $LIMIT;
my %header = map { $_ => "$_ (" . $commit_count{$_} . ")" } @order;
my @hdrlens = map { length $_ } values %header;
my $hdrwidth = max(@hdrlens);
my $colwidth = max($hdrwidth, 24) + 2;
my $pad = ' ' x ($colwidth - 24);
binmode STDOUT, ':utf8';
while ( my @row = splice @order, 0, $PER_ROW ) {
my $fmt = (('%-' . $colwidth . 's') x @row) . "\n";
printf $fmt, map { $header{$_} } @row;
my @maps = @map{@row};
my @split = map { [ split /\n/, $_ ] } @maps;
for my $line (0..$#{$split[0]}) {
print join '', map { $split[$_][$line] . $pad } 0..$#split;
print "\n";
}
}
examples/gray.pl view on Meta::CPAN
use strict;
use warnings;
use ANSI::Heatmap;
binmode STDOUT, ':utf8';
# Advanced usage
my $map = ANSI::Heatmap->new(
half => 1,
min_x => 1,
min_y => 1,
max_x => 10,
max_y => 10,
swatch => 'grayscale',
);
for my $x (1..10) {
for my $y (1..10) {
$map->set($x, $y, $x * $y);
}
}
print $map->to_string; # explicit
examples/interp.pl view on Meta::CPAN
use strict;
use warnings;
use ANSI::Heatmap;
binmode STDOUT, ':utf8';
my $map = ANSI::Heatmap->new(
half => 1,
swatch => 'grayscale',
);
for my $x (0..5) {
for my $y (0..5) {
$map->set($x, $y, ($x % 2 == $y % 2));
}
}
for my $wh ([3,3], [3,5], [5,5], [6,6], [12,12], [15,16], [10,20]) {
my ($w, $h) = @$wh;
$map->width($w);
$map->height($h);
$map->interpolate(0);
print "${w}x${h}\n";
print $map;
$map->interpolate(1);
print "${w}x${h}, interpolated\n";
print $map;
}
examples/random.pl view on Meta::CPAN
#!/usr/bin/perl
use strict;
use warnings;
use ANSI::Heatmap;
my $map = ANSI::Heatmap->new;
binmode STDOUT, ':utf8';
# Randomness
for (1..2000) {
my $x = int(rand(50));
my $y = int(rand(21));
$map->inc($x, $y);
}
print $map;
$map->half(1);
print "\n";
print $map;
examples/smallinterp.pl view on Meta::CPAN
use strict;
use warnings;
use ANSI::Heatmap;
binmode STDOUT, ':utf8';
my $map = ANSI::Heatmap->new(
half => 1,
swatch => 'grayscale',
);
for my $x (0..19) {
for my $y (0..19) {
my $sc = 1 + ($x >= 10) + ($y >= 10) * 2;
my $z = (($x/$sc) % 2 == ($y/$sc) % 2);
$map->set($x, $y, $z);
}
}
print "$map\n";
$map->width(15);
$map->height(15);
print "$map\n";
$map->interpolate(1);
print "$map\n";
lib/ANSI/Heatmap.pm view on Meta::CPAN
package ANSI::Heatmap;
# POD after __END__
use strict;
use warnings;
our $VERSION = 0.3;
use overload '""' => 'to_string';
use Carp;
use List::Util qw(min max);
use POSIX qw(floor modf);
use Class::Accessor::Fast;
our @ISA = qw(Class::Accessor::Fast);
our @_minmax_fields = map { ("min_$_", "max_$_") } qw(x y z);
our @_fields = ('half', 'interpolate', 'width', 'height', @_minmax_fields);
__PACKAGE__->mk_accessors(@_fields);
my $TOPBLOCK = "\N{U+2580}";
my %SWATCHES = (
'blue-red' => [0x10 .. 0x15, 0x39, 0x5d, 0x81, 0xa5, reverse(0xc4 .. 0xc9)],
'grayscale' => [0xe8 .. 0xff],
);
my $DEFAULT_SWATCH = 'blue-red';
sub new {
my $class = shift;
my %args = (@_ == 1 && ref $_[0] && ref $_[0] eq 'HASH') ? %{$_[0]} : @_;
my $self = bless { map => [], minmax => {} }, $class;
$self->swatch($DEFAULT_SWATCH);
$self->interpolate(0);
$self->half(0);
for my $field (@_fields, 'swatch') {
$self->$field(delete $args{$field}) if exists $args{$field};
}
if (keys %args) {
croak "Invalid constructor argument(s) " . join(', ', sort keys %args);
}
return $self;
}
sub swatch_names {
my $self = shift;
return (sort keys %SWATCHES);
}
sub set {
my ($self, $x, $y, $z) = @_;
$self->{map}[$y][$x] = $z;
$self->_set_minmax(x => $x, y => $y, z => $z);
}
sub get {
my ($self, $x, $y) = @_;
return $self->{map}[$y][$x] || 0;
}
sub inc {
my ($self, $x, $y) = @_;
$self->set( $x, $y, $self->get($x, $y) + 1 );
}
sub swatch {
my $self = shift;
if (@_) {
lib/ANSI/Heatmap.pm view on Meta::CPAN
}
}
return $self->{swatch};
}
sub to_string {
my $self = shift;
return $self->render($self->data);
}
# Convert heatmap hash to a 2D grid of intensities, normalised between 0 and 1,
# cropped to the min/max range supplied and scaled to the desired width/height.
sub data {
my ($self, $mm) = @_;
my %mm = $self->_figure_out_min_and_max;
my $inv_max_z = $mm{zrange} ? 1 / $mm{zrange} : 0;
my @out;
my $xscale = $mm{width} / ($mm{max_x} - $mm{min_x} + 1);
my $yscale = $mm{height} / ($mm{max_y} - $mm{min_y} + 1);
my $get = sub { $self->{map}[ $_[1] ][ $_[0] ] || 0 };
my $sample;
if (!$self->interpolate
|| $xscale == int($xscale) && $yscale == int($yscale)) {
$sample = $get; # nearest neighbour/direct lookup
}
else {
$sample = _binterp($get);
}
for my $y (0..$mm{height}-1) {
lib/ANSI/Heatmap.pm view on Meta::CPAN
my @s;
for my $y (0..$#{$matrix}) {
next if $half && $y % 2 == 1;
for my $x (0..$#{$matrix->[$y]}) {
my $top = $matrix->[$y][$x] || 0;
my $bottom = $half ? ($y == $#{$matrix} ? undef : $matrix->[$y+1][$x] || 0)
: $top;
my ($top_color, $bottom_color) = map {
$self->_swatch_lookup($_)
} grep { defined } ($top, $bottom);
my $fg = sprintf "\e[38;5;%d%s", $top_color, 'm';
my $bg = defined $bottom ? sprintf "\e[48;5;%d%s", $bottom_color, 'm'
: '';
my $char = $half ? $fg . $bg . $TOPBLOCK : $bg . ' ';
push @s, $char . "\e[0m";
lib/ANSI/Heatmap.pm view on Meta::CPAN
push @s, "\n";
}
return join '', @s;
}
# Return hash of min/max values for each axis.
sub _figure_out_min_and_max {
my $self = shift;
my %calc = (
(map { $_ => 0 } @_minmax_fields),
%{$self->{minmax}},
($self->{minmax}{min_z}||0) >= 0 ? (min_z => 0) : (),
);
# Override with user-specified values, if supplied.
for my $k (keys %calc) {
$calc{$k} = $self->{$k} if defined $self->{$k};
}
# If user did not specify width/height, assume 1x scale.
lib/ANSI/Heatmap.pm view on Meta::CPAN
return %calc;
}
sub _binterp {
my $get = shift;
return sub {
my ($x, $y) = @_;
my ($fx, $bx) = modf($x);
my ($fy, $by) = modf($y);
my @p = map { $get->($bx + $_->[0], $by + $_->[1]) } ([0,0],[0,1],[1,0],[1,1]);
my $y1 = $p[0] + ($p[1] - $p[0]) * $fy;
my $y2 = $p[2] + ($p[3] - $p[2]) * $fy;
my $z = $y1 + ($y2 - $y1) * $fx;
return $z;
};
}
sub _set_minmax {
my ($self, %vals) = @_;
lib/ANSI/Heatmap.pm view on Meta::CPAN
# Maps a number from [0,1] to a swatch colour.
sub _swatch_lookup {
my ($self, $index) = @_;
return $self->{swatch}->[$index * $#{$self->{swatch}} + .5];
}
1;
=head1 NAME
ANSI::Heatmap - render heatmaps to your terminal
=head1 SYNOPSIS
my $map = ANSI::Heatmap->new(
half => 1,
min_x => 0, max_x => 49,
min_y => 0, max_y => 49,
swatch => 'blue-red',
);
for (1..2000) {
my $x = int(rand(50));
my $y = int(rand(50));
$map->inc($x, $y);
}
print $map;
$map->interpolate(1);
$map->width(25);
$map->height(25);
$map->swatch('grayscale');
print $map;
my $data = $map->data;
# Mess with the data...
print $map->render($data);
# Custom swatch
$map->swatch([0x10 .. 0x15]);
=head1 DESCRIPTION
Produce cutting-edge ANSI heatmaps using 256 colours and weird Unicode
characters! Perfect for 3D (2D + intensity) data.
=head1 METHODS
=head2 new ( [ARGS] )
C<ARGS> may be a hash or hashref accepting the following keys, which
also have getter/setter methods:
=over 4
=item min_x, max_x ( INT )
Specify the smallest and largest X-axis value to include. If not
provided, defaults to the smallest/largest values passed to C<set>
or C<inc>. Can be used to crop the map or ensure it keeps a fixed
size even if some values are unset.
To make automatic again, set to C<undef>.
=item min_y, max_y ( INT )
Ditto for the Y-axis.
=item min_z, max_z ( FLOAT )
Ditto for intensity; useful for keeping a fixed intensity across
multiple heatmaps.
The default C<min_z> value is 0, unless negative intensities are
used.
=item swatch ( STR | ARRAYREF )
Set the colour swatch; see C<swatch> below.
=item half ( BOOL )
A boolean indicating if the map should be rendered in half-height
mode using special characters. On most terminals, this means the
X and Y axis will be scaled identically.
Off by default.
=item width, height ( INT )
Specify the width/height of the map in characters. Defaults to
using the min_I<axis> and max_I<axis> values to determine the
width/height.
=item interpolate ( BOOL )
If width/height is not a nice multiple of the input data and
this flag is set, perform bilinear interpolation (instead of
nearest neighbour). This is a trade off; interpolated data is
blurrier, but retains a linear relationship with the original
data. Off by default.
=back
=head2 set ( X, Y, Z )
Set the heatmap intensity for the given X and Y co-ordinate.
Currently, only integer values for X and Y are supported.
=head2 get ( X, Y )
Return the heatmap intensity for the given X and Y co-ordinate,
or 0 if unset.
=head2 inc ( X, Y )
Increase the intensity at the given co-ordinate by 1.
=head2 to_string
Return a string containing the ANSI heatmap. If C<half> is set,
this string contains wide characters, so you may need to:
binmode STDOUT, ':utf8';
or
use open OUT => ':utf8';
before printing anything (in this case) to STDOUT.
=head2 data
Returns the heatmap data, cropped, scaled and normalised with
intensity values between 0 and 1.
Expressed as an arrayref of arrayrefs indexed by Y and then
X co-ordinate.
=head2 render ( DATA )
Manually render heatmap data as returned by C<data>. Useful
if you want to do any custom processing.
=head2 swatch ( [ARRAYREF | STRING] )
Set the colour swatch that decided how the heatmap will look.
A string alias can be provided, or an arrayref of numeric values
from 0..255 declaring the colour indexes to use from least
intensive to most.
With no arguments, returns thw swatch as an arrayref.
Defaults to a traditional 'thermography' blue -> red swatch
('blue-red'). Another valid option is 'grayscale'.
=head2 swatch_names
use Test::More tests => 16;
use strict;
use warnings;
use ANSI::Heatmap;
my $map = ANSI::Heatmap->new;
$map->set(0,0,1);
$map->set(1,1,1);
is_deeply( $map->data, [[1,0], [0,1]], 'set' );
is( $map->get(0,0), 1 );
is( $map->get(0,1), 0 );
is( $map->get(1,0), 0 );
is( $map->get(1,1), 1 );
is( $map->get(200,0), 0 );
is( $map->to_string, "\e[48;5;196m \e[0m\e[48;5;16m \e[0m\n\e[48;5;16m \e[0m\e[48;5;196m \e[0m\n" );
is( "$map", $map->to_string );
$map->inc(0,0);
is( $map->get(0,0), 2 );
is_deeply( $map->data, [[1,0], [0,0.5]], 'inc' );
$map->set(0,3,1);
is( $map->get(0,3), 1 );
is( $map->get(1,3), 0 );
is_deeply( $map->data, [[1,0], [0,.5], [0,0], [.5,0]], 'extend y' );
$map->set(3,0,1);
is( $map->get(3,0), 1 );
is( $map->get(3,1), 0 );
is_deeply( $map->data, [[1,0,0,.5], [0,.5,0,0], [0,0,0,0], [.5,0,0,0]], 'extend x' );