File Coverage

blib/lib/Chart/ECharts.pm
Criterion Covered Total %
statement 33 214 15.4
branch 0 48 0.0
condition 1 24 4.1
subroutine 11 46 23.9
pod 35 35 100.0
total 80 367 21.8


line stmt bran cond sub pod time code
1             package Chart::ECharts;
2              
3 1     1   319718 use feature ':5.10';
  1         3  
  1         188  
4 1     1   8 use strict;
  1         2  
  1         41  
5 1     1   10 use utf8;
  1         3  
  1         7  
6 1     1   32 use warnings;
  1         2  
  1         90  
7              
8 1     1   699 use Digest::SHA qw(sha1_hex);
  1         4159  
  1         118  
9 1     1   9 use File::Basename;
  1         3  
  1         81  
10 1     1   19 use File::ShareDir qw(dist_file);
  1         3  
  1         109  
11 1     1   8 use File::Spec;
  1         4  
  1         31  
12 1     1   587 use IPC::Open3;
  1         5845  
  1         65  
13 1     1   9 use JSON::PP ();
  1         2  
  1         79  
14              
15             our $VERSION = '1.03';
16             $VERSION =~ tr/_//d; ## no critic
17              
18 1   50 1   6 use constant DEBUG => $ENV{ECHARTS_DEBUG} || 0;
  1         3  
  1         8956  
