1
# plotmaker.rb: the main class for ctioga
2
# copyright (c) 2006, 2007, 2008, 2009, 2010 by Vincent Fourmond
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details (in the COPYING file).
17
# It currently is a pain to make complex plots with ctioga. A real
18
# pain. What could be done to improve the situation ?
20
# * hide the difference between edges and axes.
21
# * the layout mechanism is not comfortable enough to work with, especially
22
# with the need for relative positioning.
24
# Would it be possible to allow for the 'real size' to be determined
25
# *afterwards* ??? Difficult !
27
# TODO, an even bigger one:
28
# Switch to a real command-based plotting program:
29
# - any single operation that is realized by ctioga would be a command
30
# - every single of these commands would take a given (fixed) number of
31
# parameters (we should take care about boolean stuff)
32
# - every command would be of course reachable as command-line options
33
# but it could also be within files
34
# - in these files, provide an additional mechanism for quickly defining
35
# variables and do variable substitution.
36
# - one command (plus arguments) per line, with provisions for
38
# - allow some kind of 'include' directives (that would also be used for
39
# cmdline inclusion of files)
40
# - command-line arguments and command files could intermix (that *would*
41
# be fun, since it would allow very little changes to a command-line
42
# to change significantly the look of a file...!)
43
# - LONG TERM: allow conditionals and variable
44
# definition/substitution on command-line ?
45
# - Use typed variables, converted into string when substitution occurs,
46
# but manipulable as *typed* before ?? proposed syntax:
47
# type: variable = contents ?
49
# Each command could take *typed* arguments. That would allow typed
50
# variables along with a string-to-type conversion ? (is that useful
51
# ?) NO. Commands take String. And that is fine...
53
# Provide *optional* hash-like arguments that probably could not be
54
# used in the command-line, but could be in the file.
56
# Provide self-documentation in each and every command
58
# Manipulations of a buffer stack - including mathematical
59
# expressions; provide commands to only *load* a file, but not
60
# necessarily draw it.
62
# Provide a way to 'save' a command-line into a command-file.
64
# Write as many test suites as possible ??
66
# Merge Metabuilder and Backends into the ctioga code base. There's
67
# no need for extra complexity.
69
# That requires a huge amount of work, but on the other hand, that
70
# would be much more satisfactory than the current mess.
72
# Commands would be part of "groups".
74
# Release a new version of ctioga before that.
76
# Don't rely on huge mess of things !
80
# * write a :point type that would parse figure/frame/page coordinates + maybe
81
# arbitrary additions ?
82
# * drop the layout system, but instead write a simple plotting system:
83
# - start the image as a figure
84
# - start a subplot in the full figure if nothing was specified before the
86
# - start subplots manually using --inset or things of this spirit
87
# - maybe, for the case when subplots were manually specified, resize
88
# the graph so it fits ? (difficult, especially if the positions/sizes
89
# are relative... but trivial if that isn't the case. Maybe provide
90
# a autoresize function for that ? Or do it automatically if all the
91
# toplevel (sub)plot positions are absolute ?)
93
# This scheme would allow for a relatively painless way to draw graphs...
99
# \todo make --xrange automatically select the range for the --math
100
# backend unless another range was explicitly specified.
102
require 'ctioga2/utils'
103
require 'ctioga2/log'
107
# Maybe, maybe, maybe... We need tioga ?
108
require 'Tioga/FigureMaker'
111
# Command interpreter
112
require 'ctioga2/commands/interpreter'
113
# Various global scope commands:
114
require 'ctioga2/commands/general-commands'
116
require 'ctioga2/commands/doc/introspection'
117
require 'ctioga2/commands/doc/documentation-commands'
121
require 'ctioga2/data/dataset'
122
require 'ctioga2/data/stack'
123
require 'ctioga2/data/backends/factory'
127
require 'ctioga2/graphics/root'
128
require 'ctioga2/graphics/styles'
129
require 'ctioga2/graphics/generator'
133
require 'ctioga2/postprocess'
136
## \mainpage CTioga2's code documentation.
137
# This module contains all the classes used by ctioga
140
# This holds the main page for CTioga2 code documentation. Most
141
# interesting classes/namespaces are:
143
# * CTioga2::PlotMaker
144
# * CTioga2::Graphics
145
# * CTioga2::Commands
148
# Have fun hacking...
151
Version::register_svn_info('$Revision: 244 $', '$Date: 2011-01-23 23:36:02 +0100 (Sun, 23 Jan 2011) $')
153
# This class is the core of ctioga. It parses the command-line arguments,
154
# reads all necessary files and plots graphs. Most of its functionality
155
# is delegated into classes.
157
# \todo An important point would be to provide a facility that holds
158
# all the default values. To each would be assigned a given name,
159
# and programs would only use something like
161
# value = Default::value('stuff')
164
# Setting up defaults would only be a question of using one single
165
# command (with admittedly many optional arguments)
168
# Include logging facilities for ctioga2
171
# The Commands::Interpreter object which runs all the commands.
172
attr_accessor :interpreter
174
# The Data::DataStack object that manipulates Dataset objects
175
attr_accessor :data_stack
177
# The Graphics::RootObject in charge of holding all things that
178
# will eventually get drawn
179
attr_accessor :root_object
181
# A Graphics::CurveGenerator object in charge of producing
182
# suitable elements to be added to the Graphics::RootObject
183
attr_accessor :curve_generator
185
# Below are simple plot attributes. Maybe they should be in their
188
# The name of the figure
189
attr_accessor :figure_name
191
# The output directory
192
attr_accessor :output_directory
194
# Additional preamble for LaTeX output
195
attr_accessor :latex_preamble
197
# What happens to generated PDF files (a PostProcess object)
198
attr_accessor :postprocess
200
# Whether or not to include the command-line used to produce the
201
# file in the target PDF file.
204
# Whether intermediate files are cleaned up automatically
205
# afterwards or not...
206
attr_accessor :cleanup
208
# The stack of CurveStyle objects that were used so far.
209
attr_accessor :curve_style_stack
212
# The first instance of PlotMaker created
213
@@first_plotmaker_instance = nil
215
# Returns the first created instance of PlotMaker. This sounds
216
# less object-oriented, yes, but that can come in useful some
219
return @@first_plotmaker_instance
223
# Setting up of the PlotMaker object
225
CTioga2::Log::init_logger
226
@data_stack = Data::DataStack.new
227
@root_object = Graphics::RootObject.new
228
@interpreter = Commands::Interpreter.new(self)
229
@curve_generator = Graphics::CurveGenerator.new
237
@postprocess = PostProcess.new
239
# Make sure it is registered
240
@@first_plotmaker_instance ||= self
242
# We mark by default, as it comes dead useful.
245
# Remove intermediate files by default.
248
# Make curve style stack empty
249
@curve_style_stack = []
252
# ctioga's entry point.
253
def run(command_line)
255
# The main catch-all around the plot:
257
@command_line = command_line.dup
258
if ENV.key? 'CTIOGA2_PRE'
259
command_line.unshift(*Shellwords.shellwords(ENV['CTIOGA2_PRE']))
262
if ENV.key? 'CTIOGA2_POST'
263
command_line.push(*Shellwords.shellwords(ENV['CTIOGA2_POST']))
266
@interpreter.run_command_line(command_line)
268
# Now, draw the main figure
269
file = draw_figure(@figure_name || "Plot-%03d", true)
270
rescue SystemExit => e
271
# We special-case the exit exception ;-)...
272
rescue Exception => e
273
debug { format_exception(e) }
274
fatal { "#{e.message}" }
278
# Flushes the current root object and starts a new one:
280
draw_figure(@figure_name || "Plot-%03d", true)
282
@root_object = Graphics::RootObject.new
283
@curve_generator = Graphics::CurveGenerator.new
286
# Returns a quoted version of the command line, that possibly
287
# could be used again to reproduce the same results.
288
def quoted_command_line
289
quoted_args = @command_line.collect do |s|
290
Utils::shell_quote_string(s)
293
return "#{File.basename($0)} #{quoted_args}"
296
# Draws the figure currently accumulated in the #root_object. It
297
# returns the path of the PDF file produced.
299
# If _figname_ contains a % sign, it will be interpreted as a
300
# format, and ctioga will attempt to find the first numbered file
301
# that does not exists.
305
def draw_figure(figname = "Plot-%03d", last = false)
306
return if @root_object.empty?
317
if File::exist?("#{f}.pdf")
327
info { "Producing figure '#{figname}'" }
329
t = create_figure_maker
330
# If figname is clearly a path, we split it into directory/name
331
# and set the output directory to directory.
332
if File::basename(figname) != figname
333
dir = File::dirname(figname)
334
# If path is relative and output_directory is specified, we make
335
# the path relative to output_dir
336
if @output_directory && dir =~ /^[^\/~]/
337
dir = File::join(@output_directory, dir)
340
figname = File::basename(figname)
341
elsif @output_directory
342
t.save_dir = @output_directory
345
t.def_figure(figname) do
346
@root_object.draw_root_object(t)
348
t.make_preview_pdf(t.figure_index(figname))
350
file = t.save_dir ? File::join(t.save_dir, figname + ".pdf") :
354
@postprocess.process_file(file, last)
358
# Add *one* Data::Dataset object using the current style (that can
359
# be overridden by stuff given as options) to the #root_object.
361
# \todo here, keep a state of the current styles:
362
# * which is the color/marker/filling and so on of the curve ?
363
# * are we drawing plain 2D curve, a histogram or something
365
# * this should be a separated class.
367
# \todo all curve objects should only take a Data::Dataset and a
368
# style as arguments to new.
369
def add_curve(dataset, options = {})
370
plot = @root_object.current_plot
371
curve = @curve_generator.
372
curve_from_dataset(plot, dataset, options)
373
plot.add_element(curve)
374
@curve_style_stack << curve.curve_style
375
info { "Adding curve '#{dataset.name}' to the current plot" }
378
# Transforms a _dataset_spec_ into one or several Data::Dataset
379
# using the current backend (or any other that might be specified
380
# in the options), and add them as curves to the #root_object,
382
def add_curves(dataset_spec, options = {})
384
sets = @data_stack.get_datasets(dataset_spec, options)
385
rescue Exception => exception
386
error { "A problem occurred while processing dataset '#{dataset_spec}' using backend #{@data_stack.backend_factory.current.description.name}. Ignoring it." }
387
debug { format_exception(exception) }
391
# We first trim elements from options that are not inside
392
# Graphics::Styles::CurveStyleFactory::PlotCommandOptions
393
options.delete_if { |k,v|
395
CurveStyleFactory::PlotCommandOptions.key?(k)
397
add_curve(set, options)
403
# Creates a new FigureMaker object and returns it
404
def create_figure_maker
405
t = Tioga::FigureMaker.new
406
t.tex_preamble += @latex_preamble
407
t.autocleanup = @cleanup
409
# The title field of the information is the command-line if marking
412
title = "/Title (#{Utils::pdftex_quote_string(quoted_command_line)})\n"
417
# We use Vincent's algorithm for major ticks when available ;-)...
419
t.vincent_or_bill = true
420
info { "Using Vincent's algorithm for major ticks" }
422
info { "Using Bill's algorithm for major ticks" }
426
# We now use \pdfinfo to provide information about the version
427
# of ctioga2 used to produce the PDF, and the command-line if
430
"\n\\pdfinfo {\n#{title}/Creator(#{Utils::pdftex_quote_string("ctioga2 #{Version::version}")})\n}\n"
435
PlotGroup = CmdGroup.new('plots', "Plots","Plots", 0)
438
Graphics::Styles::CurveStyleFactory::PlotCommandOptions.dup
441
PlotOptions.merge!(Data::LoadDatasetOptions) do |key, oldval, newval|
442
raise "Duplicated option between PlotCommandOptions and LoadDatasetOptions"
446
Cmd.new("plot",nil,"--plot",
447
[ CmdArg.new('dataset') ],
448
PlotOptions ) do |plotmaker, set, options|
449
plotmaker.add_curves(set, options)
452
PlotCommand.describe("Plots the given datasets",
454
Use the current backend to load the given datasets onto the data stack
455
and plot them. It is a combination of the {command: load} and the
456
{command: plot-last} commands; you might want to see their
461
Graphics::Styles::CurveStyleFactory::PlotCommandOptions.dup
463
PlotLastOptions['which'] = CmdArg.new('stored-dataset')
466
Cmd.new("plot-last",'-p',"--plot-last",
467
[], PlotLastOptions) do |plotmaker, options|
468
ds = plotmaker.data_stack.specified_dataset(options)
469
options.delete('which') # To avoid problems with extra options.
470
plotmaker.add_curve(ds, options)
473
PlotLastCommand.describe("Plots the last dataset pushed onto the stack",
475
Plots the last dataset pushed onto the data stack (or the one
476
specified with the @which@ option), with the current style. All
477
aspects of the curve style (colors, markers, line styles...) can be
478
overridden through the use of options.
481
LaTeXGroup = CmdGroup.new('latex', "LaTeX",<<EOD, 30)
482
Commands providing control over the LaTeX output (preamble,
487
Cmd.new("use",nil,"--use",
488
[ CmdArg.new('text') ],
489
{ 'arguments' => CmdArg.new('text')}
490
) do |plotmaker, package, options|
491
if options['arguments']
492
plotmaker.latex_preamble <<
493
"\\usepackage[#{options['arguments']}]{#{package}}\n"
495
plotmaker.latex_preamble << "\\usepackage{#{package}}\n"
499
UsePackageCommand.describe('Includes a LaTeX package',
501
Adds a command to include the LaTeX package into the preamble. The
502
arguments, if given, are given within [square backets].
506
Cmd.new("preamble",nil,"--preamble",
507
[ CmdArg.new('text') ]) do |plotmaker, txt|
508
plotmaker.latex_preamble << "#{txt}\n"
511
PreambleCommand.describe('Adds a string to the LaTeX preamble',
513
Adds the given string to the LaTeX preamble of the output.
517
Cmd.new("utf8",nil,"--utf8", []) do |plotmaker|
518
plotmaker.latex_preamble <<
519
"\\usepackage[utf8]{inputenc}\n\\usepackage[T1]{fontenc}"
522
Utf8Command.describe('Uses UTF-8 in strings',
524
Makes ctioga2 use UTF-8 for all text. It is exactly equivalent to
525
the command {command: preamble} with the argument:
527
@ \\usepackage[utf8]{inputenc}\\usepackage[T1]{fontenc}
534
CmdGroup.new('output-setup',
535
"Output setup", <<EOD, 50)
536
Commands in this group deal with various aspects of the production of
538
* output file location
539
* post-processing (including automatic display)
544
Cmd.new("page-size",'-r',"--page-size",
545
[ CmdArg.new('text') ], # \todo change that !
546
{ 'count-legend' => CmdArg.new('boolean')}
547
) do |plotmaker, size, options|
548
plotmaker.root_object.set_page_size(size)
549
if options.key? 'count-legend'
550
plotmaker.root_object.count_legend_in_page =
551
options['count-legend']
555
PageSizeCommand.describe('Sets the page size',
556
<<EOH, OutputSetupGroup)
557
Sets the size of the output PDF file, in real units. Takes arguments in the
558
form of 12cm x 3in (spaces can be omitted).
562
Cmd.new("clean",nil,"--clean",
563
[ CmdArg.new('boolean') ]) do |plotmaker, cleanup|
564
plotmaker.cleanup = cleanup
568
CleanupCommand.describe('Remove intermediate files',
569
<<EOH, OutputSetupGroup)
570
When this is on (the default), ctioga2 automatically cleans up
571
intermediate files produced by Tioga. When LaTeX fails, it can be
572
useful to have a closer look at them, so disable it to be able to look
578
Cmd.new("name",'-n',"--name",
579
[ CmdArg.new('text', 'figure name') ]) do |plotmaker, name|
580
plotmaker.figure_name = name
584
NameCommand.describe('Sets the name of the figure',
585
<<EOH, OutputSetupGroup)
586
Sets the name of the figure, which is also the base name for the output file.
587
This has nothing to do with the title of the plot, which can be set using
588
the command {command: title}.
590
If the name contains a %, it is interpreted by ctioga2 as a
591
printf-like format. It will attempt to find the first file that does
592
not exist, feeding it with increasing numbers.
594
The default value is now Plot-%03d, which means you'll get increasing numbers
599
Cmd.new("output-now",'-o',"--output",
600
[ CmdArg.new('text', 'figure name') ]) do |plotmaker, name|
601
plotmaker.draw_figure(name)
604
OutputNowCommand.describe('Outputs the current state of the figure',
605
<<EOH, OutputSetupGroup)
606
Writes a figure with the given name (see {command: name}) and keeps the
607
current state. This can be used to create an animation.
610
OutputAndResetCommand =
611
Cmd.new("output-and-reset",nil,"--output-and-reset",
613
plotmaker.reset_graphics
616
OutputAndResetCommand.describe('Writes the current figure and starts anew',
617
<<EOH, OutputSetupGroup)
618
Writes the current figure and starts a fresh one. All non-graphical
619
information are kept (curves loaded, figure names, preamble, and so on).
623
Cmd.new("output-directory",'-O',"--output-directory",
624
[ CmdArg.new('text') ]) do |plotmaker, dir|
625
plotmaker.output_directory = dir
628
OutputDirCommand.describe('Sets the output directory for produced files',
629
<<EOH, OutputSetupGroup)
630
Sets the directory to which files will be plot. It defaults to the current
635
# These commands belong rather to the PostProcess file, but, well,
636
# they don't do much harm here anyway...
640
Cmd.new("viewer",nil,"--viewer",
641
[ CmdArg.new('text') ]) do |plotmaker, viewer|
642
plotmaker.postprocess.viewer = viewer
645
ViewerCommand.describe('Uses the given viewer to view the produced PDF files',
646
<<EOH, OutputSetupGroup)
647
Sets the command for viewing the PDF file after ctioga2 has been run.
651
Cmd.new("xpdf",'-X',"--xpdf", [ ]) do |plotmaker|
652
plotmaker.postprocess.viewer = "xpdf -z page"
655
XpdfViewerCommand.describe('Uses xpdf to view the produced PDF files',
656
<<EOH, OutputSetupGroup)
657
Uses xpdf to view the PDF files produced by ctioga2.
661
Cmd.new("open",nil,"--open", [ ]) do |plotmaker|
662
plotmaker.postprocess.viewer = "open"
665
OpenViewerCommand.describe('Uses open to view the produced PDF files',
666
<<EOH, OutputSetupGroup)
667
Uses open (available on MacOS) to view the PDF files produced by ctioga2.
671
Cmd.new("svg",nil,"--svg",
672
[CmdArg.new('boolean') ]) do |plotmaker,val|
673
plotmaker.postprocess.svg = val
676
SVGCommand.describe('Converts produced PDF to SVG using pdf2svg',
677
<<EOH, OutputSetupGroup)
678
When this feature is on, all produced PDF files are converted to SVG
679
using the neat pdf2svg program.
683
Cmd.new("eps",nil,"--eps",
684
[CmdArg.new('boolean') ]) do |plotmaker,val|
685
plotmaker.postprocess.eps = val
688
EPSCommand.describe('Converts produced PDF to EPS using pdftops',
689
<<EOH, OutputSetupGroup)
690
When this feature is on, all produced PDF files are converted to EPS
691
using the pdftops program (from the xpdf tools suite).
695
Cmd.new("png",nil,"--png",
696
[CmdArg.new('text', 'resolution') ],
698
'oversampling' => CmdArg.new('float'),
699
'scale' => CmdArg.new('float'),
700
}) do |plotmaker,res, opts|
701
if res =~ /^\s*(\d+)\s*x\s*(\d+)\s*$/
702
size = [$1.to_i, $2.to_i]
703
plotmaker.postprocess.png_res = size
704
if opts['oversampling']
705
plotmaker.postprocess.png_oversampling = opts['oversampling']
707
scale = opts['scale'] || 1
708
plotmaker.postprocess.png_scale = scale
709
page_size = size.map { |n| (n/(1.0 *scale)).to_s + "bp" }.join('x')
710
plotmaker.root_object.set_page_size(page_size)
712
raise "Invalid resolution for PNG output: #{res}"
716
PNGCommand.describe('Converts produced PDF to PNG using convert',
717
<<EOH, OutputSetupGroup)
718
Turns all produced PDF files into PNG images of the given resolution
719
using convert. This also has for effect to set the {command:
720
page-size} to the resolution divided by the 'scale' option in
721
Postscript points. By default, 2 pixels are rendered for 1 final to
722
produce a nicely antialiased image. Use the 'oversampling' option to
723
change that, in case the output looks too pixelized. This option only
724
affects conversion time.
728
Cmd.new("mark",nil,"--mark",
729
[CmdArg.new('boolean') ]) do |plotmaker,val|
733
MarkCommand.describe('Fills the title of the produced PDF with the command-line',
734
<<EOH, OutputSetupGroup)
735
When this feature is on (which is the default, as it comes in very
736
useful), the 'title' field of the PDF informations is set to the
737
command-line that resulted in the PDF file. Disable it if you don't
738
want any information to leak.
740
Please note that this will not log the values of the CTIOGA2_PRE and
741
CTIOGA2_POST variables, so you might still get a different output if
742
you make heavy use of those.