PageRenderTime 30ms CodeModel.GetById 33ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/SVG/TT/Graph/HeatMap.pm

http://github.com/ranguard/svg-tt-graph
Perl | 726 lines | 566 code | 146 blank | 14 comment | 39 complexity | ba4061f2975c235bdc52a51fa49425a3 MD5 | raw file
  1. package SVG::TT::Graph::HeatMap;
  2. use Modern::Perl;
  3. use Carp;
  4. use Data::Dumper;
  5. use SVG::TT::Graph;
  6. use base qw(SVG::TT::Graph);
  7. our $VERSION = $SVG::TT::Graph::VERSION;
  8. our $TEMPLATE_FH = \*DATA;
  9. =head1 NAME
  10. SVG::TT::Graph::HeatMap - Create presentation quality SVG HeatMap graph of XYZ data points easily
  11. =head1 SYNOPSIS
  12. use SVG::TT::Graph::HeatMap;
  13. my @data_cpu = (
  14. { x => x_point1,
  15. y_point1 => 10,
  16. y_point2 => 200,
  17. y_point3 => 1000,
  18. },
  19. { x => x_point2,
  20. y_point1 => 100,
  21. y_point2 => 400,
  22. y_point3 => 500,
  23. },
  24. { x => x_point3,
  25. y_point1 => 1000,
  26. y_point2 => 600,
  27. y_point3 => 0,
  28. },
  29. );
  30. my $graph = SVG::TT::Graph::HeatMap->new(
  31. { block_height => 24,
  32. block_width => 24,
  33. gutter_width => 1,
  34. } );
  35. $graph->add_data(
  36. { 'data' => \@data_cpu,
  37. 'title' => 'CPU',
  38. } );
  39. print "Content-type: image/svg+xml\n\n";
  40. print $graph->burn();
  41. =head1 DESCRIPTION
  42. This object aims to allow you to easily create high quality
  43. SVG HeatMap graphs of XYZ data. You can either use the default style sheet
  44. or supply your own.
  45. Please note, the height and width of the final image is computed from the
  46. size of the labels, block_height/block_with and gutter_size.
  47. =head1 METHODS
  48. =head2 new()
  49. use SVG::TT::Graph::HeatMap;
  50. my $graph = SVG::TT::Graph::HeatMap->new({
  51. # Optional - defaults shown
  52. block_height => 24,
  53. block_width => 24,
  54. gutter_width => 1,
  55. 'y_axis_order' => [],
  56. });
  57. The constructor takes a hash reference with values defaulted to those
  58. shown above - with the exception of style_sheet which defaults
  59. to using the internal style sheet.
  60. =head2 add_data()
  61. my @data_cpu = (
  62. { x => x_point1,
  63. y_point1 => 10,
  64. y_point2 => 200,
  65. y_point3 => 1000,
  66. },
  67. { x => x_point2,
  68. y_point1 => 100,
  69. y_point2 => 400,
  70. y_point3 => 500,
  71. },
  72. { x => x_point3,
  73. y_point1 => 1000,
  74. y_point2 => 600,
  75. y_point3 => 0,
  76. },
  77. );
  78. or
  79. my @data_cpu = ( ['x', 'y_point1', 'y_point2', 'y_point3'],
  80. ['x_point1', 10, 200, 5],
  81. ['x_point2', 100, 400, 1000],
  82. ['x_point3', 1000, 600, 0],
  83. );
  84. $graph->add_data({
  85. 'data' => \@data_cpu,
  86. 'title' => 'CPU',
  87. });
  88. This method allows you to add data to the graph object. The
  89. data are expected to be either a array of hashes or as a 2D
  90. matrix (array of arrays), with the Y-axis as the first arrayref,
  91. and the X-axis values in the first element of subsequent arrayrefs.
  92. =head2 clear_data()
  93. my $graph->clear_data();
  94. This method removes all data from the object so that you can
  95. reuse it to create a new graph but with the same config options.
  96. =head2 burn()
  97. print $graph->burn();
  98. This method processes the template with the data and
  99. config which has been set and returns the resulting SVG.
  100. This method will croak unless at least one data set has
  101. been added to the graph object.
  102. =head2 config methods
  103. my $value = $graph->method();
  104. my $confirmed_new_value = $graph->method($value);
  105. The following is a list of the methods which are available
  106. to change the config of the graph object after it has been
  107. created.
  108. =over 4
  109. =item compress()
  110. Whether or not to compress the content of the SVG file (Compress::Zlib required).
  111. =item tidy()
  112. Whether or not to tidy the content of the SVG file (XML::Tidy required).
  113. =item block_width()
  114. The width of the blocks in px.
  115. =item block_height()
  116. The height of the blocks in px.
  117. =item gutter()
  118. The space between the blocks in px.
  119. =item y_axis_order()
  120. This is order the columns are presented on the y-axis, if the data is in a Array of hashes,
  121. this has to be set, however is the data is in an 2D matrix (array of arrays), it will use
  122. the order presented in the header array.
  123. If the data is given in a 2D matrix, and the y_axis_order is set, the y_axis_order will take
  124. prescience.
  125. =back
  126. =head1 EXAMPLES
  127. For examples look at the project home page
  128. http://leo.cuckoo.org/projects/SVG-TT-Graph/
  129. =head1 EXPORT
  130. None by default.
  131. =head1 SEE ALSO
  132. L<SVG::TT::Graph>,
  133. L<SVG::TT::Graph::Line>,
  134. L<SVG::TT::Graph::Bar>,
  135. L<SVG::TT::Graph::BarHorizontal>,
  136. L<SVG::TT::Graph::BarLine>,
  137. L<SVG::TT::Graph::Pie>,
  138. L<SVG::TT::Graph::Bubble>,
  139. L<Compress::Zlib>,
  140. L<XML::Tidy>
  141. =cut
  142. sub _init
  143. {
  144. my $self = shift;
  145. }
  146. sub _set_defaults
  147. {
  148. my $self = shift;
  149. my @fields = ();
  150. my %default = (
  151. 'fields' => \@fields,
  152. 'block_width' => 24,
  153. 'block_height' => 24,
  154. 'gutter' => 1,
  155. 'y_axis_order' => [],
  156. );
  157. while ( my ( $key, $value ) = each %default )
  158. {
  159. $self->{ config }->{ $key } = $value;
  160. }
  161. }
  162. # override this so we can pre-manipulate the data
  163. sub add_data
  164. {
  165. my ( $self, $conf ) = @_;
  166. croak 'no data provided'
  167. unless ( defined $conf->{ 'data' } &&
  168. ref( $conf->{ 'data' } ) eq 'ARRAY' );
  169. # create an array
  170. unless ( defined $self->{ 'data' } )
  171. {
  172. my @data;
  173. $self->{ 'data' } = \@data;
  174. }
  175. else
  176. {
  177. croak 'There can only be a single piece of data';
  178. }
  179. # If there is an order this takes prescience and all data points should have this
  180. # however if there are
  181. my %check;
  182. if ( 0 == scalar @{ $self->{ config }->{ y_axis_order } } )
  183. {
  184. if ( ref( $conf->{ 'data' }->[0] ) eq 'ARRAY' )
  185. {
  186. my @header = @{ $conf->{ 'data' }->[0] };
  187. $self->{ config }->{ y_axis_order } = [@header[1 .. $#header]];
  188. }
  189. }
  190. %check = map {$_, 1} @{ $self->{ config }->{ y_axis_order } };
  191. croak
  192. 'The Data needs to have either a y_axis_order or a header array in the data'
  193. if 0 == scalar keys %check;
  194. # convert to sorted (by ascending numeric value) array of [ x, y ]
  195. my @new_data = ();
  196. my ( $i, $x );
  197. $i = ref( $conf->{ 'data' }->[0] ) eq 'ARRAY' ? 1 : 0;
  198. my $max = scalar @{ $conf->{ 'data' } };
  199. while ( $i < $max )
  200. {
  201. my %row;
  202. if ( ref( $conf->{ 'data' }->[$i] ) eq 'ARRAY' )
  203. {
  204. $row{ x } = $conf->{ 'data' }->[$i]->[0];
  205. for my $col ( 1 .. $#{ $conf->{ 'data' }->[$i] } )
  206. {
  207. $row{ $conf->{ 'data' }->[0]->[$col] } =
  208. $self->colourDecide( $conf->{ 'data' }->[$i]->[$col] )
  209. #$conf->{ 'data' }->[$i]->[$col];
  210. }
  211. }
  212. elsif ( ref( $conf->{ 'data' }->[$i] ) eq 'HASH' )
  213. {
  214. # check the hash to make sure make sure the data is in it
  215. croak "row '$i' has no x value"
  216. unless defined $conf->{ 'data' }->[$i]->{ x };
  217. $row{ x } = $conf->{ 'data' }->[$i]->{ x };
  218. while ( my ( $k, $v ) = each %check )
  219. {
  220. unless ( defined $conf->{ 'data' }->[$i]->{ $k } )
  221. {
  222. croak "zzz '$row{ x }' does not have a '$k' vaule"
  223. unless ( $self->{ config }->{ include_undef_values } );
  224. }
  225. $row{ $k } =
  226. $self->colourDecide( $conf->{ 'data' }->[$i]->{ $k } );
  227. }
  228. }
  229. else
  230. {
  231. croak
  232. 'Data needs to be in an Array of Arrays or an Array of Hashes ';
  233. }
  234. push @new_data, \%row;
  235. $i++;
  236. }
  237. my %store = ( 'pairs' => \@new_data, );
  238. $store{ 'title' } = $conf->{ 'title' } if defined $conf->{ 'title' };
  239. push( @{ $self->{ 'data' } }, \%store );
  240. return 1;
  241. }
  242. # override calculations to set a few calculated values, mainly for scaling
  243. sub calculations
  244. {
  245. my $self = shift;
  246. # run through the data and calculate maximum and minimum values
  247. my ( $max_key_size, $max_x, $min_x, $max_y, $min_y, $max_x_label_length,
  248. $x_label, $max_y_label_length );
  249. my @y_axis_order = @{ $self->{ config }->{ y_axis_order } };
  250. for my $y_axis_label (@y_axis_order)
  251. {
  252. $max_y_label_length = length $y_axis_label
  253. if ( ( !defined $max_y_label_length ) ||
  254. ( $max_y_label_length < length $y_axis_label ) );
  255. }
  256. foreach my $dataset ( @{ $self->{ data } } )
  257. {
  258. $max_key_size = length( $dataset->{ title } )
  259. if ( ( !defined $max_key_size ) ||
  260. ( $max_key_size < length( $dataset->{ title } ) ) );
  261. $max_x = scalar @{ $dataset->{ pairs } }
  262. if ( ( !defined $max_x ) ||
  263. ( $max_x < scalar @{ $dataset->{ pairs } } ) );
  264. foreach my $pair ( @{ $dataset->{ pairs } } )
  265. {
  266. $min_x = 0;
  267. $max_y = scalar @y_axis_order;
  268. for my $y_vaules (@y_axis_order)
  269. {
  270. $min_y = 0;
  271. }
  272. $x_label = $pair->{ x };
  273. $max_x_label_length = length($x_label)
  274. if ( ( !defined $max_x_label_length ) ||
  275. ( $max_x_label_length < length($x_label) ) );
  276. }
  277. }
  278. $self->{ calc }->{ max_key_size } = $max_key_size;
  279. $self->{ calc }->{ max_x } = $max_x;
  280. $self->{ calc }->{ min_x } = $min_x;
  281. $self->{ calc }->{ max_y } = $max_y;
  282. $self->{ calc }->{ min_y } = $min_y;
  283. $self->{ calc }->{ max_x_label_length } = $max_x_label_length;
  284. $self->{ calc }->{ max_y_label_length } = $max_y_label_length;
  285. $self->{ config }->{ width } =
  286. ( 10 * 2 ) + ( $max_y_label_length * 8 ) + 1 + (
  287. $max_x * (
  288. $self->{ config }->{ block_width } +
  289. $self->{ config }->{ gutter_width }
  290. ) );
  291. $self->{ config }->{ height } =
  292. ( 10 * 2 ) + ( $max_x_label_length * 8 ) + 1 + (
  293. $max_y * (
  294. $self->{ config }->{ block_width } +
  295. $self->{ config }->{ gutter_width }
  296. ) );
  297. }
  298. sub defaultColours
  299. {
  300. my ($self) = @_;
  301. my %default = (
  302. '<=' => { 1000 => [0, 0, 255],
  303. 900 => [4, 150, 252],
  304. 800 => [4, 218, 252],
  305. 700 => [4, 200, 100],
  306. 600 => [36, 225, 36],
  307. 500 => [132, 255, 14],
  308. 400 => [244, 254, 4],
  309. 300 => [252, 190, 4],
  310. 200 => [252, 125, 4],
  311. 100 => [252, 2, 4],
  312. },
  313. '=' => { 0 => [0, 0, 0],
  314. -1 => [0, 0, 0],
  315. -2 => [0, 0, 0],
  316. -3 => [0, 0, 0],
  317. -4 => [0, 0, 0],
  318. } );
  319. return %default;
  320. }
  321. sub colourDecide
  322. {
  323. my ( $self, $score ) = @_;
  324. my %key = $self->defaultColours;
  325. # return the default missing colour if the score is undef
  326. return 'rgb(255,255,255)' unless defined $score;
  327. my @precidence = qw(< <= > >= = );
  328. my %tests = ( '<' => sub {return 1, if $_[0] < $_[1]},
  329. '<=' => sub {return 1, if $_[0] <= $_[1]},
  330. '>' => sub {return 1, if $_[0] > $_[1]},
  331. '>=' => sub {return 1, if $_[0] >= $_[1]},
  332. '=' => sub {return 1, if $_[0] == $_[1]},
  333. );
  334. # set this to the default so if there are no rule matches
  335. # we just use the default
  336. my $colour = [0, 0, 0];
  337. for my $symbol (@precidence)
  338. {
  339. next unless exists $key{ $symbol };
  340. my @values = sort {$b <=> $a} keys %{ $key{ $symbol } };
  341. # if we are looking for the highest we flip the order
  342. @values = reverse @values if ( $symbol =~ /^>/ );
  343. for my $value (@values)
  344. {
  345. if ( $tests{ $symbol }( $score, $value ) )
  346. {
  347. $colour = $key{ $symbol }{ $value };
  348. }
  349. }
  350. }
  351. return sprintf "rgb(%s,%s,%s)", @$colour;
  352. }
  353. 1;
  354. __DATA__
  355. <?xml version="1.0"?>
  356. <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN"
  357. "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
  358. [% stylesheet = 'included' %]
  359. [% IF config.style_sheet && config.style_sheet != '' %]
  360. <?xml-stylesheet href="[% config.style_sheet %]" type="text/css"?>
  361. [% ELSE %]
  362. [% stylesheet = 'excluded' %]
  363. [% END %]
  364. <svg width="[% config.width %]" height="[% config.height %]" viewBox="0 0 [% config.width %] [% config.height %]" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  365. <!-- \\\\\\\\\\\\\\\\\\\\\\\\\\\\ -->
  366. <!-- Created with SVG::TT::Graph -->
  367. <!-- Dave Meibusch -->
  368. <!-- //////////////////////////// -->
  369. [% IF stylesheet == 'excluded' %]
  370. [%# include default stylesheet if none specified %]
  371. <defs>
  372. <style type="text/css">
  373. <![CDATA[
  374. /* Copy from here for external style sheet */
  375. .svgBackground{
  376. fill:#f0f0f0;
  377. }
  378. .graphBackground{
  379. fill:#fafafa;
  380. }
  381. /* graphs titles */
  382. .mainTitle{
  383. text-anchor: middle;
  384. fill: #000000;
  385. font-size: 14px;
  386. font-family: "Arial", sans-serif;
  387. font-weight: normal;
  388. }
  389. .subTitle{
  390. text-anchor: middle;
  391. fill: #999999;
  392. font-size: 12px;
  393. font-family: "Arial", sans-serif;
  394. font-weight: normal;
  395. }
  396. .axis{
  397. stroke: #000000;
  398. stroke-width: 1px;
  399. }
  400. .guideLines{
  401. stroke: #666666;
  402. stroke-width: 1px;
  403. stroke-dasharray: 5 5;
  404. }
  405. .xAxisLabels{
  406. text-anchor: middle;
  407. fill: #000000;
  408. font-size: 12px;
  409. font-family: "Arial", sans-serif;
  410. font-weight: normal;
  411. }
  412. .yAxisLabels{
  413. text-anchor: end;
  414. fill: #000000;
  415. font-size: 12px;
  416. font-family: "Lucida Console", Monaco, monospace;
  417. font-weight: normal;
  418. }
  419. .xAxisTitle{
  420. text-anchor: middle;
  421. fill: #ff0000;
  422. font-size: 14px;
  423. font-family: "Lucida Console", Monaco, monospace;
  424. font-weight: normal;
  425. }
  426. .yAxisTitle{
  427. fill: #ff0000;
  428. text-anchor: middle;
  429. font-size: 14px;
  430. font-family: "Arial", sans-serif;
  431. font-weight: normal;
  432. }
  433. .dataPointLabel{
  434. fill: #000000;
  435. text-anchor:middle;
  436. font-size: 10px;
  437. font-family: "Arial", sans-serif;
  438. font-weight: normal;
  439. }
  440. .staggerGuideLine{
  441. fill: none;
  442. stroke: #000000;
  443. stroke-width: 0.5px;
  444. }
  445. [% FOREACH dataset = data %]
  446. [% color = '' %]
  447. [% IF config.random_colors %]
  448. [% color = random_color() %]
  449. [% ELSE %]
  450. [% color = predefined_color(loop.count) %]
  451. [% END %]
  452. .fill[% loop.count %]{
  453. fill: [% color %];
  454. fill-opacity: 0.2;
  455. stroke: none;
  456. }
  457. .line[% loop.count %]{
  458. fill: none;
  459. stroke: [% color %];
  460. stroke-width: 1px;
  461. }
  462. .key[% loop.count %],.fill[% loop.count %]{
  463. fill: [% color %];
  464. stroke: none;
  465. stroke-width: 1px;
  466. }
  467. [% LAST IF (config.random_colors == 0 && loop.count == 12) %]
  468. [% END %]
  469. .keyText{
  470. fill: #000000;
  471. text-anchor:start;
  472. font-size: 10px;
  473. font-family: "Arial", sans-serif;
  474. font-weight: normal;
  475. }
  476. /* End copy for external style sheet */
  477. ]]>
  478. </style>
  479. </defs>
  480. [% END %]
  481. [% IF config.key %]
  482. <!-- Script to toggle paths when their key is clicked on -->
  483. <script language="JavaScript"><![CDATA[
  484. function togglePath( series ) {
  485. var path = document.getElementById('groupDataSeries' + series);
  486. var points = document.getElementById('groupDataLabels' + series);
  487. var current = path.getAttribute('opacity');
  488. if ( path.getAttribute('opacity') == 0 ) {
  489. path.setAttribute('opacity',1);
  490. points.setAttribute('opacity',1);
  491. } else {
  492. path.setAttribute('opacity',0);
  493. points.setAttribute('opacity',0);
  494. }
  495. }
  496. ]]></script>
  497. [% END %]
  498. <!-- svg bg -->
  499. <rect x="0" y="0" width="[% config.width %]" height="[% config.height %]" class="svgBackground"/>
  500. <!-- ///////////////// CALCULATE GRAPH AREA AND BOUNDARIES //////////////// -->
  501. [%# get dimensions of actual graph area (NOT SVG area) %]
  502. [% w = config.width %]
  503. [% h = config.height %]
  504. [%# set start/default coords of graph %]
  505. [% x = 0 %]
  506. [% y = 0 %]
  507. [% char_width = 8 %]
  508. [% half_char_height = 2.5 %]
  509. <!-- min_y [% calc.min_y %] max_y [% calc.max_y %] min_x [% calc.min_x %] max_x [% calc.max_x %] -->
  510. [%# reduce height and width of graph area for padding %]
  511. [% h = h - 20 %]
  512. [% w = w - 20 %]
  513. [% x = x + 10 %]
  514. [% y = y + 10 %]
  515. [% max_x_label_char = calc.max_x_label_length * char_width %]
  516. [% max_y_label_char = calc.max_y_label_length * char_width %]
  517. [% w = w - max_y_label_char %]
  518. [% x = x + max_y_label_char %]
  519. [% h = h - max_x_label_char %]
  520. [%# y = y + max_x_label_char %]
  521. <!-- max_x_label_char [% calc.max_x_label_length %] max_y_label_char [% calc.max_y_label_length %] -->
  522. <!-- ////////////////////////////// BUILD GRAPH AREA ////////////////////////////// -->
  523. [%# graph bg and clipping regions for lines/fill and clip extended to included data labels %]
  524. <rect x="[% x %]" y="[% y %]" width="[% w %]" height="[% h %]" class="graphBackground"/>
  525. [% base_line = h + y %]
  526. <!-- axis -->
  527. <path d="M[% x %] [% y %] v[% h %]" class="axis" id="xAxis"/>
  528. <path d="M[% x %] [% base_line %] h[% w %]" class="axis" id="yAxis"/>
  529. <!-- x axis labels -->
  530. [%# TODO %]
  531. <!-- y axis labels -->
  532. [%# TODO %]
  533. <g id="groupData" class="data">
  534. [% FOREACH dataset = data.reverse %]
  535. <g id="groupDataSeries[% line %]" class="dataSeries[% line %]" clip-path="url(#clipGraphArea)">
  536. [% xx = 0 %]
  537. [% yy = 0 %]
  538. [% FOREACH y_data = config.y_axis_order %]
  539. <text
  540. x="[% max_y_label_char %]"
  541. y="[% (base_line - 1 ) - (yy * (config.block_height + config.gutter_width)) - config.block_height / 3 %]"
  542. class="yAxisLabels">
  543. [% y_data %]
  544. </text>
  545. [% yy = yy + 1 %]
  546. [% END %]
  547. [% FOREACH pair = dataset.pairs %]
  548. [% yy = 0 %]
  549. [% block_start_x = x + 1 + (xx * (config.block_width + config.gutter_width)) %]
  550. [% IF config.debug %]
  551. <circle
  552. cx="[% block_start_x + (config.block_width / 2 ) %]"
  553. cy="[% (base_line + 5) %]"
  554. r="2" fill="red" />
  555. [% END %]
  556. [% textx = block_start_x %]
  557. [% texty = (base_line + 1) %]
  558. <text
  559. x="[% textx %]"
  560. y="[% texty %]"
  561. transform="rotate(90 [% textx %],[% texty %]) translate([% (pair.x.length + 1) * 4 %], [% (config.block_height - config.gutter_width) / -3 %])"
  562. class="xAxisLabels">[% pair.x %]</text>
  563. [% FOREACH y_data = config.y_axis_order %]
  564. <rect
  565. x="[% block_start_x %]"
  566. y="[% (base_line - 1 - config.block_height) - (yy * (config.block_height + config.gutter_width)) %]"
  567. width="[% config.block_width %]"
  568. height="[% config.block_height %]" style="fill:[% pair.$y_data %]" />
  569. <!-- [% y_data %] -z- [% pair.$y_data %] -->
  570. [% yy = yy + 1 %]
  571. [% END %]
  572. [% xx = xx + 1 %]
  573. [% END %]
  574. [% END %]
  575. </g>
  576. </g>
  577. [% IF config.debug %]
  578. <circle cx="[% x %]" cy="[% base_line %]" r="1" stroke="black" stroke-width="1" fill="red" />
  579. [% END %]
  580. </svg>