19              
20             sub new {
21              
22 0     0 1   my $class = shift;
23              
24 0           my %params = (
25             charts_object => 'ChartECharts',
26             class => 'chart-container',
27             container_prefix => '',
28             dataset => [],
29             events => {},
30             scripts => [],
31             height => undef,
32             id => ('chart_' . get_random_id()),
33             locale => 'en',
34             options => {},
35             renderer => 'canvas',
36             responsive => 0,
37             series => [],
38             styles => ['min-width:auto', 'min-height:300px'],
39             theme => 'white',
40             vertical => 0,
41             width => undef,
42             xAxis => [],
43             yAxis => [],
44             init_method => 'event',
45             init_event => 'load',
46             @_
47             );
48              
49 0           my $self = {%params};
50              
51 0           $self->{js} = {};
52              
53 0           return bless $self, $class;
54              
55             }
56              
57 0     0 1   sub chart_id { shift->{id} }
58              
59 0     0 1   sub init_function { join '_', 'init', shift->{id} }
60              
61             sub set_options {
62 0     0 1   my ($self, %options) = @_;
63 0           $self->{options} = {%{$self->{options}}, %options};
  0            
64             }
65              
66             sub set_option {
67 0     0 1   Carp::carp 'DEPRECATED use $chart->set_options(%params)';
68 0           shift->set_options(@_);
69             }
70              
71             sub set_option_item {
72 0     0 1   my ($self, $name, $params) = @_;
73 0           $self->{options}->{$name} = $params;
74             }
75              
76             sub set_title {
77 0     0 1   my ($self, %params) = @_;
78 0           $self->set_options(title => \%params);
79             }
80              
81             sub set_tooltip {
82 0     0 1   my ($self, %params) = @_;
83 0           $self->set_options(tooltip => \%params);
84             }
85              
86             sub set_toolbox {
87 0     0 1   my ($self, %params) = @_;
88 0           $self->set_options(toolbox => \%params);
89             }
90              
91             sub set_legend {
92 0     0 1   my ($self, %params) = @_;
93 0           $self->set_options(legend => \%params);
94             }
95              
96             sub set_timeline {
97 0     0 1   my ($self, %params) = @_;
98 0           $self->set_options(timeline => \%params);
99             }
100              
101             sub set_data_zoom {
102 0     0 1   my ($self, %params) = @_;
103 0           $self->set_options(dataZoom => \%params);
104             }
105              
106             sub add_data_zoom {
107              
108 0     0 1   my ($self, %params) = @_;
109              
110 0   0       $self->{options}->{dataZoom} //= [];
111 0           push @{$self->{options}}, \%params;
  0            
112              
113             }
114              
115             sub get_random_id {
116 0     0 1   return sha1_hex(join('', time, rand));
117             }
118              
119             sub set_event {
120 0     0 1   my ($self, $event, $callback) = @_;
121 0           $self->{events}->{$event} = $callback;
122             }
123              
124             sub add_script {
125 0     0 1   my ($self, $script) = @_;
126 0           push @{$self->{scripts}}, $script;
  0            
127             }
128              
129 0     0 1   sub on { shift->set_event(@_) }
130              
131             sub set_xAxis {
132 0     0 1   my ($self, %axis) = @_;
133 0           $self->{xAxis} = \%axis;
134             }
135              
136             sub add_xAxis {
137 0     0 1   my ($self, %axis) = @_;
138 0           push @{$self->{xAxis}}, \%axis;
  0            
139             }
140              
141             sub set_yAxis {
142 0     0 1   my ($self, %axis) = @_;
143 0           $self->{yAxis} = \%axis;
144             }
145              
146             sub add_yAxis {
147 0     0 1   my ($self, %axis) = @_;
148 0           push @{$self->{yAxis}}, \%axis;
  0            
149             }
150              
151             sub add_series {
152 0     0 1   my ($self, %series) = @_;
153 0           push @{$self->{series}}, \%series;
  0            
154             }
155              
156             sub add_dataset {
157 0     0 1   my ($self, %dataset) = @_;
158 0           push @{$self->{dataset}}, \%dataset;
  0            
159             }
160              
161 0     0 1   sub xAxis { shift->{xAxis} }
162 0     0 1   sub yAxis { shift->{yAxis} }
163 0     0 1   sub series { shift->{series} }
164 0     0 1   sub dataset { shift->{dataset} }
165              
166 0     0 1   sub default_options { {} }
167              
168             sub options {
169              
170 0     0 1   my ($self) = @_;
171              
172 0           my $default_options = $self->default_options;
173 0           my $global_options = $self->{options};
174              
175 0   0       my $default_series_options = delete $default_options->{series} || {};
176 0   0       my $series_options = delete $global_options->{series} || {};
177              
178 0           my $options = {series => $self->series};
179              
180 0           for (my $i = 0; $i < @{$options->{series}}; $i++) {
  0            
181 0           $options->{series}->[$i] = {%{$options->{series}->[$i]}, %{$default_series_options}};
  0            
  0            
182 0           $options->{series}->[$i] = {%{$options->{series}->[$i]}, %{$series_options}};
  0            
  0            
183             }
184              
185 0 0         if (@{$self->dataset}) {
  0            
186 0 0         if (scalar @{$self->dataset} == 1) {
  0            
187 0           $options->{dataset} = $self->dataset->[0];
188             }
189             else {
190 0           $options->{dataset} = \@{$self->dataset};
  0            
191             }
192             }
193              
194 0           $options = {%{$options}, %{$self->axies}, %{$default_options}, %{$global_options}};
  0            
  0            
  0            
  0            
195              
196 0           return $options;
197              
198             }
199              
200             sub axies {
201              
202 0     0 1   my ($self) = @_;
203              
204 0 0         if ($self->{vertical}) {
205 0           return {xAxis => $self->yAxis, yAxis => $self->xAxis};
206             }
207              
208 0           return {xAxis => $self->xAxis, yAxis => $self->yAxis};
209              
210             }
211              
212             sub render_script {
213              
214 0     0 1   my ($self, %params) = @_;
215              
216 0           my $chart_id = $self->{id};
217 0           my $charts_object = $self->{charts_object};
218 0           my $theme = $self->{theme};
219 0           my $renderer = $self->{renderer};
220 0           my $locale = $self->{locale};
221 0           my $container = join '', $self->{container_prefix}, $chart_id;
222 0           my $init_event = $self->{init_event};
223 0           my $init_method = $self->{init_method};
224              
225 0   0       my $wrap = $params{wrap} //= 0;
226              
227 0 0         Carp::croak 'Malformed chart "id" name' if ($chart_id !~ /^[a-zA-Z0-9_-]*$/);
228 0 0         Carp::croak 'Malformed "charts_object" name' if ($charts_object !~ /^[a-zA-Z0-9_-]*$/);
229 0 0         Carp::croak 'Malformed chart "theme"' if ($theme !~ /^[a-zA-Z0-9_-]*$/);
230 0 0         Carp::croak 'Malformed chart "container"' if ($container !~ /^[a-zA-Z0-9_-]*$/);
231 0 0         Carp::croak 'Malformed chart "locale"' if ($locale !~ /^[a-zA-Z_-]*$/);
232 0 0         Carp::croak 'Malformed chart "renderer"' if ($renderer !~ /^(svg|canvas)$/);
233 0 0         Carp::croak 'Malformed init event name' if ($init_event !~ /^[a-zA-Z\_\-\:]*$/);
234 0 0         Carp::croak 'Unknown "init_method"' if ($init_method !~ /^(event|iife)$/);
235              
236 0           my $json = JSON::PP->new;
237              
238 0           $json->utf8->canonical->allow_nonref->allow_unknown->allow_blessed->convert_blessed->escape_slash(0);
239              
240 0           my $option = $json->encode($self->options);
241              
242 0           foreach my $identifier (keys %{$self->{js}}) {
  0            
243              
244 0           my $search = qr/"\{JS:$identifier\}"/;
245 0           my $replace = $self->{js}->{$identifier};
246              
247 0           $option =~ s/$search/$replace/;
248              
249             }
250              
251 0           my @script = ();
252              
253 0           my $init_options = $json->encode({locale => $locale, renderer => $renderer});
254              
255 0           my $extra_script = '';
256 0           my $chart_events = '';
257 0           my $responsive = '';
258 0           my $init_script = qq[window.addEventListener('$init_event', init_$chart_id);];
259              
260 0           foreach my $script (@{$self->{scripts}}) {
  0            
261 0           $extra_script .= qq[
262             $script
263             ];
264             }
265              
266 0           foreach my $event (keys %{$self->{events}}) {
  0            
267 0           my $callback = $self->{events}->{$event};
268 0           $chart_events .= qq[
269             chart.on('$event', function (params) { $callback });
270             ];
271             }
272              
273 0 0         if ($self->{responsive}) {
274 0           $responsive = qq[
275             window.addEventListener('resize', function () { chart.resize() });
276             ];
277             }
278              
279 0 0         if ($self->{init_method} eq 'iife') {
280 0           $init_script = qq[(function(){ init_$chart_id() })();];
281             }
282              
283 0           my $script = qq[
284             if (!window.$charts_object) { window.$charts_object = {} }
285             if (!window.$charts_object.charts) { window.$charts_object.charts = {} }
286              
287             function init_$chart_id() {
288              
289             let chartContainer = document.getElementById('$container');
290              
291             if (! chartContainer) { return false }
292              
293             let chart = echarts.init(chartContainer, '$theme', $init_options);
294             let option = $option;
295              
296             option && chart.setOption(option);
297              
298             window.$charts_object.charts["$chart_id"] = chart;
299              
300             $chart_events
301             $extra_script
302             $responsive
303              
304             return chart;
305              
306             }
307              
308             $init_script
309             ];
310              
311 0 0         return "" if $wrap;
312              
313 0           return $script;
314              
315             }
316              
317             sub render_html {
318              
319 0     0 1   my ($self) = @_;
320              
321 0           my $style = '';
322 0           my @styles = @{$self->{styles}};
  0            
323              
324 0 0         push @styles, sprintf('width:%s', $self->{width}) if ($self->{width});
325 0 0         push @styles, sprintf('height:%s', $self->{height}) if ($self->{height});
326              
327 0           my $script = $self->render_script;
328              
329 0           my $chart_id = $self->{id};
330 0           my $container_id = join '', $self->{container_prefix}, $chart_id;
331 0           my $styles = join ';', @styles, $style;
332 0           my $class_container = $self->{class};
333              
334 0           my $html = qq[
335            
336            
339            
340             ];
341              
342 0           return $html;
343              
344             }
345              
346             sub render_image {
347              
348 0     0 1   my ($self, %params) = @_;
349              
350 0           my $render_script = dist_file('Chart-ECharts', 'render.cjs');
351              
352 0           my $node_path = delete $params{node_path};
353 0   0       my $node_bin = delete $params{node_bin} || '/usr/bin/node';
354 0   0       my $output = delete $params{output} || Carp::croak 'Specify "output" file';
355 0           my $format = delete $params{format};
356 0           my $width = delete $params{width};
357 0           my $height = delete $params{height};
358 0           my $option = JSON::PP->new->encode($self->options);
359              
360 0 0         if (!$format) {
361              
362 0           my ($file, $dir, $suffix) = fileparse($output, ('.png', ',svg'));
363              
364 0 0         Carp::croak 'Unsupported output "format"' unless $suffix;
365              
366 0           ($format = $suffix) =~ s/\.//;
367              
368             }
369              
370 0 0 0       if ($format ne 'png' && $format ne 'svg') {
371 0           Carp::croak 'Unknown output "format"';
372             }
373              
374 0 0         if ($node_bin !~ /(node|node.exe)$/i) {
375 0           Carp::croak 'Unknown node command';
376             }
377              
378 0 0 0       if (!-e $node_bin && !-x _) {
379 0           Carp::croak 'Node binary not found';
380             }
381              
382 0 0 0       local $ENV{NODE_PATH} //= $node_path if $node_path;
383              
384 0           my @cmd = ($node_bin, $render_script, '--output', $output, '--format', $format, '--option', $option);
385              
386 0 0         push @cmd, '--width', $width if ($width);
387 0 0         push @cmd, '--height', $height if ($height);
388              
389 0           DEBUG and say STDERR sprintf('Command: %s', join ' ', @cmd);
390              
391 0           my $pid = open3(my $stdin, my $stdout, my $stderr, @cmd);
392              
393 0           waitpid($pid, 0);
394 0           my $exit_status = $? >> 8;
395              
396 0           if (DEBUG) {
397              
398             say STDERR "Enviroment Variables:";
399             say STDERR sprintf("NODE_PATH=%s\n", $ENV{NODE_PATH} || '');
400              
401             if ($stderr) {
402             say STDERR 'Command STDERR:';
403             say STDERR <$stderr>;
404             }
405              
406             if ($stdout) {
407             say STDERR 'Command STDOUT:';
408             say STDERR <$stdout>;
409             }
410              
411             say STDERR "Command exit status: $exit_status";
412              
413             }
414              
415              
416             }
417              
418             sub js {
419              
420 0     0 1   my ($self, $expression) = @_;
421              
422 0           my $identifier = sha1_hex($expression);
423 0           $self->{js}->{$identifier} = $expression;
424              
425 0           return "{JS:$identifier}";
426              
427             }
428              
429 0     0 1   sub TO_JSON { shift->options }
430              
431              
432             1;
433              
434             __END__