#!/usr/local/bin/webwork-perl =head1 NAME PGgraphmacros -- in courseScripts directory =head1 SYNPOSIS # use Fun; # use Label; # use Circle; # use WWPlot; =head1 DESCRIPTION This collection of macros provides easy access to the facilities provided by the graph module WWPlot and the modules for objects which can be drawn on a graph: functions (Fun.pm) labels (Label.pm) and images. The only image implemented currently are open and closed circles (Circle) which can be used to mark graphs of functions defined on open and closed intervals. These macros provide an easy ability to graph simple functions. More complicated projects may require direct access to the underlying modules. If these complicated projects are common then it may be desirable to create additional macros. (See numericalmacros.pl for one example.) =cut =head2 Other constructs See F for definitions of C and C =cut #my $User = $main::studentLogin; #my $psvn = $main::psvnNumber; #$main::in{'probSetKey'}; #in{'probSetNumber'}; #$main::probSetNumber; #my $setNumber = $main::setNumber; #my $probNum = $main::probNum; ######################################################### # this initializes a graph object ######################################################### # graphObject = init_graph(xmin,ymin,xmax,ymax,options) # options include 'grid' =>[8,8] or # 'ticks'=>[8,8] and/or # 'axes' ######################################################### my %images_created = (); # this keeps track of the base names of the images created during this session. # We tack on # $imageNum = ++$images_created{$imageName} to keep from overwriting files # when we don't want to. =head2 init_graph =pod $graphObject = init_graph(xmin,ymin,xmax,ymax,'ticks'=>[4,4],'axes'=>[0,0]) options are 'grid' =>[8,8] or # there are 8 evenly spaced lines intersecting the horizontal axis 'ticks'=>[8,8] and/or # there are 8 ticks on the horizontal axis, 8 on the vertical 'axes' => [0,0] # axes pass through the point (0,0) in real coordinates 'size' => [200,200] # dimensions of the graph in pixels. 'pixels' =>[200,200] # synonym for size Creates a graph object with the default size 200 by 200 pixels. If you want axes or grids you need to specify them in options. But the default values can be selected for you. =cut BEGIN { be_strict(); } sub _PGgraphmacros_init { } #sub _PGgraphmacros_export { # # my @EXPORT = ( # '&init_graph', '&add_functions', '&plot_functions', '&open_circle', # '&closed_circle', '&my_math_constants', '&string_to_sub', # ); # @EXPORT; #} sub init_graph { my ($xmin,$ymin,$xmax,$ymax,%options) = @_; my @size; if ( defined($options{'size'}) ) { @size = @{$options{'size'}}; } elsif ( defined($options{'pixels'}) ) { @size = @{$options{'pixels'}}; } else { @size=(200,200); } my $graphRef = new WWPlot(@size); # select a name for this graph based on the user, the psvn and the problem my $imageName = "$main::studentLogin-$main::psvnNumber-set${main::setNumber}prob${main::probNum}"; # $imageNum counts the number of graphs with this name which have been created since PGgraphmacros.pl was initiated. my $imageNum = ++$main::images_created{$imageName}; # this provides a unique name for the graph -- it does not include an extension. $graphRef->imageName("${imageName}image${imageNum}"); $graphRef->xmin($xmin) if defined($xmin); $graphRef->xmax($xmax) if defined($xmax); $graphRef->ymin($ymin) if defined($ymin); $graphRef->ymax($ymax) if defined($ymax); my $x_delta = ($graphRef->xmax - $graphRef->xmin)/8; my $y_delta = ($graphRef->ymax - $graphRef->ymin)/8; if (defined($options{grid})) { # draw grid my $xdiv = ( ${$options{'grid'}}[0]) ? ${$options{'grid'}}[0] : 8; # number of ticks (8 is default) my $ydiv = ( ${$options{'grid'}}[1] ) ? ${$options{'grid'}}[1] : 8; my $x_delta = ($graphRef->xmax - $graphRef->xmin)/$xdiv; my $y_delta = ($graphRef->ymax - $graphRef->ymin)/$ydiv; my $i; my @x_values=(); my @y_values=(); foreach $i (1..($xdiv-1) ) { push( @x_values, $i*$x_delta+$graphRef->{xmin}); } foreach $i (1..($ydiv-1) ) { push( @y_values, $i*$y_delta+$graphRef->{ymin}); } $graphRef->v_grid('gray',@x_values); $graphRef->h_grid('gray',@y_values); $graphRef->lb(new Label($x_delta,0,sprintf("%1.1f",$x_delta),'black','center','middle')); $graphRef->lb(new Label(0,$y_delta,sprintf("%1.1f",$y_delta),'black','center','middle')); $graphRef->lb(new Label($xmax,0,$xmax,'black','right')); $graphRef->lb(new Label($xmin,0,$xmin,'black','left')); $graphRef->lb(new Label(0,$ymax,$ymax,'black','top')); $graphRef->lb(new Label(0,$ymin,$ymin,'black','bottom','right')); } elsif ($options{ticks}) { # draw ticks -- grid over rides ticks my $xdiv = ${$options{ticks}}[0]? ${$options{ticks}}[0] : 8; # number of ticks (8 is default) my $ydiv = ${$options{ticks}}[1]? ${$options{ticks}}[1] : 8; my $x_delta = ($graphRef->xmax - $graphRef->xmin)/$xdiv; my $y_delta = ($graphRef->ymax - $graphRef->ymin)/$ydiv; my $i; my @x_values=(); my @y_values=(); foreach $i (1..($xdiv-1) ) { push( @x_values, $i*$x_delta+$graphRef->{xmin}); } foreach $i (1..($ydiv-1) ) { push( @y_values, $i*$y_delta+$graphRef->{ymin}); } $graphRef->v_ticks(0,'black',@x_values); $graphRef->h_ticks(0,'black',@y_values); $graphRef->lb(new Label($x_delta,0,$x_delta,'black','right')); $graphRef->lb(new Label(0,$y_delta,$y_delta,'black','top')); $graphRef->lb(new Label($xmax,0,$xmax,'black','right')); $graphRef->lb(new Label($xmin,0,$xmin,'black','left')); $graphRef->lb(new Label(0,$ymax,$ymax,'black','top')); $graphRef->lb(new Label(0,$ymin,$ymin,'black','bottom','right')); } if ($options{axes}) { # draw axis my $ra_axes = $options{axes}; $graphRef->h_axis($ra_axes->[1],'black'); $graphRef->v_axis($ra_axes->[0],'black'); } $graphRef; } sub init_graph_no_labels { my ($xmin,$ymin,$xmax,$ymax,%options) = @_; my @size; if ( defined($options{'size'}) ) { @size = @{$options{'size'}}; } elsif ( defined($options{'pixels'}) ) { @size = @{$options{'pixels'}}; } else { @size=(200,200); } my $graphRef = new WWPlot(@size); # select a name for this graph based on the user, the psvn and the problem my $imageName = "$main::studentLogin-$main::psvnNumber-set${main::setNumber}prob${main::probNum}"; # $imageNum counts the number of graphs with this name which have been created since PGgraphmacros.pl was initiated. my $imageNum = ++$main::images_created{$imageName}; # this provides a unique name for the graph -- it does not include an extension. $graphRef->imageName("${imageName}image${imageNum}"); $graphRef->xmin($xmin) if defined($xmin); $graphRef->xmax($xmax) if defined($xmax); $graphRef->ymin($ymin) if defined($ymin); $graphRef->ymax($ymax) if defined($ymax); my $x_delta = ($graphRef->xmax - $graphRef->xmin)/8; my $y_delta = ($graphRef->ymax - $graphRef->ymin)/8; if (defined($options{grid})) { # draw grid my $xdiv = ( ${$options{'grid'}}[0]) ? ${$options{'grid'}}[0] : 8; # number of ticks (8 is default) my $ydiv = ( ${$options{'grid'}}[1] ) ? ${$options{'grid'}}[1] : 8; my $x_delta = ($graphRef->xmax - $graphRef->xmin)/$xdiv; my $y_delta = ($graphRef->ymax - $graphRef->ymin)/$ydiv; my $i; my @x_values=(); my @y_values=(); foreach $i (1..($xdiv-1) ) { push( @x_values, $i*$x_delta+$graphRef->{xmin}); } foreach $i (1..($ydiv-1) ) { push( @y_values, $i*$y_delta+$graphRef->{ymin}); } $graphRef->v_grid('gray',@x_values); $graphRef->h_grid('gray',@y_values); #$graphRef->lb(new Label($x_delta,0,sprintf("%1.1f",$x_delta),'black','center','top')); #$graphRef->lb(new Label($x_delta,0,"|",'black','center','middle')); #$graphRef->lb(new Label(0,$y_delta,sprintf("%1.1f ",$y_delta),'black','right','middle')); #$graphRef->lb(new Label(0,$y_delta,"-",'black','center','middle')); $graphRef->lb(new Label($xmax,0,$xmax,'black','right')); $graphRef->lb(new Label($xmin,0,$xmin,'black','left')); $graphRef->lb(new Label(0,$ymax,$ymax,'black','top','right')); $graphRef->lb(new Label(0,$ymin,$ymin,'black','bottom','right')); } elsif ($options{ticks}) { # draw ticks -- grid over rides ticks my $xdiv = ${$options{ticks}}[0]? ${$options{ticks}}[0] : 8; # number of ticks (8 is default) my $ydiv = ${$options{ticks}}[1]? ${$options{ticks}}[1] : 8; my $x_delta = ($graphRef->xmax - $graphRef->xmin)/$xdiv; my $y_delta = ($graphRef->ymax - $graphRef->ymin)/$ydiv; my $i; my @x_values=(); my @y_values=(); foreach $i (1..($xdiv-1) ) { push( @x_values, $i*$x_delta+$graphRef->{xmin}); } foreach $i (1..($ydiv-1) ) { push( @y_values, $i*$y_delta+$graphRef->{ymin}); } $graphRef->v_ticks(0,'black',@x_values); $graphRef->h_ticks(0,'black',@y_values); $graphRef->lb(new Label($x_delta,0,$x_delta,'black','right')); $graphRef->lb(new Label(0,$y_delta,$y_delta,'black','top')); $graphRef->lb(new Label($xmax,0,$xmax,'black','right')); $graphRef->lb(new Label($xmin,0,$xmin,'black','left')); $graphRef->lb(new Label(0,$ymax,$ymax,'black','top')); $graphRef->lb(new Label(0,$ymin,$ymin,'black','bottom','right')); } if ($options{axes}) { # draw axis my $ra_axes = $options{axes}; $graphRef->h_axis($ra_axes->[1],'black'); $graphRef->v_axis($ra_axes->[0],'black'); } $graphRef; } =head2 plot_functions =pod Usage: ($f1, $f2, $f3) = plot_functions($graph, $f1, $f2, $f3); Synonym: add_functions($graph,$f1,$f2,$f3); Where $f1 is a string of the form $f1 = qq! x^2 - 3*x + 45 for x in [0, 45) using color:red and weight:2! The phrase translates as: formula B variable B interval B option-list. The option-list contains pairs of the form attribute:value. The default for color is "default_color" which is usually black. The default for the weight (pixel width) of the pen is 2 pixels. The string_to_sub subroutine is used to translate the formula into a subroutine. The functions in the list are installed in the graph object $graph and will appear when the graph object is next drawn. =cut sub add_functions { &plot_functions; } sub plot_functions { my $graph = shift; my @function_list = @_; my $error = ""; $error .= "The first argument to plot_functions must be a graph object" unless ref($graph) =~/WWPlot/; my $fn; my @functions=(); foreach $fn (@function_list) { # model: "2.5-x^2 for x in <-1,0> using color:red and weight:2" if ($fn =~ /^(.+)for\s*(\w+)\s*in\s*([\(\[\<])\s*([\d\.\-]+)\s*,\s*([\d\.\-]+)\s*([\)\]\>])\s*using\s*(.*)$/ ) { my ($rule,$var, $left_br, $left_end, $right_end, $right_br, $options)= ($1, $2, $3, $4, $5, $6, $7); my %options = split( /\s*and\s*|\s*:\s*|\s*,\s*|\s*=\s*|\s+/,$options); my ($color, $weight); if ( defined($options{'color'}) ){ $color = $options{'color'}; #set pen color } else { $color = 'default_color'; } if ( defined($options{'weight'}) ) { $weight = $options{'weight'}; # set pen weight (width in pixels) } else { $weight =2; } my $subRef = string_to_sub($rule,$var); my $funRef = new Fun($subRef,$graph); $funRef->color($color); $funRef->weight($weight); $funRef->domain($left_end , $right_end); push(@functions,$funRef); # place open (1,3) or closed (1,3) circle at the endpoints or do nothing <1,3> if ($left_br eq '[' ) { $graph->stamps(closed_circle($left_end,&$subRef($left_end),$color) ); } elsif ($left_br eq '(' ) { $graph->stamps(open_circle($left_end, &$subRef($left_end), $color) ); } if ($right_br eq ']' ) { $graph->stamps(closed_circle($right_end,&$subRef($right_end),$color) ); } elsif ($right_br eq ')' ) { $graph->stamps(open_circle($right_end, &$subRef($right_end), $color) ); } } else { $error .= "Error in parsing: $fn $main::BR"; } } die ("Error in plot_functions: \n\t $error ") if $error; @functions; # return function references unless there is an error. } =head2 insertGraph $filePath = insertGraph(graphObject); returns a path to the file containing the graph image. B Because insertGraph involves writing to the disk, it is actually defined in dangerousMacros.pl. insertGraph(graphObject) writes a image file to the C directory of the current course. The file name is obtained from the graphObject. Warnings are issued if errors occur while writing to the file. The permissions and ownership of the file are controlled by C<$main::tmp_file_permission> and C<$main::numericalGroupID>. B A string containing the full path to the temporary file containing the image. InsertGraph draws the object $graph, stores it in "${tempDirectory}gif/$imageName.gif (or .png)" where the $imageName is obtained from the graph object. ConvertPath and surePathToTmpFile are used to insure that the correct directory separators are used for the platform and that the necessary directories are created if they are not already present. The directory address to the file is the result. The most common use of C,insertGraph> is TEXT(image(insertGraph($graph)) ); where C takes care of creating the proper URL for accessing the graph and for creating the HTML code to display the image. Another common usage is: TEXT(htmlLink( alias(insertGraph($graph), "picture" ) ) ); which inserts the URL pointing to the picture. alias converts the directory address to a URL when serving HTML pages and insures that an eps file is generated when creating TeX code for downloading. (Image, automatically applies alias to its input in order to obtain the URL.) See the documentation in F for the latest details. =cut =head2 'Circle' lables Usage: $circle_object = open_circle( $x_position, $y_position, $color ); $circle_object2 = closed_circle( $x_position, $y_position, $color ); Creates a small open (resp. filled in or closed) circle for use as a stamp in marking graphs. For example $graph -> stamps($circle_object2); # puts a filled dot at $x_position, $y_position =cut ######################################################### sub open_circle { my ($cx,$cy,$color) = @_; new Circle ($cx, $cy, 4,$color,'nearwhite'); } sub closed_circle { my ($cx,$cy, $color) = @_; $color = 'black' unless defined $color; new Circle ($cx, $cy, 4,$color, $color); } =head2 Auxiliary macros =head3 string_to_sub and my_math_constants These are internal macros which govern the interpretation of equations. Usage: $string = my_math_constants($string) $subroutine_reference = my_string_to_sub($string) C interprets pi, e as mathematical constants 3.1415926... and 2.71828... respectively. (Case is important). The power operator ^ is replaced by ** to conform with perl constructs C converts a string defining a single perl arithmetic expression with independent variable $XVAR into a subroutine. The string is first filtered through C. The resulting subroutine takes a single real number as input and produces a single output value. =cut sub my_math_constants { my($in) = @_; $in =~s/\bpi\b/(4*atan2(1,1))/g; $in =~s/\be\b/(exp(1))/g; $in =~s/\^/**/g; $in; } sub string_to_sub { my $str_in = shift; my $var = shift; my $out = undef; if ( defined(&check_syntax) ) { #prepare the correct answer and check it's syntax my $rh_correct_ans = new AnswerHash; $rh_correct_ans->input($str_in); $rh_correct_ans = check_syntax($rh_correct_ans); warn $rh_correct_ans->{error_message} if $rh_correct_ans->{error_flag}; $rh_correct_ans->clear_error(); $rh_correct_ans = function_from_string2($rh_correct_ans, ra_vars => ['x'], store_in =>'rf_correct_ans'); my $correct_eqn_sub = $rh_correct_ans->{rf_correct_ans}; warn $rh_correct_ans->{error_message} if $rh_correct_ans->{error_flag}; $out = sub{ scalar( &$correct_eqn_sub(@_) ) }; #ignore the error messages from the function. } else { my $in =$str_in; $in =~ s/\b$var\b/\$XVAR/g; $in = &my_math_constants($in); my ($subRef, $PG_eval_errors,$PG_full_error_report) = PG_restricted_eval( " sub { my \$XVAR = shift; my \$out = $in; \$out; } "); if ($PG_eval_errors) { die " ERROR while defining a function from the string:\n\n$main::BR $main::BR $str_in $main::BR $main::BR\n\n $PG_eval_errors" } else { $out = $subRef; } } $out; } ######################################################### 1;