diff --git a/glucometer_graphs.pl b/glucometer_graphs.pl index 505a26c..1734de3 100644 --- a/glucometer_graphs.pl +++ b/glucometer_graphs.pl @@ -13,19 +13,23 @@ use warnings; use Getopt::Long; use IPC::Open3; use Time::Piece; +use Time::Seconds; use Data::Dumper; +$Data::Dumper::Sortkeys = 1; + my $error = "Usage: $0 --input --output [--max ] [--low ] [--high ]\n"; -my @lines; -my @sorted_lines; +my @filelines; +my @sortedlines; my @data; my @avg_data; -my %intervals; -my %seen; -my $a1c_calc; +my $intervals; +my %seen_days; +my %seen_weeks; my $page_size; my $gnuplot_data; -my $total_graphs; +my $total_day_graphs; +my $total_week_graphs; my $count_graphs = 0; my $page_number = 0; my $interval = 15; # The number of minutes to average points for the area range graph @@ -38,7 +42,7 @@ my $min_glucose = 4; my $graph_max = 21; my $units = ''; my $page = 'a4'; -my $days_per_page = 2; +my $graphs_per_page = 2; GetOptions ("input=s" => \$input, # The name of the CSV file from which to read values "output=s" => \$output, # The name of the PDF file to output "high:f" => \$max_glucose, # The high end of your target blood glucose level @@ -46,15 +50,55 @@ GetOptions ("input=s" => \$input, # The name of the CSV file from wh "max:i" => \$graph_max, # The highest displayed glucose level on each graph "units:s" => \$units, # mmol/L or mg/dL "pagesize:s" => \$page, # size of page to print - "graphs:i" => \$days_per_page) # The number of days printed on each page + "graphs:i" => \$graphs_per_page) # The number of days printed on each page or die $error; + + +# Calculate the max and min glucose values for each time interval +# Takes an array ref of lines; returns a hash of intervals by time and min/max +sub calculate_max_min { + my %opts = @_; + my $lines = $opts{lines}; + my $fmt = $opts{format} || qq("%Y-%m-%d %H:%M:%S"); + my %intervals; + foreach my $row ( @{$lines} ) { + my ( $key, $value ) = split /,/, $row; + $value =~ s#"(.*)"#$1#; + my $date = Time::Piece->strptime( $key, $fmt ); + my ( $hour, $minute ) = ( $date->hour, $date->min ); + my $time = sprintf( "%02d:%02d:00", $hour, int($minute/$interval)*$interval ); + + # Override the current minimum values for this interval if it + # exists; otherwise, set it + if ( exists ( $intervals{$time}{min} ) ) { + if ( $intervals{$time}{min} < $value ) { + $intervals{$time}{min} = $value; + } + } else { + $intervals{$time}{min} = $value; + } + # Override the current maximum values for this interval if it + # exists; otherwise, set it + if ( exists ( $intervals{$time}{max} ) ) { + if ( $intervals{$time}{max} > $value ) { + $intervals{$time}{max} = $value; + } + } else { + $intervals{$time}{max} = $value; + } + } + return \%intervals; +} + + + open( my $ifh, '<:encoding(UTF-8)', $input ) or die "Could not open file '$input' $!"; while ( my $row = <$ifh> ) { chomp( $row ); - push @lines, $row; + push @filelines, $row; } close( $ifh ) @@ -71,64 +115,230 @@ if ( $page =~ /a4/i ) { $page_size = "29.7cm,21.0cm"; } -# Set up basic gnuplot options for reading the CSV data +# Standardise units for gnuplot's A1C calculations +if ( $units =~ /mg/i ) { + $units = 'mg/dL'; +} elsif ( $units =~ /mmol/i ) { + $units = 'mmol/L'; +} else { + $units = ''; +} + +# Get the list of days for which to produce graphs +foreach my $row ( @filelines ) { + if ( $row =~ m#^"((\d{4})-(\d{2})-(\d{2}))#ms ) { + my ( $date,$year,$month,$day ) = ( $1, split /-/, $1 ); + my $time = Time::Piece->strptime( $date, "%Y-%m-%d" ); + my $week = $time->strftime("%W"); + $seen_weeks{$year}{$week}++; + $seen_days{$date}++; + } +} +$total_day_graphs = scalar keys %seen_days; +$total_week_graphs = scalar keys %seen_weeks; + +$intervals = calculate_max_min( 'lines' => \@filelines, 'format' => '"%Y-%m-%d %H:%M:%S"' ); + +# Set up basic gnuplot output options push @data, qq( set terminal pdf size $page_size enhanced font 'Calibri,14' linewidth 1 #set output '$output' - ); -# Get the list of days for which to produce graphs -foreach my $row ( @lines ) { - if ( $row =~ m#^"(\d{4}-\d{2}-\d{2})#ms ) { - my $day = $1; - $seen{$day}++; - } -} -$total_graphs = scalar keys %seen; - # Read each line into a $Data variable for use by gnuplot -# Then sample into a smoothed plot for each day, and store each smoothed line in a new $SmoothData$date variable -foreach my $d ( sort keys %seen ) { +foreach my $d ( sort keys %seen_days ) { my $label = "$1$2$3" if ( $d =~ m#(\d{4})-(\d{2})-(\d{2})# ); push @data, qq( \$Data$label << EOD "timestamp","blood glucose","meal","method","comment"); - foreach my $row (@lines) { - if ( $row =~ s#^"($d )#"$1#ms ) { - push @data, $row; + @sortedlines = (); + foreach my $row (@filelines) { + if ( $row =~ m#^"$d .*$# ) { + push @sortedlines, qq($row); } } +# @sortedlines = map { " $_" } @sortedlines; # indent data structure + push @data, join "\n", sort @sortedlines; push @data, qq(EOD); +} - push @data, qq( +# Output data averages by hour of the day +push @data, qq( +\$DataAvg << EOD +"timestamp","blood glucose","meal","method","comment"); +@sortedlines = (); +foreach my $row (@filelines) { + if ( $row =~ m#^(")\d{4}-\d{2}-\d{2} (.*)$# ) { + push @sortedlines, qq($1$2); + } +} +#@sortedlines = map { " $_" } @sortedlines; # indent data structure +push @data, join "\n", sort @sortedlines; +push @data, qq(EOD); + +# Output the max and min glucose values for each $interval time period +push @data, qq( +\$DataMaxMin << EOD +"timestamp","max","min"); +@sortedlines = (); +foreach my $time ( sort keys %$intervals ) { + push @sortedlines, qq("$time","$intervals->{$time}->{max}","$intervals->{$time}->{min}"); +} +#@sortedlines = map { " $_" } @sortedlines; # indent data structure +push @data, join "\n", sort @sortedlines; +push @data, qq(EOD); + + +# Output weekly data averages by hour of the day +foreach my $year ( sort keys %seen_weeks ) { + foreach my $week ( sort keys %{$seen_weeks{$year}} ) { + my $time = Time::Piece->strptime( $year, "%Y" ); + my $mon = $time + ( ONE_WEEK * ( $week - 1 ) ); + my $sun = $time + ( ONE_WEEK * ( $week - 1 ) ) + ( ONE_DAY * 6 ); + my $label = $mon->strftime("%Y%m%d"); + + # Select data from the week in question + my @weeklines; + foreach my $row (@filelines) { + foreach my $dow ( 0 .. 7 ) { + my $day = $mon + ( ONE_DAY * $dow ); + my $d = $day->strftime("%Y-%m-%d"); + if ( $row =~ m#^"$d .*$#ms ) { + push @weeklines, $row; + } + } + } + + push @data, qq( +\$DataWeek$label << EOD +"timestamp","blood glucose","meal","method","comment"); + @sortedlines = (); + foreach my $row (@weeklines) { + if ($row =~ m#^(")\d{4}-\d{2}-\d{2} (.*)$# ) { + push @sortedlines, qq($1$2); + } + } +# @sortedlines = map { " $_" } @sortedlines; # indent data structure + push @data, join "\n", sort @sortedlines; + push @data, qq(EOD); + + + push @data, qq( +\$DataWeekAvg$label << EOD +"timestamp","blood glucose","meal","method","comment"); + @sortedlines = (); + foreach my $row (@weeklines) { + if ($row =~ m#^(")\d{4}-\d{2}-\d{2} (.*)$# ) { + push @sortedlines, qq($1$2); + } + } + my $week_intervals = calculate_max_min( 'lines' => \@sortedlines, 'format' => '"%H:%M:%S"' ); +# @sortedlines = map { " $_" } @sortedlines; # indent data structure + push @data, join "\n", sort @sortedlines; + push @data, qq(EOD); + + + push @data, qq( +\$DataMaxMin$label << EOD +"timestamp","max","min"); + @sortedlines = (); + foreach my $time ( sort keys %{$week_intervals} ) { + push @sortedlines, qq("$time","$intervals->{$time}->{max}","$intervals->{$time}->{min}"); + } +# @sortedlines = map { " $_" } @sortedlines; # indent data structure + push @data, sort @sortedlines; + push @data, qq(EOD); + } +} + + +# Sample each day's values into a smoothed plot, and store each plot in a new table +push @data, qq( set datafile separator "," +# Read the CSV time format set timefmt "%Y-%m-%d %H:%M:%S" +# Store in table in seconds, as the value must be a number set format x "%s" timedate set format y "%.2f" numeric + +); +foreach my $d ( sort keys %seen_days ) { + my $label = "$1$2$3" if ( $d =~ m#(\d{4})-(\d{2})-(\d{2})# ); + push @data, qq( set samples 10000 + set xdata stats \$Data$label using 2 Mean$label = STATS_mean set xdata time set table \$SmoothData$label -#plot \$Data$label using "timestamp":"blood glucose" -#plot \$Data$label using "timestamp":"blood glucose" smooth frequency -plot \$Data$label using "timestamp":"blood glucose" smooth mcsplines -#plot \$Data$label using "timestamp":"blood glucose" smooth bezier +plot \$Data$label using 1:2 smooth mcsplines +unset table +set table 'SmoothData$label' +plot \$Data$label using 1:2 smooth mcsplines unset table -undefine \$Data$label ); } +# Sample the average $interval values into a smoothed plot, and store in a new table +push @data, qq( +set datafile separator "," + +# Read the CSV time format +set timefmt "%H:%M:%S" +# Store in table in seconds, as the value must be a number +set format x "%s" timedate +set format y "%.2f" numeric + +set samples 10000 + +set xdata +stats \$DataAvg using 2 +MeanTotal = STATS_mean + +set xdata time + +set table \$DataAvgTable +plot \$DataAvg using 1:2 smooth mcsplines +unset table + +set table \$SmoothDataAvg +plot \$DataAvg using 1:2 smooth bezier +unset table + + +# Convert DataMaxMin from CSV to table +set table \$DataMaxMinTable +plot \$DataMaxMin using 1:2:3 with table +unset table +); + + + + + + + + + + + + + + + + + + + # Set up output options for gnuplot. # We don't bother to do this at the start, since the CSV needs a comma separator # and the new $SmoothData, which contains a table, needs a whitespace separator push @data, qq( -# change separator from CSV to table -reset +# ensure separator handles tables +#reset set datafile separator whitespace set key off @@ -137,10 +347,9 @@ set xdata time set timefmt "%H:%M:%S" set format x "%H:%M" timedate set format y "%.0f" numeric - -set yrange [0:$graph_max] # If extended to 23:59, the x grid overlaps with the border set xrange ["00:00":"23:58"] +set yrange [0:$graph_max] set style line 100 dt 3 lw 1 lc rgb "#202020" set style line 101 dt 1 lw 1 lc rgb "#202020" @@ -151,14 +360,14 @@ set rmargin 10 set tmargin 5 set bmargin 5 -set multiplot title layout $days_per_page,1 +set multiplot title layout $graphs_per_page,1 ); # For each day, generate a graph with some fancy options -foreach my $d ( sort keys %seen ) { +foreach my $d ( sort keys %seen_days ) { my $label = "$1$2$3" if ( $d =~ m#(\d{4})-(\d{2})-(\d{2})# ); - my $time = Time::Piece->strptime ( $d, "%Y-%m-%d" ); + my $time = Time::Piece->strptime( $d, "%Y-%m-%d" ); #my $title = $time->strftime("%a %d %b %Y"); my $title = $time->strftime("%A, %d %B %Y"); @@ -172,8 +381,6 @@ set xtics left tc rgb "#000000" set ytics 2 tc rgb "#000000" set grid ytics ls 100 front -#set arrow from graph 0,first $min_glucose to graph 1,first $min_glucose ls 6 lw 2 nohead -#set arrow from graph 0,first $max_glucose to graph 1,first $max_glucose ls 6 lw 2 nohead set object 1 rect from graph 0, first $min_glucose to graph 1,first $max_glucose fc ls 6 fs solid 0.2 back AVG = Mean$label @@ -181,11 +388,8 @@ AVG_LABEL = gprintf("Average glucose: %.2f", AVG) set object 2 rect at graph 0.9, graph 0.9 fc ls 2 fs transparent solid 0.5 front size char strlen(AVG_LABEL), char 3 set label 2 AVG_LABEL at graph 0.9, graph 0.9 front center -#plot \$SmoothData$label using 1:2:( \$2 > $max_glucose || \$2 < $min_glucose ? 110 : $count_graphs ) with linespoints ls 120 lc variable plot \$SmoothData$label using (strftime("%H:%M:%S", \$1)):2:( \$2 > $max_glucose || \$2 < $min_glucose ? 110 : 1 ) with lines lw 3 lc variable -undefine \$SmoothData$label - # Add an x grid set multiplot previous set title " " @@ -199,124 +403,20 @@ set grid xtics ls 101 plot 1/0 ); - if ( $count_graphs % $days_per_page == 0 && $count_graphs < $total_graphs ) { + if ( $count_graphs % $graphs_per_page == 0 && $count_graphs < $total_day_graphs ) { push @data, qq(unset multiplot); - push @data, qq(set multiplot layout $days_per_page,1); + push @data, qq(set multiplot layout $graphs_per_page,1); $page_number++; } } +# End daily graph plot -# Output data averages by hour of the day -@sorted_lines = (); +# Plot and display a graph with the average glucose values for every $interval for all recorded days push @data, qq( -\$DataAvg << EOD -"timestamp","blood glucose","meal","method","comment"); -foreach my $row (@lines) { - if ( $row =~ s#^"\d{4}-\d{2}-\d{2} #"#ms ) { - push @sorted_lines, $row; - } -} -push @data, sort @sorted_lines; -push @data, qq(EOD); +unset multiplot -# Output min/max for each time interval -foreach my $row ( @sorted_lines ) { - $row =~ s/"//g; - my ( $time, $value ) = split /,/, $row; - my ( $hour, $minute, $second ) = split /:/, $time; - $time = sprintf( "%02d:%02d:00", $hour, int($minute/$interval)*$interval ); - - # Override the current minimum values for this interval if it - # exists; otherwise, set it - if ( exists ( $intervals{$time}{min} ) ) { - if ( $intervals{$time}{min} < $value ) { - $intervals{$time}{min} = $value; - } - } else { - $intervals{$time}{min} = $value; - } - # Override the current maximum values for this interval if it - # exists; otherwise, set it - if ( exists ( $intervals{$time}{max} ) ) { - if ( $intervals{$time}{max} > $value ) { - $intervals{$time}{max} = $value; - } - } else { - $intervals{$time}{max} = $value; - } - -} -$Data::Dumper::Sortkeys = 1; -#die Dumper(\%intervals); - -push @data, qq( -\$DataMaxMin << EOD -"timestamp","max","min"); -foreach my $time ( sort keys %intervals ) { - warn $time; - push @data, qq("$time","$intervals{$time}{max}","$intervals{$time}{min}"); -} -push @data, qq(EOD); - -# Standardise units for gnuplot's A1C calculations -if ( $units =~ /mg/i ) { - $units = 'mg/dL'; -} elsif ( $units =~ /mmol/i ) { - $units = 'mmol/L'; -} else { - $units = ''; -} - -push @data, qq( -reset -set datafile separator "," - -set timefmt "%H:%M:%S" -set format x "%s" timedate -set format y "%.2f" numeric -set samples 10000 -set xdata -stats \$DataAvg using 2 -MeanTotal = STATS_mean - -set xdata time - -set table \$DataAvgTable - -#avg(x) = g -#min(x) = xg -#f(x) = g -#fit f(x) \$DataAvg using 1:(\$2>MeanTotal?\$2:'') via t, g -#plot f(x) smooth mcsplines - -#plot \$DataAvg using 1:(\$2>MeanTotal?\$2:'') every $count_graphs/2 lc 2, \$DataAvg using 1:(\$2 4 ? 5 : (\$0+1) -#avg5(x) = (shift5(x), (back1+back2+back3+back4+back5)/samples(\$0)) -#shift5(x) = (back5 = back4, back4 = back3, back3 = back2, back2 = back1, back1 = x) -## Initialize a running sum -#init(x) = (back1 = back2 = back3 = back4 = back5 = sum = 0) - -#plot sum = init(0), \$DataAvg using 1:2 title 'data' lw 2 lc rgb 'forest-green', '' using 1:(avg5(\$2)) pt 7 ps 0.5 lw 1 lc rgb "blue" - -plot \$DataAvg using 1:2 smooth mcsplines - -unset table - -set table \$SmoothDataAvg -plot \$DataAvg using 1:2 smooth bezier -unset table - -undefine \$DataAvg - -# Convert DataMaxMin from CSV to table -set table \$DataMaxMinTable -plot \$DataMaxMin using 1:2:3 with table -unset table - -reset +# ensure separator handles tables set datafile separator whitespace set key off @@ -325,10 +425,9 @@ set xdata time set timefmt "%H:%M:%S" set format x "%H:%M" timedate set format y "%.0f" numeric - -set yrange [0:$graph_max] # If extended to 23:59, the x grid overlaps with the border set xrange ["00:00":"23:58"] +set yrange [0:$graph_max] set style line 100 dt 3 lw 1 lc rgb "#202020" set style line 101 dt 1 lw 1 lc rgb "#202020" @@ -341,9 +440,9 @@ set rmargin 10 set tmargin 5 set bmargin 5 -set multiplot title layout $days_per_page,1 +set multiplot title layout $graphs_per_page,1 -set title "Average Daily Glucose" font "Calibri,18" +set title "Overall Average Daily Glucose" font "Calibri,18" set xlabel "Time" offset 0,-0.25 set ylabel "Blood glucose" set xtics left tc rgb "#000000" @@ -377,16 +476,123 @@ A1C_LABEL = gprintf("Average A1c: %.1f", A1C) set object 3 rect at graph 0.07, graph 0.9 fc ls 4 fs transparent solid 0.5 front size char strlen(A1C_LABEL), char 3 set label 3 A1C_LABEL at graph 0.07, graph 0.9 front center -#plot \$SmoothDataAvg using ( strftime("%H:%M:%S", \$1) ):2:( \$2 > $max_glucose || \$2 < $min_glucose ? 110 : 1 ) with lines lw 3 lc variable -#plot \$DataAvgTable using (strftime("%H:%M:%S", \$1)):2 with points lc 5 ps 0.5 pt 37, \$SmoothDataAvg using (strftime("%H:%M:%S", \$1)):2:( \$2 > $max_glucose || \$2 < $min_glucose ? 110 : 1 ) with lines lw 3 lc variable - plot \$DataMaxMinTable using (strftime("%H:%M:%S", \$1)):2:3 with filledcurves lc 111, \$SmoothDataAvg using (strftime("%H:%M:%S", \$1)):2:( \$2 > $max_glucose || \$2 < $min_glucose ? 110 : 1 ) with lines lw 3 lc variable -undefine \$DataAvg -undefine \$DataMaxMin -undefine \$DataMaxMinTable -undefine \$SmoothDataAvg -undefine \$DataAvgTable +# Add an x grid +set multiplot previous +set title " " +set xlabel " " offset 0,-0.25 +set ylabel " " +set xtics tc rgb "#ffffff00" +set ytics tc rgb "#ffffff00" +unset grid +unset object 1 +set grid xtics ls 101 +plot 1/0 +); +# End overall average plot + + + + + +=cut + +foreach my $year ( sort keys %seen_weeks ) { + foreach my $week ( sort keys %{$seen_weeks{$year}} ) { + my $time = Time::Piece->strptime( "$year", "%Y" ); + my $mon = $time + ( ONE_WEEK * ( $week - 1 ) ); + my $sun = $time + ( ONE_WEEK * ( $week - 1 ) ) + ( ONE_DAY * 6 ); + my $label = $mon->strftime("%Y-%m-%d"); + my $title = $mon->strftime("%Y-%m-%d") . " to " . $sun->strftime("%Y-%m-%d"); + push @data, qq( +#reset +set datafile separator "," + +set timefmt "%H:%M:%S" +set format x "%s" timedate +set format y "%.2f" numeric +set samples 10000 +set xdata +stats \$DataAvg using 2 +MeanTotal = STATS_mean + +set xdata time + +set table \$DataAvgTable +plot \$DataAvg using 1:2 smooth mcsplines +unset table + +set table \$SmoothDataAvg +plot \$DataAvg using 1:2 smooth bezier +unset table + +# Convert DataMaxMin from CSV to table +set table \$DataMaxMinTable +plot \$DataMaxMin using 1:2:3 with table +unset table + +reset +set datafile separator whitespace + +set key off +set style data lines +set xdata time +set timefmt "%H:%M:%S" +set format x "%H:%M" timedate +set format y "%.0f" numeric +# If extended to 23:59, the x grid overlaps with the border +set xrange ["00:00":"23:58"] +set yrange [0:$graph_max] + +set style line 100 dt 3 lw 1 lc rgb "#202020" +set style line 101 dt 1 lw 1 lc rgb "#202020" +set linetype 110 lc rgb "red" +set linetype 111 lc rgb "#B0B0B0" +set style fill transparent solid 0.5 noborder + +set lmargin 12 +set rmargin 10 +set tmargin 5 +set bmargin 5 + +set multiplot title layout $graphs_per_page,1 + +set title "Average Daily Glucose from $title" font "Calibri,18" +set xlabel "Time" offset 0,-0.25 +set ylabel "Blood glucose" +set xtics left tc rgb "#000000" +set ytics 2 tc rgb "#000000" +set grid ytics ls 100 front + +set object 1 rect from graph 0, first $min_glucose to graph 1,first $max_glucose fc ls 6 fs solid 0.05 back + +AVG = MeanTotal +AVG_LABEL = gprintf("Average glucose: %.2f", AVG) +set object 2 rect at graph 0.9, graph 0.9 fc ls 2 fs transparent solid 0.5 front size char strlen(AVG_LABEL), char 3 +set label 2 AVG_LABEL at graph 0.9, graph 0.9 front center + +A1C = 0 +if (A1C == 0 && '$units' eq 'mg/dL') { + A1C = (MeanTotal + 46.7) / 28.7 +} +if (A1C == 0 && '$units' eq 'mmol/L') { + A1C = (MeanTotal + 2.59) / 1.59 +} +# mg/dL numbers tend to be higher than 35 +if (A1C == 0 && MeanTotal >= 35) { + A1C = (MeanTotal + 46.7) / 28.7 +} +# mmol/L numbers tend to be lower than 35 +if (A1C == 0 && MeanTotal < 35) { + A1C = (MeanTotal + 2.59) / 1.59 +} + +A1C_LABEL = gprintf("Average A1c: %.1f", A1C) +set object 3 rect at graph 0.07, graph 0.9 fc ls 4 fs transparent solid 0.5 front size char strlen(A1C_LABEL), char 3 +set label 3 A1C_LABEL at graph 0.07, graph 0.9 front center + +plot \$DataMaxMinTable using (strftime("%H:%M:%S", \$1)):2:3 with filledcurves lc 111, \$SmoothDataAvg using (strftime("%H:%M:%S", \$1)):2:( \$2 > $max_glucose || \$2 < $min_glucose ? 110 : 1 ) with lines lw 3 lc variable # Add an x grid set multiplot previous @@ -401,12 +607,36 @@ set grid xtics ls 101 plot 1/0 ); + if ( $count_graphs % $graphs_per_page == 0 && $count_graphs < $total_day_graphs ) { + push @data, qq(unset multiplot); + push @data, qq(set multiplot layout $graphs_per_page,1); + $page_number++; + } + } +} +=cut push @data, qq( unset multiplot test ); +# Cleanup stored variables +foreach my $d ( sort keys %seen_days ) { + my $label = "$1$2$3" if ( $d =~ m#(\d{4})-(\d{2})-(\d{2})# ); + push @data, qq(undefine \$Data$label); + push @data, qq(undefine \$SmoothData$label); +} + +push @data, qq( +undefine \$DataAvg +undefine \$DataAvgTable +undefine \$SmoothDataAvg +undefine \$DataMaxMin +undefine \$DataMaxMinTable +); + + # run the data through gnuplot $gnuplot_data = join "\n", @data; print $gnuplot_data; @@ -444,4 +674,5 @@ close( $ofh ) #print GNUPLOT $gnuplot_data; #close(GNUPLOT); + # vim : set expandtab shiftwidth=4 softtabstop=4 tw=1000 :