/* Writes case C to output. */
static void
list_case (const struct ccase *c, casenumber case_idx,
- const struct dataset *ds)
+ const struct dataset *ds, struct ll_list *targets)
{
struct dictionary *dict = dataset_dict (ds);
- struct outp_driver *d;
++ const char *encoding = dict_get_encoding (dict);
+ struct list_target *target;
- for (d = outp_drivers (NULL); d; d = outp_drivers (d))
- if (d->class->special == 0)
- {
- const struct list_ext *prc = d->prc;
- const int max_width = n_chars_width (d);
- int column;
-
- if (!prc->header_rows)
- {
- ds_put_format(&line_buffer, "%8s: ",
- var_get_name (cmd.v_variables[0]));
- }
-
-
- for (column = 0; column < cmd.n_variables; column++)
- {
- const struct variable *v = cmd.v_variables[column];
- const struct fmt_spec *print = var_get_print_format (v);
- int width;
-
- if (prc->type == 0 && column >= prc->n_vertical)
- {
- int name_len = strlen (var_get_name (v));
- width = MAX (name_len, print->w);
- }
- else
- width = print->w;
-
- if (width + ds_length(&line_buffer) > max_width &&
- ds_length(&line_buffer) != 0)
- {
- if (!n_lines_remaining (d))
- {
- outp_eject_page (d);
- write_header (d);
- }
+ ll_for_each (target, struct list_target, ll, targets)
+ {
+ struct outp_driver *d = target->driver;
- write_line (d, ds_cstr (&line_buffer));
- ds_clear(&line_buffer);
+ if (d->class->special == 0)
+ {
+ const int max_width = n_chars_width (d);
+ int column;
- if (!prc->header_rows)
- ds_put_format (&line_buffer, "%8s: ", var_get_name (v));
- }
+ if (!target->header_rows)
+ {
+ ds_put_format(&line_buffer, "%8s: ",
+ var_get_name (cmd.v_variables[0]));
+ }
- if (width > print->w)
- ds_put_char_multiple(&line_buffer, ' ', width - print->w);
- if (fmt_is_string (print->type)
- || dict_contains_var (dict, v))
- {
- char *s = data_out (case_data (c, v), dict_get_encoding (dict), print);
- ds_put_cstr (&line_buffer, s);
- free (s);
- }
- else
- {
- char *s;
- union value case_idx_value;
- case_idx_value.f = case_idx;
- s = data_out (&case_idx_value, dict_get_encoding (dict), print);
- ds_put_cstr (&line_buffer, s);
- free (s);
- }
-
- ds_put_char (&line_buffer, ' ');
- }
+ for (column = 0; column < cmd.n_variables; column++)
+ {
+ const struct variable *v = cmd.v_variables[column];
+ const struct fmt_spec *print = var_get_print_format (v);
+ int width;
++ char *s;
+
+ if (target->type == 0 && column >= target->n_vertical)
+ {
+ int name_len = strlen (var_get_name (v));
+ width = MAX (name_len, print->w);
+ }
+ else
+ width = print->w;
+
+ if (width + ds_length(&line_buffer) > max_width &&
+ ds_length(&line_buffer) != 0)
+ {
+ if (!n_lines_remaining (d))
+ {
+ outp_eject_page (d);
+ write_header (target);
+ }
+
+ write_line (d, ds_cstr (&line_buffer));
+ ds_clear(&line_buffer);
+
+ if (!target->header_rows)
+ ds_put_format (&line_buffer, "%8s: ", var_get_name (v));
+ }
+
+ if (width > print->w)
+ ds_put_char_multiple(&line_buffer, ' ', width - print->w);
+
- if (fmt_is_string (print->type)
- || dict_contains_var (dict, v))
- {
- data_out (case_data (c, v), print,
- ds_put_uninit (&line_buffer, print->w));
- }
++ if (fmt_is_string (print->type) || dict_contains_var (dict, v))
++ s = data_out (case_data (c, v), encoding, print);
+ else
+ {
+ union value case_idx_value;
+ case_idx_value.f = case_idx;
- data_out (&case_idx_value, print,
- ds_put_uninit (&line_buffer,print->w));
++ s = data_out (&case_idx_value, encoding, print);
+ }
+
++ ds_put_cstr (&line_buffer, s);
++ free (s);
+ ds_put_char(&line_buffer, ' ');
+ }
- if (!n_lines_remaining (d))
- {
- outp_eject_page (d);
- write_header (d);
- }
+ if (!n_lines_remaining (d))
+ {
+ outp_eject_page (d);
+ write_header (target);
+ }
- write_line (d, ds_cstr (&line_buffer));
- ds_clear(&line_buffer);
- }
- else if (d->class == &html_class)
- {
- struct html_driver_ext *x = d->ext;
- int column;
+ write_line (d, ds_cstr (&line_buffer));
+ ds_clear(&line_buffer);
+ }
+ else if (d->class == &html_class)
+ {
+ struct html_driver_ext *x = d->ext;
+ int column;
- fputs (" <TR>\n", x->file);
+ fputs (" <TR>\n", x->file);
- for (column = 0; column < cmd.n_variables; column++)
- {
- const struct variable *v = cmd.v_variables[column];
- const struct fmt_spec *print = var_get_print_format (v);
- char *s = NULL;
-
- if (fmt_is_string (print->type)
- || dict_contains_var (dict, v))
- s = data_out (case_data (c, v), dict_get_encoding (dict), print);
- else
- {
- union value case_idx_value;
- case_idx_value.f = case_idx;
- s = data_out (&case_idx_value, dict_get_encoding (dict), print);
- }
-
- fputs (" <TD>", x->file);
- html_put_cell_contents (d, TAB_FIX, ss_buffer (s, print->w));
- free (s);
- fputs ("</TD>\n", x->file);
- }
+ for (column = 0; column < cmd.n_variables; column++)
+ {
+ const struct variable *v = cmd.v_variables[column];
+ const struct fmt_spec *print = var_get_print_format (v);
- char buf[256];
++ char *s;
+
+ if (fmt_is_string (print->type)
+ || dict_contains_var (dict, v))
- data_out (case_data (c, v), print, buf);
++ s = data_out (case_data (c, v), encoding, print);
+ else
+ {
+ union value case_idx_value;
+ case_idx_value.f = case_idx;
- data_out (&case_idx_value, print, buf);
++ s = data_out (&case_idx_value, encoding, print);
+ }
+
+ fputs (" <TD>", x->file);
- html_put_cell_contents (d, TAB_FIX, ss_buffer (buf, print->w));
++ html_put_cell_contents (d, TAB_FIX, ss_cstr (s));
+ fputs ("</TD>\n", x->file);
++
++ free (s);
+ }
- fputs (" </TR>\n", x->file);
- }
- else
- NOT_REACHED ();
+ fputs (" </TR>\n", x->file);
+ }
+ else
+ NOT_REACHED ();
+ }
}
+
/*
Local Variables:
mode: c
tab_text_format (t, 0, i + 1, TAB_LEFT, "%s", var_get_name (v));
- data_out (case_data (c, v), print, temp_buf);
- temp_buf[print->w] = 0;
+ s = data_out (case_data (c, v), dict_get_encoding (dict), print);
+
- tab_text_format (t, 1, i + 1, 0, "%.*s", print->w, temp_buf);
+ tab_text_format (t, 1, i + 1, 0, "%.*s", print->w, s);
+ free (s);
+
val_lab = var_lookup_value_label (v, case_data (c, v));
if (val_lab)
tab_text (t, 2, i + 1, TAB_LEFT, val_lab);
struct tab_table *table, int c, int r, unsigned char opt,
const union value *v, const struct variable *var)
{
- struct substring s;
- const struct fmt_spec *print = var_get_print_format (var);
-
const char *label = var_lookup_value_label (var, v);
- if (label)
- {
- tab_text (table, c, r, TAB_LEFT, label);
- return;
- }
-
- s = ss_cstr (data_out_pool (v, dict_get_encoding (proc->dict), print,
- table->container));
- if (proc->exclude == MV_NEVER && var_is_num_missing (var, v->f, MV_USER))
- s.string[s.length++] = 'M';
- while (s.length && *s.string == ' ')
+ if (label != NULL)
+ tab_text (table, c, r, TAB_LEFT, label);
+ else
{
- s.length--;
- s.string++;
+ const struct fmt_spec *print = var_get_print_format (var);
+ if (proc->exclude == MV_NEVER && var_is_value_missing (var, v, MV_USER))
+ {
- char *s = xmalloc (print->w + 2);
- strcpy (&s[print->w], "M");
- tab_text (table, c, r, opt, s + strspn (s, " "));
++ char *s = data_out (v, dict_get_encoding (proc->dict), print);
++ tab_text_format (table, c, r, opt, "%sM", s + strspn (s, " "));
+ free (s);
+ }
+ else
- tab_value (table, c, r, opt, v, print);
++ tab_value (table, c, r, opt, v, proc->dict, print);
}
- tab_raw (table, c, r, opt, &s);
}
/* Draws a line across TABLE at the current row to indicate the most
{
const struct fmt_spec f = {FMT_F, 10, 1};
union value v;
- int len = 10;
- char s[16];
- struct substring s;
++ char suffixes[3];
++ int suffix_len;
++ char *s;
v.f = value;
- data_out (&v, &f, s);
- s = ss_cstr (data_out_pool (&v, dict_get_encoding (dict), &f, table->container));
++ s = data_out (&v, dict_get_encoding (dict), &f);
+
- while (*s.string == ' ')
- {
- s.length--;
- s.string++;
- }
++ suffix_len = 0;
if (suffix != 0)
- s[len++] = suffix;
- s.string[s.length++] = suffix;
++ suffixes[suffix_len++] = suffix;
if (mark_missing)
- s[len++] = 'M';
- s[len] = '\0';
- s.string[s.length++] = 'M';
++ suffixes[suffix_len++] = 'M';
++ suffixes[suffix_len] = '\0';
- tab_text (table, c, r, TAB_RIGHT, s + strspn (s, " "));
- tab_raw (table, c, r, TAB_RIGHT, &s);
++ tab_text_format (table, c, r, TAB_RIGHT, "%s%s",
++ s + strspn (s, " "), suffixes);
}
/* Displays the crosstabulation table. */
--- /dev/null
-#include <output/charts/plot-chart.h>
-#include <output/charts/cartesian.h>
+ /* PSPP - a program for statistical analysis.
+ Copyright (C) 2009 Free Software Foundation, Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+ #include <config.h>
+
++#include <language/stats/roc.h>
++
+ #include <data/procedure.h>
+ #include <language/lexer/variable-parser.h>
+ #include <language/lexer/value-parser.h>
+ #include <language/command.h>
+ #include <language/lexer/lexer.h>
+
+ #include <data/casegrouper.h>
+ #include <data/casereader.h>
+ #include <data/casewriter.h>
+ #include <data/dictionary.h>
+ #include <data/format.h>
+ #include <math/sort.h>
+ #include <data/subcase.h>
+
+
+ #include <libpspp/misc.h>
+
+ #include <gsl/gsl_cdf.h>
+ #include <output/table.h>
+
-#define CUTPOINT 0
-#define TP 1
-#define FN 2
-#define TN 3
-#define FP 4
-
-
++#include <output/chart.h>
++#include <output/charts/roc-chart.h>
+
+ #include "gettext.h"
+ #define _(msgid) gettext (msgid)
+ #define N_(msgid) msgid
+
+ struct cmd_roc
+ {
+ size_t n_vars;
+ const struct variable **vars;
+ const struct dictionary *dict;
+
+ const struct variable *state_var ;
+ union value state_value;
+
+ /* Plot the roc curve */
+ bool curve;
+ /* Plot the reference line */
+ bool reference;
+
+ double ci;
+
+ bool print_coords;
+ bool print_se;
+ bool bi_neg_exp; /* True iff the bi-negative exponential critieria
+ should be used */
+ enum mv_class exclude;
+
+ bool invert ; /* True iff a smaller test result variable indicates
+ a positive result */
+
+ double pos;
+ double neg;
+ double pos_weighted;
+ double neg_weighted;
+ };
+
+ static int run_roc (struct dataset *ds, struct cmd_roc *roc);
+
+ int
+ cmd_roc (struct lexer *lexer, struct dataset *ds)
+ {
+ struct cmd_roc roc ;
+ const struct dictionary *dict = dataset_dict (ds);
+
+ roc.vars = NULL;
+ roc.n_vars = 0;
+ roc.print_se = false;
+ roc.print_coords = false;
+ roc.exclude = MV_ANY;
+ roc.curve = true;
+ roc.reference = false;
+ roc.ci = 95;
+ roc.bi_neg_exp = false;
+ roc.invert = false;
+ roc.pos = roc.pos_weighted = 0;
+ roc.neg = roc.neg_weighted = 0;
+ roc.dict = dataset_dict (ds);
+
+ if (!parse_variables_const (lexer, dict, &roc.vars, &roc.n_vars,
+ PV_APPEND | PV_NO_DUPLICATE | PV_NUMERIC))
+ goto error;
+
+ if ( ! lex_force_match (lexer, T_BY))
+ {
+ goto error;
+ }
+
+ roc.state_var = parse_variable (lexer, dict);
+
+ if ( !lex_force_match (lexer, '('))
+ {
+ goto error;
+ }
+
+ parse_value (lexer, &roc.state_value, var_get_width (roc.state_var));
+
+
+ if ( !lex_force_match (lexer, ')'))
+ {
+ goto error;
+ }
+
+
+ while (lex_token (lexer) != '.')
+ {
+ lex_match (lexer, '/');
+ if (lex_match_id (lexer, "MISSING"))
+ {
+ lex_match (lexer, '=');
+ while (lex_token (lexer) != '.' && lex_token (lexer) != '/')
+ {
+ if (lex_match_id (lexer, "INCLUDE"))
+ {
+ roc.exclude = MV_SYSTEM;
+ }
+ else if (lex_match_id (lexer, "EXCLUDE"))
+ {
+ roc.exclude = MV_ANY;
+ }
+ else
+ {
+ lex_error (lexer, NULL);
+ goto error;
+ }
+ }
+ }
+ else if (lex_match_id (lexer, "PLOT"))
+ {
+ lex_match (lexer, '=');
+ if (lex_match_id (lexer, "CURVE"))
+ {
+ roc.curve = true;
+ if (lex_match (lexer, '('))
+ {
+ roc.reference = true;
+ lex_force_match_id (lexer, "REFERENCE");
+ lex_force_match (lexer, ')');
+ }
+ }
+ else if (lex_match_id (lexer, "NONE"))
+ {
+ roc.curve = false;
+ }
+ else
+ {
+ lex_error (lexer, NULL);
+ goto error;
+ }
+ }
+ else if (lex_match_id (lexer, "PRINT"))
+ {
+ lex_match (lexer, '=');
+ while (lex_token (lexer) != '.' && lex_token (lexer) != '/')
+ {
+ if (lex_match_id (lexer, "SE"))
+ {
+ roc.print_se = true;
+ }
+ else if (lex_match_id (lexer, "COORDINATES"))
+ {
+ roc.print_coords = true;
+ }
+ else
+ {
+ lex_error (lexer, NULL);
+ goto error;
+ }
+ }
+ }
+ else if (lex_match_id (lexer, "CRITERIA"))
+ {
+ lex_match (lexer, '=');
+ while (lex_token (lexer) != '.' && lex_token (lexer) != '/')
+ {
+ if (lex_match_id (lexer, "CUTOFF"))
+ {
+ lex_force_match (lexer, '(');
+ if (lex_match_id (lexer, "INCLUDE"))
+ {
+ roc.exclude = MV_SYSTEM;
+ }
+ else if (lex_match_id (lexer, "EXCLUDE"))
+ {
+ roc.exclude = MV_USER | MV_SYSTEM;
+ }
+ else
+ {
+ lex_error (lexer, NULL);
+ goto error;
+ }
+ lex_force_match (lexer, ')');
+ }
+ else if (lex_match_id (lexer, "TESTPOS"))
+ {
+ lex_force_match (lexer, '(');
+ if (lex_match_id (lexer, "LARGE"))
+ {
+ roc.invert = false;
+ }
+ else if (lex_match_id (lexer, "SMALL"))
+ {
+ roc.invert = true;
+ }
+ else
+ {
+ lex_error (lexer, NULL);
+ goto error;
+ }
+ lex_force_match (lexer, ')');
+ }
+ else if (lex_match_id (lexer, "CI"))
+ {
+ lex_force_match (lexer, '(');
+ lex_force_num (lexer);
+ roc.ci = lex_number (lexer);
+ lex_get (lexer);
+ lex_force_match (lexer, ')');
+ }
+ else if (lex_match_id (lexer, "DISTRIBUTION"))
+ {
+ lex_force_match (lexer, '(');
+ if (lex_match_id (lexer, "FREE"))
+ {
+ roc.bi_neg_exp = false;
+ }
+ else if (lex_match_id (lexer, "NEGEXPO"))
+ {
+ roc.bi_neg_exp = true;
+ }
+ else
+ {
+ lex_error (lexer, NULL);
+ goto error;
+ }
+ lex_force_match (lexer, ')');
+ }
+ else
+ {
+ lex_error (lexer, NULL);
+ goto error;
+ }
+ }
+ }
+ else
+ {
+ lex_error (lexer, NULL);
+ break;
+ }
+ }
+
+ if ( ! run_roc (ds, &roc))
+ goto error;
+
+ free (roc.vars);
+ return CMD_SUCCESS;
+
+ error:
+ free (roc.vars);
+ return CMD_FAILURE;
+ }
+
+
+
+
+ static void
+ do_roc (struct cmd_roc *roc, struct casereader *group, struct dictionary *dict);
+
+
+ static int
+ run_roc (struct dataset *ds, struct cmd_roc *roc)
+ {
+ struct dictionary *dict = dataset_dict (ds);
+ bool ok;
+ struct casereader *group;
+
+ struct casegrouper *grouper = casegrouper_create_splits (proc_open (ds), dict);
+ while (casegrouper_get_next_group (grouper, &group))
+ {
+ do_roc (roc, group, dataset_dict (ds));
+ }
+ ok = casegrouper_destroy (grouper);
+ ok = proc_commit (ds) && ok;
+
+ return ok;
+ }
+
+ #if 0
+ static void
+ dump_casereader (struct casereader *reader)
+ {
+ struct ccase *c;
+ struct casereader *r = casereader_clone (reader);
+
+ for ( ; (c = casereader_read (r) ); case_unref (c))
+ {
+ int i;
+ for (i = 0 ; i < case_get_value_cnt (c); ++i)
+ {
+ printf ("%g ", case_data_idx (c, i)->f);
+ }
+ printf ("\n");
+ }
+
+ casereader_destroy (r);
+ }
+ #endif
+
+
+ /*
+ Return true iff the state variable indicates that C has positive actual state.
+
+ As a side effect, this function also accumulates the roc->{pos,neg} and
+ roc->{pos,neg}_weighted counts.
+ */
+ static bool
+ match_positives (const struct ccase *c, void *aux)
+ {
+ struct cmd_roc *roc = aux;
+ const struct variable *wv = dict_get_weight (roc->dict);
+ const double weight = wv ? case_data (c, wv)->f : 1.0;
+
+ const bool positive =
+ ( 0 == value_compare_3way (case_data (c, roc->state_var), &roc->state_value,
+ var_get_width (roc->state_var)));
+
+ if ( positive )
+ {
+ roc->pos++;
+ roc->pos_weighted += weight;
+ }
+ else
+ {
+ roc->neg++;
+ roc->neg_weighted += weight;
+ }
+
+ return positive;
+ }
+
+
+ #define VALUE 0
+ #define N_EQ 1
+ #define N_PRED 2
+
+ /* Some intermediate state for calculating the cutpoints and the
+ standard error values */
+ struct roc_state
+ {
+ double auc; /* Area under the curve */
+
+ double n1; /* total weight of positives */
+ double n2; /* total weight of negatives */
+
+ /* intermediates for standard error */
+ double q1hat;
+ double q2hat;
+
+ /* intermediates for cutpoints */
+ struct casewriter *cutpoint_wtr;
+ struct casereader *cutpoint_rdr;
+ double prev_result;
+ double min;
+ double max;
+ };
+
- const double cp = case_data_idx (cpc, CUTPOINT)->f;
+ /*
+ Return a new casereader based upon CUTPOINT_RDR.
+ The number of "positive" cases are placed into
+ the position TRUE_INDEX, and the number of "negative" cases
+ into FALSE_INDEX.
+ POS_COND and RESULT determine the semantics of what is
+ "positive".
+ WEIGHT is the value of a single count.
+ */
+ static struct casereader *
+ accumulate_counts (struct casereader *cutpoint_rdr,
+ double result, double weight,
+ bool (*pos_cond) (double, double),
+ int true_index, int false_index)
+ {
+ const struct caseproto *proto = casereader_get_proto (cutpoint_rdr);
+ struct casewriter *w =
+ autopaging_writer_create (proto);
+ struct casereader *r = casereader_clone (cutpoint_rdr);
+ struct ccase *cpc;
+ double prev_cp = SYSMIS;
+
+ for ( ; (cpc = casereader_read (r) ); case_unref (cpc))
+ {
+ struct ccase *new_case;
- TP, FN);
++ const double cp = case_data_idx (cpc, ROC_CUTPOINT)->f;
+
+ assert (cp != SYSMIS);
+
+ /* We don't want duplicates here */
+ if ( cp == prev_cp )
+ continue;
+
+ new_case = case_clone (cpc);
+
+ if ( pos_cond (result, cp))
+ case_data_rw_idx (new_case, true_index)->f += weight;
+ else
+ case_data_rw_idx (new_case, false_index)->f += weight;
+
+ prev_cp = cp;
+
+ casewriter_write (w, new_case);
+ }
+ casereader_destroy (r);
+
+ return casewriter_make_reader (w);
+ }
+
+
+
+ static void output_roc (struct roc_state *rs, const struct cmd_roc *roc);
+
+ /*
+ This function does 3 things:
+
+ 1. Counts the number of cases which are equal to every other case in READER,
+ and those cases for which the relationship between it and every other case
+ satifies PRED (normally either > or <). VAR is variable defining a case's value
+ for this purpose.
+
+ 2. Counts the number of true and false cases in reader, and populates
+ CUTPOINT_RDR accordingly. TRUE_INDEX and FALSE_INDEX are the indices
+ which receive these values. POS_COND is the condition defining true
+ and false.
+
+ 3. CC is filled with the cumulative weight of all cases of READER.
+ */
+ static struct casereader *
+ process_group (const struct variable *var, struct casereader *reader,
+ bool (*pred) (double, double),
+ const struct dictionary *dict,
+ double *cc,
+ struct casereader **cutpoint_rdr,
+ bool (*pos_cond) (double, double),
+ int true_index,
+ int false_index)
+ {
+ const struct variable *w = dict_get_weight (dict);
+
+ struct casereader *r1 =
+ casereader_create_distinct (sort_execute_1var (reader, var), var, w);
+
+ const int weight_idx = w ? var_get_case_index (w) :
+ caseproto_get_n_widths (casereader_get_proto (r1)) - 1;
+
+ struct ccase *c1;
+
+ struct casereader *rclone = casereader_clone (r1);
+ struct casewriter *wtr;
+ struct caseproto *proto = caseproto_create ();
+
+ proto = caseproto_add_width (proto, 0);
+ proto = caseproto_add_width (proto, 0);
+ proto = caseproto_add_width (proto, 0);
+
+ wtr = autopaging_writer_create (proto);
+
+ *cc = 0;
+
+ for ( ; (c1 = casereader_read (r1) ); case_unref (c1))
+ {
+ struct ccase *new_case = case_create (proto);
+ struct ccase *c2;
+ struct casereader *r2 = casereader_clone (rclone);
+
+ const double weight1 = case_data_idx (c1, weight_idx)->f;
+ const double d1 = case_data (c1, var)->f;
+ double n_eq = 0.0;
+ double n_pred = 0.0;
+
+ *cutpoint_rdr = accumulate_counts (*cutpoint_rdr, d1, weight1,
+ pos_cond,
+ true_index, false_index);
+
+ *cc += weight1;
+
+ for ( ; (c2 = casereader_read (r2) ); case_unref (c2))
+ {
+ const double d2 = case_data (c2, var)->f;
+ const double weight2 = case_data_idx (c2, weight_idx)->f;
+
+ if ( d1 == d2 )
+ {
+ n_eq += weight2;
+ continue;
+ }
+ else if ( pred (d2, d1))
+ {
+ n_pred += weight2;
+ }
+ }
+
+ case_data_rw_idx (new_case, VALUE)->f = d1;
+ case_data_rw_idx (new_case, N_EQ)->f = n_eq;
+ case_data_rw_idx (new_case, N_PRED)->f = n_pred;
+
+ casewriter_write (wtr, new_case);
+
+ casereader_destroy (r2);
+ }
+
+ casereader_destroy (r1);
+ casereader_destroy (rclone);
+
+ return casewriter_make_reader (wtr);
+ }
+
+ /* Some more indeces into case data */
+ #define N_POS_EQ 1 /* number of positive cases with values equal to n */
+ #define N_POS_GT 2 /* number of postive cases with values greater than n */
+ #define N_NEG_EQ 3 /* number of negative cases with values equal to n */
+ #define N_NEG_LT 4 /* number of negative cases with values less than n */
+
+ static bool
+ gt (double d1, double d2)
+ {
+ return d1 > d2;
+ }
+
+
+ static bool
+ ge (double d1, double d2)
+ {
+ return d1 > d2;
+ }
+
+ static bool
+ lt (double d1, double d2)
+ {
+ return d1 < d2;
+ }
+
+
+ /*
+ Return a casereader with width 3,
+ populated with cases based upon READER.
+ The cases will have the values:
+ (N, number of cases equal to N, number of cases greater than N)
+ As a side effect, update RS->n1 with the number of positive cases.
+ */
+ static struct casereader *
+ process_positive_group (const struct variable *var, struct casereader *reader,
+ const struct dictionary *dict,
+ struct roc_state *rs)
+ {
+ return process_group (var, reader, gt, dict, &rs->n1,
+ &rs->cutpoint_rdr,
+ ge,
- TN, FP);
++ ROC_TP, ROC_FN);
+ }
+
+ /*
+ Return a casereader with width 3,
+ populated with cases based upon READER.
+ The cases will have the values:
+ (N, number of cases equal to N, number of cases less than N)
+ As a side effect, update RS->n2 with the number of negative cases.
+ */
+ static struct casereader *
+ process_negative_group (const struct variable *var, struct casereader *reader,
+ const struct dictionary *dict,
+ struct roc_state *rs)
+ {
+ return process_group (var, reader, lt, dict, &rs->n2,
+ &rs->cutpoint_rdr,
+ lt,
- case_data_rw_idx (cc, CUTPOINT)->f = cutpoint;
- case_data_rw_idx (cc, TP)->f = 0;
- case_data_rw_idx (cc, FN)->f = 0;
- case_data_rw_idx (cc, TN)->f = 0;
- case_data_rw_idx (cc, FP)->f = 0;
++ ROC_TN, ROC_FP);
+ }
+
+
+
+
+ static void
+ append_cutpoint (struct casewriter *writer, double cutpoint)
+ {
+ struct ccase *cc = case_create (casewriter_get_proto (writer));
+
- be created with width 5, ready to take the values (cutpoint, TP, FN, TN, FP), and the
++ case_data_rw_idx (cc, ROC_CUTPOINT)->f = cutpoint;
++ case_data_rw_idx (cc, ROC_TP)->f = 0;
++ case_data_rw_idx (cc, ROC_FN)->f = 0;
++ case_data_rw_idx (cc, ROC_TN)->f = 0;
++ case_data_rw_idx (cc, ROC_FP)->f = 0;
+
+ casewriter_write (writer, cc);
+ }
+
+
+ /*
+ Create and initialise the rs[x].cutpoint_rdr casereaders. That is, the readers will
- However on exit from this function, only CUTPOINT entries will be set to their final
++ be created with width 5, ready to take the values (cutpoint, ROC_TP, ROC_FN, ROC_TN, ROC_FP), and the
+ reader will be populated with its final number of cases.
- subcase_init (&ordering, CUTPOINT, 0, SC_ASCEND);
++ However on exit from this function, only ROC_CUTPOINT entries will be set to their final
+ value. The other entries will be initialised to zero.
+ */
+ static void
+ prepare_cutpoints (struct cmd_roc *roc, struct roc_state *rs, struct casereader *input)
+ {
+ int i;
+ struct casereader *r = casereader_clone (input);
+ struct ccase *c;
+ struct caseproto *proto = caseproto_create ();
+
+ struct subcase ordering;
- proto = caseproto_add_width (proto, 0); /* TP */
- proto = caseproto_add_width (proto, 0); /* FN */
- proto = caseproto_add_width (proto, 0); /* TN */
- proto = caseproto_add_width (proto, 0); /* FP */
++ subcase_init (&ordering, ROC_CUTPOINT, 0, SC_ASCEND);
+
+ proto = caseproto_add_width (proto, 0); /* cutpoint */
- struct tab_table *tbl = tab_create (n_cols, n_rows, 0);
++ proto = caseproto_add_width (proto, 0); /* ROC_TP */
++ proto = caseproto_add_width (proto, 0); /* ROC_FN */
++ proto = caseproto_add_width (proto, 0); /* ROC_TN */
++ proto = caseproto_add_width (proto, 0); /* ROC_FP */
+
+ for (i = 0 ; i < roc->n_vars; ++i)
+ {
+ rs[i].cutpoint_wtr = sort_create_writer (&ordering, proto);
+ rs[i].prev_result = SYSMIS;
+ rs[i].max = -DBL_MAX;
+ rs[i].min = DBL_MAX;
+ }
+
+ for (; (c = casereader_read (r)) != NULL; case_unref (c))
+ {
+ for (i = 0 ; i < roc->n_vars; ++i)
+ {
+ const union value *v = case_data (c, roc->vars[i]);
+ const double result = v->f;
+
+ if ( mv_is_value_missing (var_get_missing_values (roc->vars[i]), v, roc->exclude))
+ continue;
+
+ minimize (&rs[i].min, result);
+ maximize (&rs[i].max, result);
+
+ if ( rs[i].prev_result != SYSMIS && rs[i].prev_result != result )
+ {
+ const double mean = (result + rs[i].prev_result ) / 2.0;
+ append_cutpoint (rs[i].cutpoint_wtr, mean);
+ }
+
+ rs[i].prev_result = result;
+ }
+ }
+ casereader_destroy (r);
+
+
+ /* Append the min and max cutpoints */
+ for (i = 0 ; i < roc->n_vars; ++i)
+ {
+ append_cutpoint (rs[i].cutpoint_wtr, rs[i].min - 1);
+ append_cutpoint (rs[i].cutpoint_wtr, rs[i].max + 1);
+
+ rs[i].cutpoint_rdr = casewriter_make_reader (rs[i].cutpoint_wtr);
+ }
+ }
+
+ static void
+ do_roc (struct cmd_roc *roc, struct casereader *reader, struct dictionary *dict)
+ {
+ int i;
+
+ struct roc_state *rs = xcalloc (roc->n_vars, sizeof *rs);
+
+ struct casereader *negatives = NULL;
+ struct casereader *positives = NULL;
+
+ struct caseproto *n_proto = caseproto_create ();
+
+ struct subcase up_ordering;
+ struct subcase down_ordering;
+
+ struct casewriter *neg_wtr = NULL;
+
+ struct casereader *input = casereader_create_filter_missing (reader,
+ roc->vars, roc->n_vars,
+ roc->exclude,
+ NULL,
+ NULL);
+
+ input = casereader_create_filter_missing (input,
+ &roc->state_var, 1,
+ roc->exclude,
+ NULL,
+ NULL);
+
+ neg_wtr = autopaging_writer_create (casereader_get_proto (input));
+
+ prepare_cutpoints (roc, rs, input);
+
+
+ /* Separate the positive actual state cases from the negative ones */
+ positives =
+ casereader_create_filter_func (input,
+ match_positives,
+ NULL,
+ roc,
+ neg_wtr);
+
+ n_proto = caseproto_create ();
+
+ n_proto = caseproto_add_width (n_proto, 0);
+ n_proto = caseproto_add_width (n_proto, 0);
+ n_proto = caseproto_add_width (n_proto, 0);
+ n_proto = caseproto_add_width (n_proto, 0);
+ n_proto = caseproto_add_width (n_proto, 0);
+
+ subcase_init (&up_ordering, VALUE, 0, SC_ASCEND);
+ subcase_init (&down_ordering, VALUE, 0, SC_DESCEND);
+
+ for (i = 0 ; i < roc->n_vars; ++i)
+ {
+ struct casewriter *w = NULL;
+ struct casereader *r = NULL;
+
+ struct ccase *c;
+
+ struct ccase *cpos;
+ struct casereader *n_neg ;
+ const struct variable *var = roc->vars[i];
+
+ struct casereader *neg ;
+ struct casereader *pos = casereader_clone (positives);
+
+
+ struct casereader *n_pos =
+ process_positive_group (var, pos, dict, &rs[i]);
+
+ if ( negatives == NULL)
+ {
+ negatives = casewriter_make_reader (neg_wtr);
+ }
+
+ neg = casereader_clone (negatives);
+
+ n_neg = process_negative_group (var, neg, dict, &rs[i]);
+
+
+ /* Merge the n_pos and n_neg casereaders */
+ w = sort_create_writer (&up_ordering, n_proto);
+ for ( ; (cpos = casereader_read (n_pos) ); case_unref (cpos))
+ {
+ struct ccase *pos_case = case_create (n_proto);
+ struct ccase *cneg;
+ const double jpos = case_data_idx (cpos, VALUE)->f;
+
+ while ((cneg = casereader_read (n_neg)))
+ {
+ struct ccase *nc = case_create (n_proto);
+
+ const double jneg = case_data_idx (cneg, VALUE)->f;
+
+ case_data_rw_idx (nc, VALUE)->f = jneg;
+ case_data_rw_idx (nc, N_POS_EQ)->f = 0;
+
+ case_data_rw_idx (nc, N_POS_GT)->f = SYSMIS;
+
+ *case_data_rw_idx (nc, N_NEG_EQ) = *case_data_idx (cneg, N_EQ);
+ *case_data_rw_idx (nc, N_NEG_LT) = *case_data_idx (cneg, N_PRED);
+
+ casewriter_write (w, nc);
+
+ case_unref (cneg);
+ if ( jneg > jpos)
+ break;
+ }
+
+ case_data_rw_idx (pos_case, VALUE)->f = jpos;
+ *case_data_rw_idx (pos_case, N_POS_EQ) = *case_data_idx (cpos, N_EQ);
+ *case_data_rw_idx (pos_case, N_POS_GT) = *case_data_idx (cpos, N_PRED);
+ case_data_rw_idx (pos_case, N_NEG_EQ)->f = 0;
+ case_data_rw_idx (pos_case, N_NEG_LT)->f = SYSMIS;
+
+ casewriter_write (w, pos_case);
+ }
+
+ /* These aren't used anymore */
+ #undef N_EQ
+ #undef N_PRED
+
+ r = casewriter_make_reader (w);
+
+ /* Propagate the N_POS_GT values from the positive cases
+ to the negative ones */
+ {
+ double prev_pos_gt = rs[i].n1;
+ w = sort_create_writer (&down_ordering, n_proto);
+
+ for ( ; (c = casereader_read (r) ); case_unref (c))
+ {
+ double n_pos_gt = case_data_idx (c, N_POS_GT)->f;
+ struct ccase *nc = case_clone (c);
+
+ if ( n_pos_gt == SYSMIS)
+ {
+ n_pos_gt = prev_pos_gt;
+ case_data_rw_idx (nc, N_POS_GT)->f = n_pos_gt;
+ }
+
+ casewriter_write (w, nc);
+ prev_pos_gt = n_pos_gt;
+ }
+
+ r = casewriter_make_reader (w);
+ }
+
+ /* Propagate the N_NEG_LT values from the negative cases
+ to the positive ones */
+ {
+ double prev_neg_lt = rs[i].n2;
+ w = sort_create_writer (&up_ordering, n_proto);
+
+ for ( ; (c = casereader_read (r) ); case_unref (c))
+ {
+ double n_neg_lt = case_data_idx (c, N_NEG_LT)->f;
+ struct ccase *nc = case_clone (c);
+
+ if ( n_neg_lt == SYSMIS)
+ {
+ n_neg_lt = prev_neg_lt;
+ case_data_rw_idx (nc, N_NEG_LT)->f = n_neg_lt;
+ }
+
+ casewriter_write (w, nc);
+ prev_neg_lt = n_neg_lt;
+ }
+
+ r = casewriter_make_reader (w);
+ }
+
+ {
+ struct ccase *prev_case = NULL;
+ for ( ; (c = casereader_read (r) ); case_unref (c))
+ {
+ const struct ccase *next_case = casereader_peek (r, 0);
+
+ const double j = case_data_idx (c, VALUE)->f;
+ double n_pos_eq = case_data_idx (c, N_POS_EQ)->f;
+ double n_pos_gt = case_data_idx (c, N_POS_GT)->f;
+ double n_neg_eq = case_data_idx (c, N_NEG_EQ)->f;
+ double n_neg_lt = case_data_idx (c, N_NEG_LT)->f;
+
+ if ( prev_case && j == case_data_idx (prev_case, VALUE)->f)
+ {
+ if ( 0 == case_data_idx (c, N_POS_EQ)->f)
+ {
+ n_pos_eq = case_data_idx (prev_case, N_POS_EQ)->f;
+ n_pos_gt = case_data_idx (prev_case, N_POS_GT)->f;
+ }
+
+ if ( 0 == case_data_idx (c, N_NEG_EQ)->f)
+ {
+ n_neg_eq = case_data_idx (prev_case, N_NEG_EQ)->f;
+ n_neg_lt = case_data_idx (prev_case, N_NEG_LT)->f;
+ }
+ }
+
+ if ( NULL == next_case || j != case_data_idx (next_case, VALUE)->f)
+ {
+ rs[i].auc += n_pos_gt * n_neg_eq + (n_pos_eq * n_neg_eq) / 2.0;
+
+ rs[i].q1hat +=
+ n_neg_eq * ( pow2 (n_pos_gt) + n_pos_gt * n_pos_eq + pow2 (n_pos_eq) / 3.0);
+ rs[i].q2hat +=
+ n_pos_eq * ( pow2 (n_neg_lt) + n_neg_lt * n_neg_eq + pow2 (n_neg_eq) / 3.0);
+
+ }
+
+ case_unref (prev_case);
+ prev_case = case_clone (c);
+ }
+
+ rs[i].auc /= rs[i].n1 * rs[i].n2;
+ if ( roc->invert )
+ rs[i].auc = 1 - rs[i].auc;
+
+ if ( roc->bi_neg_exp )
+ {
+ rs[i].q1hat = rs[i].auc / ( 2 - rs[i].auc);
+ rs[i].q2hat = 2 * pow2 (rs[i].auc) / ( 1 + rs[i].auc);
+ }
+ else
+ {
+ rs[i].q1hat /= rs[i].n2 * pow2 (rs[i].n1);
+ rs[i].q2hat /= rs[i].n1 * pow2 (rs[i].n2);
+ }
+ }
+ }
+
+ casereader_destroy (positives);
+ casereader_destroy (negatives);
+
+ output_roc (rs, roc);
+
+ free (rs);
+ }
+
+ static void
+ show_auc (struct roc_state *rs, const struct cmd_roc *roc)
+ {
+ int i;
+ const int n_fields = roc->print_se ? 5 : 1;
+ const int n_cols = roc->n_vars > 1 ? n_fields + 1: n_fields;
+ const int n_rows = 2 + roc->n_vars;
- tab_dim (tbl, tab_natural_dimensions, NULL);
++ struct tab_table *tbl = tab_create (n_cols, n_rows);
+
+ if ( roc->n_vars > 1)
+ tab_title (tbl, _("Area Under the Curve"));
+ else
+ tab_title (tbl, _("Area Under the Curve (%s)"), var_to_string (roc->vars[0]));
+
+ tab_headers (tbl, n_cols - n_fields, 0, 1, 0);
+
- struct tab_table *tbl = tab_create (n_cols, n_rows, 0);
++ tab_dim (tbl, tab_natural_dimensions, NULL, NULL);
+
+ tab_text (tbl, n_cols - n_fields, 1, TAT_TITLE, _("Area"));
+
+ tab_hline (tbl, TAL_2, 0, n_cols - 1, 2);
+
+ tab_box (tbl,
+ TAL_2, TAL_2,
+ -1, TAL_1,
+ 0, 0,
+ n_cols - 1,
+ n_rows - 1);
+
+ if ( roc->print_se )
+ {
+ tab_text (tbl, n_cols - 4, 1, TAT_TITLE, _("Std. Error"));
+ tab_text (tbl, n_cols - 3, 1, TAT_TITLE, _("Asymptotic Sig."));
+
+ tab_text (tbl, n_cols - 2, 1, TAT_TITLE, _("Lower Bound"));
+ tab_text (tbl, n_cols - 1, 1, TAT_TITLE, _("Upper Bound"));
+
+ tab_joint_text_format (tbl, n_cols - 2, 0, 4, 0,
+ TAT_TITLE | TAB_CENTER,
+ _("Asymp. %g%% Confidence Interval"), roc->ci);
+ tab_vline (tbl, 0, n_cols - 1, 0, 0);
+ tab_hline (tbl, TAL_1, n_cols - 2, n_cols - 1, 1);
+ }
+
+ if ( roc->n_vars > 1)
+ tab_text (tbl, 0, 1, TAT_TITLE, _("Variable under test"));
+
+ if ( roc->n_vars > 1)
+ tab_vline (tbl, TAL_2, 1, 0, n_rows - 1);
+
+
+ for ( i = 0 ; i < roc->n_vars ; ++i )
+ {
+ tab_text (tbl, 0, 2 + i, TAT_TITLE, var_to_string (roc->vars[i]));
+
+ tab_double (tbl, n_cols - n_fields, 2 + i, 0, rs[i].auc, NULL);
+
+ if ( roc->print_se )
+ {
+ double se ;
+ const double sd_0_5 = sqrt ((rs[i].n1 + rs[i].n2 + 1) /
+ (12 * rs[i].n1 * rs[i].n2));
+ double ci ;
+ double yy ;
+
+ se = rs[i].auc * (1 - rs[i].auc) + (rs[i].n1 - 1) * (rs[i].q1hat - pow2 (rs[i].auc)) +
+ (rs[i].n2 - 1) * (rs[i].q2hat - pow2 (rs[i].auc));
+
+ se /= rs[i].n1 * rs[i].n2;
+
+ se = sqrt (se);
+
+ tab_double (tbl, n_cols - 4, 2 + i, 0,
+ se,
+ NULL);
+
+ ci = 1 - roc->ci / 100.0;
+ yy = gsl_cdf_gaussian_Qinv (ci, se) ;
+
+ tab_double (tbl, n_cols - 2, 2 + i, 0,
+ rs[i].auc - yy,
+ NULL);
+
+ tab_double (tbl, n_cols - 1, 2 + i, 0,
+ rs[i].auc + yy,
+ NULL);
+
+ tab_double (tbl, n_cols - 3, 2 + i, 0,
+ 2.0 * gsl_cdf_ugaussian_Q (fabs ((rs[i].auc - 0.5 ) / sd_0_5)),
+ NULL);
+ }
+ }
+
+ tab_submit (tbl);
+ }
+
+
+ static void
+ show_summary (const struct cmd_roc *roc)
+ {
+ const int n_cols = 3;
+ const int n_rows = 4;
- tab_dim (tbl, tab_natural_dimensions, NULL);
++ struct tab_table *tbl = tab_create (n_cols, n_rows);
+
+ tab_title (tbl, _("Case Summary"));
+
+ tab_headers (tbl, 1, 0, 2, 0);
+
- tbl = tab_create (n_cols, n_rows, 0);
++ tab_dim (tbl, tab_natural_dimensions, NULL, NULL);
+
+ tab_box (tbl,
+ TAL_2, TAL_2,
+ -1, -1,
+ 0, 0,
+ n_cols - 1,
+ n_rows - 1);
+
+ tab_hline (tbl, TAL_2, 0, n_cols - 1, 2);
+ tab_vline (tbl, TAL_2, 1, 0, n_rows - 1);
+
+
+ tab_hline (tbl, TAL_2, 1, n_cols - 1, 1);
+ tab_vline (tbl, TAL_1, 2, 1, n_rows - 1);
+
+
+ tab_text (tbl, 0, 1, TAT_TITLE | TAB_LEFT, var_to_string (roc->state_var));
+ tab_text (tbl, 1, 1, TAT_TITLE, _("Unweighted"));
+ tab_text (tbl, 2, 1, TAT_TITLE, _("Weighted"));
+
+ tab_joint_text (tbl, 1, 0, 2, 0,
+ TAT_TITLE | TAB_CENTER,
+ _("Valid N (listwise)"));
+
+
+ tab_text (tbl, 0, 2, TAB_LEFT, _("Positive"));
+ tab_text (tbl, 0, 3, TAB_LEFT, _("Negative"));
+
+
+ tab_double (tbl, 1, 2, 0, roc->pos, &F_8_0);
+ tab_double (tbl, 1, 3, 0, roc->neg, &F_8_0);
+
+ tab_double (tbl, 2, 2, 0, roc->pos_weighted, 0);
+ tab_double (tbl, 2, 3, 0, roc->neg_weighted, 0);
+
+ tab_submit (tbl);
+ }
+
+
+ static void
+ show_coords (struct roc_state *rs, const struct cmd_roc *roc)
+ {
+ int x = 1;
+ int i;
+ const int n_cols = roc->n_vars > 1 ? 4 : 3;
+ int n_rows = 1;
+ struct tab_table *tbl ;
+
+ for (i = 0; i < roc->n_vars; ++i)
+ n_rows += casereader_count_cases (rs[i].cutpoint_rdr);
+
- tab_dim (tbl, tab_natural_dimensions, NULL);
++ tbl = tab_create (n_cols, n_rows);
+
+ if ( roc->n_vars > 1)
+ tab_title (tbl, _("Coordinates of the Curve"));
+ else
+ tab_title (tbl, _("Coordinates of the Curve (%s)"), var_to_string (roc->vars[0]));
+
+
+ tab_headers (tbl, 1, 0, 1, 0);
+
- const double se = case_data_idx (cc, TP)->f /
++ tab_dim (tbl, tab_natural_dimensions, NULL, NULL);
+
+ tab_hline (tbl, TAL_2, 0, n_cols - 1, 1);
+
+ if ( roc->n_vars > 1)
+ tab_text (tbl, 0, 0, TAT_TITLE, _("Test variable"));
+
+ tab_text (tbl, n_cols - 3, 0, TAT_TITLE, _("Positive if greater than or equal to"));
+ tab_text (tbl, n_cols - 2, 0, TAT_TITLE, _("Sensitivity"));
+ tab_text (tbl, n_cols - 1, 0, TAT_TITLE, _("1 - Specificity"));
+
+ tab_box (tbl,
+ TAL_2, TAL_2,
+ -1, TAL_1,
+ 0, 0,
+ n_cols - 1,
+ n_rows - 1);
+
+ if ( roc->n_vars > 1)
+ tab_vline (tbl, TAL_2, 1, 0, n_rows - 1);
+
+ for (i = 0; i < roc->n_vars; ++i)
+ {
+ struct ccase *cc;
+ struct casereader *r = casereader_clone (rs[i].cutpoint_rdr);
+
+ if ( roc->n_vars > 1)
+ tab_text (tbl, 0, x, TAT_TITLE, var_to_string (roc->vars[i]));
+
+ if ( i > 0)
+ tab_hline (tbl, TAL_1, 0, n_cols - 1, x);
+
+
+ for (; (cc = casereader_read (r)) != NULL;
+ case_unref (cc), x++)
+ {
- case_data_idx (cc, TP)->f
++ const double se = case_data_idx (cc, ROC_TP)->f /
+ (
- case_data_idx (cc, FN)->f
++ case_data_idx (cc, ROC_TP)->f
+ +
- const double sp = case_data_idx (cc, TN)->f /
++ case_data_idx (cc, ROC_FN)->f
+ );
+
- case_data_idx (cc, TN)->f
++ const double sp = case_data_idx (cc, ROC_TN)->f /
+ (
- case_data_idx (cc, FP)->f
++ case_data_idx (cc, ROC_TN)->f
+ +
- tab_double (tbl, n_cols - 3, x, 0, case_data_idx (cc, CUTPOINT)->f,
++ case_data_idx (cc, ROC_FP)->f
+ );
+
-static void
-draw_roc (struct roc_state *rs, const struct cmd_roc *roc)
-{
- int i;
-
- struct chart *roc_chart = chart_create ();
-
- chart_write_title (roc_chart, _("ROC Curve"));
- chart_write_xlabel (roc_chart, _("1 - Specificity"));
- chart_write_ylabel (roc_chart, _("Sensitivity"));
-
- chart_write_xscale (roc_chart, 0, 1, 5);
- chart_write_yscale (roc_chart, 0, 1, 5);
-
- if ( roc->reference )
- {
- chart_line (roc_chart, 1.0, 0,
- 0.0, 1.0,
- CHART_DIM_X);
- }
-
- for (i = 0; i < roc->n_vars; ++i)
- {
- struct ccase *cc;
- struct casereader *r = casereader_clone (rs[i].cutpoint_rdr);
-
- chart_vector_start (roc_chart, var_get_name (roc->vars[i]));
- for (; (cc = casereader_read (r)) != NULL;
- case_unref (cc))
- {
- double se = case_data_idx (cc, TP)->f;
- double sp = case_data_idx (cc, TN)->f;
-
- se /= case_data_idx (cc, FN)->f +
- case_data_idx (cc, TP)->f ;
-
- sp /= case_data_idx (cc, TN)->f +
- case_data_idx (cc, FP)->f ;
-
- chart_vector (roc_chart, 1 - sp, se);
- }
- chart_vector_end (roc_chart);
- casereader_destroy (r);
- }
-
- chart_write_legend (roc_chart);
-
- chart_submit (roc_chart);
-}
-
-
++ tab_double (tbl, n_cols - 3, x, 0, case_data_idx (cc, ROC_CUTPOINT)->f,
+ var_get_print_format (roc->vars[i]));
+
+ tab_double (tbl, n_cols - 2, x, 0, se, NULL);
+ tab_double (tbl, n_cols - 1, x, 0, 1 - sp, NULL);
+ }
+
+ casereader_destroy (r);
+ }
+
+ tab_submit (tbl);
+ }
+
+
- draw_roc (rs, roc);
+ static void
+ output_roc (struct roc_state *rs, const struct cmd_roc *roc)
+ {
+ show_summary (roc);
+
+ if ( roc->curve )
-
++ {
++ struct roc_chart *rc;
++ size_t i;
++
++ rc = roc_chart_create (roc->reference);
++ for (i = 0; i < roc->n_vars; i++)
++ roc_chart_add_var (rc, var_get_name (roc->vars[i]),
++ rs[i].cutpoint_rdr);
++ chart_submit (roc_chart_get_chart (rc));
++ }
+
+ show_auc (rs, roc);
+
+ if ( roc->print_coords )
+ show_coords (rs, roc);
+ }
+
--- /dev/null
--- /dev/null
++/* PSPP - a program for statistical analysis.
++ Copyright (C) 2009 Free Software Foundation, Inc.
++
++ This program is free software: you can redistribute it and/or modify
++ it under the terms of the GNU General Public License as published by
++ the Free Software Foundation, either version 3 of the License, or
++ (at your option) any later version.
++
++ This program is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++ GNU General Public License for more details.
++
++ You should have received a copy of the GNU General Public License
++ along with this program. If not, see <http://www.gnu.org/licenses/>. */
++
++#ifndef LANGUAGE_STATS_ROC_H
++#define LANGUAGE_STATS_ROC_H 1
++
++/* These are case indexes into the cutpoint case readers for ROC
++ output, used by roc.c and roc-chart.c. */
++#define ROC_CUTPOINT 0
++#define ROC_TP 1
++#define ROC_FN 2
++#define ROC_TN 3
++#define ROC_FP 4
++
++#endif /* language/stats/roc.h */
src/output/afm.c \
src/output/afm.h \
src/output/ascii.c \
+ src/output/chart.c \
+ src/output/chart.h \
+ src/output/charts/box-whisker.c \
+ src/output/charts/box-whisker.h \
+ src/output/charts/cartesian.c \
+ src/output/charts/cartesian.h \
+ src/output/charts/np-plot.c \
+ src/output/charts/np-plot.h \
+ src/output/charts/piechart.c \
+ src/output/charts/piechart.h \
+ src/output/charts/plot-chart.c \
+ src/output/charts/plot-chart.h \
+ src/output/charts/plot-hist.c \
+ src/output/charts/plot-hist.h \
++ src/output/charts/roc-chart.c \
++ src/output/charts/roc-chart.h \
src/output/html.c \
src/output/htmlP.h \
src/output/journal.c \
--- /dev/null
- chart_geometry_free (x->cairo);
+/* PSPP - a program for statistical analysis.
+ Copyright (C) 2009 Free Software Foundation, Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#include <config.h>
+
+#include <output/cairo.h>
+
+#include <libpspp/assertion.h>
+#include <libpspp/start-date.h>
+#include <libpspp/version.h>
+#include <output/afm.h>
+#include <output/chart-provider.h>
+#include <output/manager.h>
+#include <output/output.h>
+
+#include <cairo/cairo-pdf.h>
+#include <cairo/cairo-ps.h>
+#include <cairo/cairo-svg.h>
+#include <cairo/cairo.h>
+#include <pango/pango-font.h>
+#include <pango/pango-layout.h>
+#include <pango/pango.h>
+#include <pango/pangocairo.h>
+#include <stdlib.h>
+
+#include "error.h"
+#include "intprops.h"
+#include "minmax.h"
+#include "xalloc.h"
+
+#include "gettext.h"
+#define _(msgid) gettext (msgid)
+
+/* Cairo driver options: (defaults listed first)
+
+ output-file="pspp.pdf"
+ output-type=pdf|ps|png|svg
+ paper-size=letter (see "papersize" file)
+ orientation=portrait|landscape
+ headers=on|off
+
+ left-margin=0.5in
+ right-margin=0.5in
+ top-margin=0.5in
+ bottom-margin=0.5in
+
+ prop-font=serif
+ emph-font=serif italic
+ fixed-font=monospace
+ font-size=10000
+
+ line-gutter=1pt
+ line-spacing=1pt
+ line-width=0.5pt
+ */
+
+/* Measurements as we present to the rest of PSPP. */
+#define XR_POINT PANGO_SCALE
+#define XR_INCH (XR_POINT * 72)
+
+/* Conversions to and from points. */
+static double
+xr_to_pt (int x)
+{
+ return x / (double) XR_POINT;
+}
+
+static int
+pt_to_xr (double x)
+{
+ return x * XR_POINT + 0.5;
+}
+
+/* Output types. */
+enum xr_output_type
+ {
+ XR_PDF,
+ XR_PS,
+ XR_SVG
+ };
+
+/* A font for use with Cairo. */
+struct xr_font
+ {
+ char *string;
+ PangoFontDescription *desc;
+ PangoLayout *layout;
+ PangoFontMetrics *metrics;
+ };
+
+/* Cairo output driver extension record. */
+struct xr_driver_ext
+ {
+ cairo_t *cairo;
+ struct xr_font fonts[OUTP_FONT_CNT];
+
+ bool draw_headers; /* Draw headers at top of page? */
+ int page_number; /* Current page number. */
+
+ int line_gutter; /* Space around lines. */
+ int line_space; /* Space between lines. */
+ int line_width; /* Width of lines. */
+ };
+
+struct xr_driver_options
+ {
+ struct outp_driver *driver;
+
+ char *file_name; /* Output file name. */
+ enum xr_output_type file_type; /* Type of output file. */
+
+
+ bool portrait; /* Portrait mode? */
+
+ int paper_width; /* Width of paper before dropping margins. */
+ int paper_length; /* Length of paper before dropping margins. */
+ int left_margin; /* Left margin in XR units. */
+ int right_margin; /* Right margin in XR units. */
+ int top_margin; /* Top margin in XR units. */
+ int bottom_margin; /* Bottom margin in XR units. */
+ };
+
+static bool handle_option (void *options, const char *key,
+ const struct string *val);
+static void draw_headers (struct outp_driver *this);
+
+static bool load_font (struct outp_driver *this, struct xr_font *);
+static void free_font (struct xr_font *);
+static int text_width (struct outp_driver *, const char *, enum outp_font);
+\f
+/* Driver initialization. */
+
+static struct outp_driver *
+xr_allocate (const char *name, int types)
+{
+ struct outp_driver *this;
+ struct xr_driver_ext *x;
+ size_t i;
+
+ this = outp_allocate_driver (&cairo_class, name, types);
+ this->width = this->length = 0;
+ this->font_height = XR_POINT * 10;
+ this->ext = x = xzalloc (sizeof *x);
+ x->cairo = NULL;
+ x->fonts[OUTP_FIXED].string = xstrdup ("monospace");
+ x->fonts[OUTP_PROPORTIONAL].string = xstrdup ("serif");
+ x->fonts[OUTP_EMPHASIS].string = xstrdup ("serif italic");
+ for (i = 0; i < OUTP_FONT_CNT; i++)
+ {
+ struct xr_font *font = &x->fonts[i];
+ font->desc = NULL;
+ font->metrics = NULL;
+ font->layout = NULL;
+ }
+ x->draw_headers = true;
+ x->page_number = 0;
+ x->line_gutter = XR_POINT;
+ x->line_space = XR_POINT;
+ x->line_width = XR_POINT / 2;
+
+ return this;
+}
+
+static bool
+xr_set_cairo (struct outp_driver *this, cairo_t *cairo)
+{
+ struct xr_driver_ext *x = this->ext;
+ int i;
+
+ x->cairo = cairo;
+
+ cairo_set_line_width (x->cairo, xr_to_pt (x->line_width));
+
+ for (i = 0; i < OUTP_FONT_CNT; i++)
+ if (!load_font (this, &x->fonts[i]))
+ return false;
+
+ this->fixed_width = text_width (this, "0", OUTP_FIXED);
+ this->prop_em_width = text_width (this, "0", OUTP_PROPORTIONAL);
+
+ this->horiz_line_width[OUTP_L_NONE] = 0;
+ this->horiz_line_width[OUTP_L_SINGLE] = 2 * x->line_gutter + x->line_width;
+ this->horiz_line_width[OUTP_L_DOUBLE] = (2 * x->line_gutter + x->line_space
+ + 2 * x->line_width);
+ memcpy (this->vert_line_width, this->horiz_line_width,
+ sizeof this->vert_line_width);
+
+ return true;
+}
+
+struct outp_driver *
+xr_create_driver (cairo_t *cairo)
+{
+ struct outp_driver *this;
+
+ this = xr_allocate ("cairo", 0);
+ this->width = INT_MAX / 8;
+ this->length = INT_MAX / 8;
+ if (!xr_set_cairo (this, cairo))
+ {
+ this->class->close_driver (this);
+ outp_free_driver (this);
+ return NULL;
+ }
+ return this;
+}
+
+static bool
+xr_open_driver (const char *name, int types, struct substring option_string)
+{
+ struct outp_driver *this;
+ struct xr_driver_ext *x;
+ struct xr_driver_options options;
+ cairo_surface_t *surface;
+ cairo_status_t status;
+ double width_pt, length_pt;
+
+ this = xr_allocate (name, types);
+ x = this->ext;
+
+ options.driver = this;
+ options.file_name = xstrdup ("pspp.pdf");
+ options.file_type = XR_PDF;
+ options.portrait = true;
+ outp_get_paper_size ("", &options.paper_width, &options.paper_length);
+ options.left_margin = XR_INCH / 2;
+ options.right_margin = XR_INCH / 2;
+ options.top_margin = XR_INCH / 2;
+ options.bottom_margin = XR_INCH / 2;
+
+ outp_parse_options (this->name, option_string, handle_option, &options);
+
+ width_pt = options.paper_width / 1000.0;
+ length_pt = options.paper_length / 1000.0;
+ if (options.portrait)
+ {
+ this->width = pt_to_xr (width_pt);
+ this->length = pt_to_xr (length_pt);
+ }
+ else
+ {
+ this->width = pt_to_xr (width_pt);
+ this->length = pt_to_xr (length_pt);
+ }
+ if (x->draw_headers)
+ options.top_margin += 3 * this->font_height;
+ this->width -= options.left_margin + options.right_margin;
+ this->length -= options.top_margin + options.bottom_margin;
+
+ if (options.file_type == XR_PDF)
+ surface = cairo_pdf_surface_create (options.file_name,
+ width_pt, length_pt);
+ else if (options.file_type == XR_PS)
+ surface = cairo_ps_surface_create (options.file_name, width_pt, length_pt);
+ else if (options.file_type == XR_SVG)
+ surface = cairo_svg_surface_create (options.file_name,
+ width_pt, length_pt);
+ else
+ NOT_REACHED ();
+
+ status = cairo_surface_status (surface);
+ if (status != CAIRO_STATUS_SUCCESS)
+ {
+ error (0, 0, _("opening output file \"%s\": %s"),
+ options.file_name, cairo_status_to_string (status));
+ cairo_surface_destroy (surface);
+ goto error;
+ }
+
+ x->cairo = cairo_create (surface);
+ cairo_surface_destroy (surface);
+
+ cairo_translate (x->cairo,
+ xr_to_pt (options.left_margin),
+ xr_to_pt (options.top_margin));
+
+ if (this->length / this->font_height < 15)
+ {
+ error (0, 0, _("The defined page is not long "
+ "enough to hold margins and headers, plus least 15 "
+ "lines of the default fonts. In fact, there's only "
+ "room for %d lines."),
+ this->length / this->font_height);
+ goto error;
+ }
+
+ if (!xr_set_cairo (this, x->cairo))
+ goto error;
+
+ outp_register_driver (this);
+ free (options.file_name);
+ return true;
+
+ error:
+ this->class->close_driver (this);
+ outp_free_driver (this);
+ free (options.file_name);
+ return false;
+}
+
+static bool
+xr_close_driver (struct outp_driver *this)
+{
+ struct xr_driver_ext *x = this->ext;
+ bool ok = true;
+ size_t i;
+
+ if (x->cairo != NULL)
+ {
+ cairo_status_t status;
+
+ cairo_surface_finish (cairo_get_target (x->cairo));
+ status = cairo_status (x->cairo);
+ if (status != CAIRO_STATUS_SUCCESS)
+ error (0, 0, _("error writing output file for %s driver: %s"),
+ this->name, cairo_status_to_string (status));
+ cairo_destroy (x->cairo);
+ }
+
+ for (i = 0; i < OUTP_FONT_CNT; i++)
+ free_font (&x->fonts[i]);
+ free (x);
+
+ return ok;
+}
+
+/* Generic option types. */
+enum
+{
+ output_file_arg,
+ output_type_arg,
+ paper_size_arg,
+ orientation_arg,
+ line_style_arg,
+ boolean_arg,
+ dimension_arg,
+ string_arg,
+ nonneg_int_arg
+};
+
+/* All the options that the Cairo driver supports. */
+static const struct outp_option option_tab[] =
+{
+ {"output-file", output_file_arg,0},
+ {"output-type", output_type_arg,0},
+ {"paper-size", paper_size_arg, 0},
+ {"orientation", orientation_arg,0},
+
+ {"headers", boolean_arg, 1},
+
+ {"prop-font", string_arg, OUTP_PROPORTIONAL},
+ {"emph-font", string_arg, OUTP_EMPHASIS},
+ {"fixed-font", string_arg, OUTP_FIXED},
+
+ {"left-margin", dimension_arg, 0},
+ {"right-margin", dimension_arg, 1},
+ {"top-margin", dimension_arg, 2},
+ {"bottom-margin", dimension_arg, 3},
+ {"font-size", dimension_arg, 4},
+ {"line-width", dimension_arg, 5},
+ {"line-gutter", dimension_arg, 6},
+ {"line-width", dimension_arg, 7},
+ {NULL, 0, 0},
+};
+
+static bool
+handle_option (void *options_, const char *key, const struct string *val)
+{
+ struct xr_driver_options *options = options_;
+ struct outp_driver *this = options->driver;
+ struct xr_driver_ext *x = this->ext;
+ int subcat;
+ char *value = ds_cstr (val);
+
+ switch (outp_match_keyword (key, option_tab, &subcat))
+ {
+ case -1:
+ error (0, 0,
+ _("unknown configuration parameter `%s' for %s device "
+ "driver"), key, this->class->name);
+ break;
+ case output_file_arg:
+ free (options->file_name);
+ options->file_name = xstrdup (value);
+ break;
+ case output_type_arg:
+ if (!strcmp (value, "pdf"))
+ options->file_type = XR_PDF;
+ else if (!strcmp (value, "ps"))
+ options->file_type = XR_PS;
+ else if (!strcmp (value, "svg"))
+ options->file_type = XR_SVG;
+ else
+ {
+ error (0, 0, _("unknown Cairo output type \"%s\""), value);
+ return false;
+ }
+ break;
+ case paper_size_arg:
+ outp_get_paper_size (value,
+ &options->paper_width, &options->paper_length);
+ break;
+ case orientation_arg:
+ if (!strcmp (value, "portrait"))
+ options->portrait = true;
+ else if (!strcmp (value, "landscape"))
+ options->portrait = false;
+ else
+ error (0, 0, _("unknown orientation `%s' (valid orientations are "
+ "`portrait' and `landscape')"), value);
+ break;
+ case boolean_arg:
+ if (!strcmp (value, "on") || !strcmp (value, "true")
+ || !strcmp (value, "yes") || atoi (value))
+ x->draw_headers = true;
+ else if (!strcmp (value, "off") || !strcmp (value, "false")
+ || !strcmp (value, "no") || !strcmp (value, "0"))
+ x->draw_headers = false;
+ else
+ {
+ error (0, 0, _("boolean value expected for %s"), key);
+ return false;
+ }
+ break;
+ case dimension_arg:
+ {
+ int dimension = outp_evaluate_dimension (value);
+
+ if (dimension <= 0)
+ break;
+ switch (subcat)
+ {
+ case 0:
+ options->left_margin = dimension;
+ break;
+ case 1:
+ options->right_margin = dimension;
+ break;
+ case 2:
+ options->top_margin = dimension;
+ break;
+ case 3:
+ options->bottom_margin = dimension;
+ break;
+ case 4:
+ this->font_height = dimension;
+ break;
+ case 5:
+ x->line_width = dimension;
+ break;
+ case 6:
+ x->line_gutter = dimension;
+ break;
+ case 7:
+ x->line_width = dimension;
+ break;
+ default:
+ NOT_REACHED ();
+ }
+ }
+ break;
+ case string_arg:
+ free (x->fonts[subcat].string);
+ x->fonts[subcat].string = ds_xstrdup (val);
+ break;
+ default:
+ NOT_REACHED ();
+ }
+
+ return true;
+}
+\f
+/* Basic file operations. */
+
+static void
+xr_open_page (struct outp_driver *this)
+{
+ struct xr_driver_ext *x = this->ext;
+
+ x->page_number++;
+
+ if (x->draw_headers)
+ draw_headers (this);
+}
+
+static void
+xr_close_page (struct outp_driver *this)
+{
+ struct xr_driver_ext *x = this->ext;
+ cairo_show_page (x->cairo);
+}
+
+static void
+xr_output_chart (struct outp_driver *this, const struct chart *chart)
+{
+ struct xr_driver_ext *x = this->ext;
+ struct chart_geometry geom;
+
+ outp_eject_page (this);
+ outp_open_page (this);
+
+ cairo_save (x->cairo);
+ cairo_translate (x->cairo, 0.0, xr_to_pt (this->length));
+ cairo_scale (x->cairo, 1.0, -1.0);
+ chart_geometry_init (x->cairo, &geom,
+ xr_to_pt (this->width), xr_to_pt (this->length));
+ chart_draw (chart, x->cairo, &geom);
++ chart_geometry_free (x->cairo, &geom);
+ cairo_restore (x->cairo);
+
+ outp_close_page (this);
+}
+\f
+/* Draws a line from (x0,y0) to (x1,y1). */
+static void
+dump_line (struct outp_driver *this, int x0, int y0, int x1, int y1)
+{
+ struct xr_driver_ext *x = this->ext;
+ cairo_new_path (x->cairo);
+ cairo_move_to (x->cairo, xr_to_pt (x0), xr_to_pt (y0));
+ cairo_line_to (x->cairo, xr_to_pt (x1), xr_to_pt (y1));
+ cairo_stroke (x->cairo);
+}
+
+/* Draws a horizontal line X0...X2 at Y if LEFT says so,
+ shortening it to X0...X1 if SHORTEN is true.
+ Draws a horizontal line X1...X3 at Y if RIGHT says so,
+ shortening it to X2...X3 if SHORTEN is true. */
+static void
+horz_line (struct outp_driver *this,
+ int x0, int x1, int x2, int x3, int y,
+ enum outp_line_style left, enum outp_line_style right,
+ bool shorten)
+{
+ if (left != OUTP_L_NONE && right != OUTP_L_NONE && !shorten)
+ dump_line (this, x0, y, x3, y);
+ else
+ {
+ if (left != OUTP_L_NONE)
+ dump_line (this, x0, y, shorten ? x1 : x2, y);
+ if (right != OUTP_L_NONE)
+ dump_line (this, shorten ? x2 : x1, y, x3, y);
+ }
+}
+
+/* Draws a vertical line Y0...Y2 at X if TOP says so,
+ shortening it to Y0...Y1 if SHORTEN is true.
+ Draws a vertical line Y1...Y3 at X if BOTTOM says so,
+ shortening it to Y2...Y3 if SHORTEN is true. */
+static void
+vert_line (struct outp_driver *this,
+ int y0, int y1, int y2, int y3, int x,
+ enum outp_line_style top, enum outp_line_style bottom,
+ bool shorten)
+{
+ if (top != OUTP_L_NONE && bottom != OUTP_L_NONE && !shorten)
+ dump_line (this, x, y0, x, y3);
+ else
+ {
+ if (top != OUTP_L_NONE)
+ dump_line (this, x, y0, x, shorten ? y1 : y2);
+ if (bottom != OUTP_L_NONE)
+ dump_line (this, x, shorten ? y2 : y1, x, y3);
+ }
+}
+
+/* Draws a generalized intersection of lines in the rectangle
+ (X0,Y0)-(X3,Y3). The line coming from the top to the center
+ is of style TOP, from left to center of style LEFT, from
+ bottom to center of style BOTTOM, and from right to center of
+ style RIGHT. */
+static void
+xr_line (struct outp_driver *this,
+ int x0, int y0, int x3, int y3,
+ enum outp_line_style top, enum outp_line_style left,
+ enum outp_line_style bottom, enum outp_line_style right)
+{
+ /* The algorithm here is somewhat subtle, to allow it to handle
+ all the kinds of intersections that we need.
+
+ Three additional ordinates are assigned along the x axis. The
+ first is xc, midway between x0 and x3. The others are x1 and
+ x2; for a single vertical line these are equal to xc, and for
+ a double vertical line they are the ordinates of the left and
+ right half of the double line.
+
+ yc, y1, and y2 are assigned similarly along the y axis.
+
+ The following diagram shows the coordinate system and output
+ for double top and bottom lines, single left line, and no
+ right line:
+
+ x0 x1 xc x2 x3
+ y0 ________________________
+ | # # |
+ | # # |
+ | # # |
+ | # # |
+ | # # |
+ y1 = y2 = yc |######### # |
+ | # # |
+ | # # |
+ | # # |
+ | # # |
+ y3 |________#_____#_______|
+ */
+ struct xr_driver_ext *ext = this->ext;
+
+ /* Offset from center of each line in a pair of double lines. */
+ int double_line_ofs = (ext->line_space + ext->line_width) / 2;
+
+ /* Are the lines along each axis single or double?
+ (It doesn't make sense to have different kinds of line on the
+ same axis, so we don't try to gracefully handle that case.) */
+ bool double_vert = top == OUTP_L_DOUBLE || bottom == OUTP_L_DOUBLE;
+ bool double_horz = left == OUTP_L_DOUBLE || right == OUTP_L_DOUBLE;
+
+ /* When horizontal lines are doubled,
+ the left-side line along y1 normally runs from x0 to x2,
+ and the right-side line along y1 from x3 to x1.
+ If the top-side line is also doubled, we shorten the y1 lines,
+ so that the left-side line runs only to x1,
+ and the right-side line only to x2.
+ Otherwise, the horizontal line at y = y1 below would cut off
+ the intersection, which looks ugly:
+ x0 x1 x2 x3
+ y0 ________________________
+ | # # |
+ | # # |
+ | # # |
+ | # # |
+ y1 |######### ########|
+ | |
+ | |
+ y2 |######################|
+ | |
+ | |
+ y3 |______________________|
+ It is more of a judgment call when the horizontal line is
+ single. We actually choose to cut off the line anyhow, as
+ shown in the first diagram above.
+ */
+ bool shorten_y1_lines = top == OUTP_L_DOUBLE;
+ bool shorten_y2_lines = bottom == OUTP_L_DOUBLE;
+ bool shorten_yc_line = shorten_y1_lines && shorten_y2_lines;
+ int horz_line_ofs = double_vert ? double_line_ofs : 0;
+ int xc = (x0 + x3) / 2;
+ int x1 = xc - horz_line_ofs;
+ int x2 = xc + horz_line_ofs;
+
+ bool shorten_x1_lines = left == OUTP_L_DOUBLE;
+ bool shorten_x2_lines = right == OUTP_L_DOUBLE;
+ bool shorten_xc_line = shorten_x1_lines && shorten_x2_lines;
+ int vert_line_ofs = double_horz ? double_line_ofs : 0;
+ int yc = (y0 + y3) / 2;
+ int y1 = yc - vert_line_ofs;
+ int y2 = yc + vert_line_ofs;
+
+ if (!double_horz)
+ horz_line (this, x0, x1, x2, x3, yc, left, right, shorten_yc_line);
+ else
+ {
+ horz_line (this, x0, x1, x2, x3, y1, left, right, shorten_y1_lines);
+ horz_line (this, x0, x1, x2, x3, y2, left, right, shorten_y2_lines);
+ }
+
+ if (!double_vert)
+ vert_line (this, y0, y1, y2, y3, xc, top, bottom, shorten_xc_line);
+ else
+ {
+ vert_line (this, y0, y1, y2, y3, x1, top, bottom, shorten_x1_lines);
+ vert_line (this, y0, y1, y2, y3, x2, top, bottom, shorten_x2_lines);
+ }
+}
+
+/* Writes STRING at location (X,Y) trimmed to the given MAX_WIDTH
+ and with the given JUSTIFICATION for THIS driver. */
+static int
+draw_text (struct outp_driver *this,
+ const char *string, int x, int y, int max_width,
+ enum outp_justification justification)
+{
+ struct outp_text text;
+ int width;
+
+ text.font = OUTP_PROPORTIONAL;
+ text.justification = justification;
+ text.string = ss_cstr (string);
+ text.h = max_width;
+ text.v = this->font_height;
+ text.x = x;
+ text.y = y;
+ this->class->text_metrics (this, &text, &width, NULL);
+ this->class->text_draw (this, &text);
+ return width;
+}
+
+/* Writes STRING at location (X,Y) trimmed to the given MAX_WIDTH
+ and with the given JUSTIFICATION for THIS driver. */
+static int
+text_width (struct outp_driver *this, const char *string, enum outp_font font)
+{
+ struct outp_text text;
+ int width;
+
+ text.font = font;
+ text.justification = OUTP_LEFT;
+ text.string = ss_cstr (string);
+ text.h = INT_MAX;
+ text.v = this->font_height;
+ text.x = 0;
+ text.y = 0;
+ this->class->text_metrics (this, &text, &width, NULL);
+ return width;
+}
+
+/* Writes LEFT left-justified and RIGHT right-justified within
+ (X0...X1) at Y. LEFT or RIGHT or both may be null. */
+static void
+draw_header_line (struct outp_driver *this,
+ const char *left, const char *right,
+ int x0, int x1, int y)
+{
+ int right_width = 0;
+ if (right != NULL)
+ right_width = (draw_text (this, right, x0, y, x1 - x0, OUTP_RIGHT)
+ + this->prop_em_width);
+ if (left != NULL)
+ draw_text (this, left, x0, y, x1 - x0 - right_width, OUTP_LEFT);
+}
+
+/* Draw top of page headers for THIS driver. */
+static void
+draw_headers (struct outp_driver *this)
+{
+ struct xr_driver_ext *ext = this->ext;
+ char *r1, *r2;
+ int x0, x1;
+ int y;
+
+ y = -3 * this->font_height;
+ x0 = this->prop_em_width;
+ x1 = this->width - this->prop_em_width;
+
+ /* Draw box. */
+ cairo_rectangle (ext->cairo, 0, xr_to_pt (y), xr_to_pt (this->width),
+ xr_to_pt (2 * (this->font_height
+ + ext->line_width + ext->line_gutter)));
+ cairo_save (ext->cairo);
+ cairo_set_source_rgb (ext->cairo, 0.9, 0.9, 0.9);
+ cairo_fill_preserve (ext->cairo);
+ cairo_restore (ext->cairo);
+ cairo_stroke (ext->cairo);
+
+ y += ext->line_width + ext->line_gutter;
+
+ r1 = xasprintf (_("%s - Page %d"), get_start_date (), ext->page_number);
+ r2 = xasprintf ("%s - %s", version, host_system);
+
+ draw_header_line (this, outp_title, r1, x0, x1, y);
+ y += this->font_height;
+
+ draw_header_line (this, outp_subtitle, r2, x0, x1, y);
+
+ free (r1);
+ free (r2);
+}
+\f
+/* Format TEXT on THIS driver.
+ If DRAW is nonzero, draw the text.
+ The width of the widest line is stored into *WIDTH, if WIDTH
+ is nonnull.
+ The total height of the text written is stored into *HEIGHT,
+ if HEIGHT is nonnull. */
+static void
+text (struct outp_driver *this, const struct outp_text *text, bool draw,
+ int *width, int *height)
+{
+ struct xr_driver_ext *ext = this->ext;
+ struct xr_font *font = &ext->fonts[text->font];
+
+ pango_layout_set_text (font->layout,
+ text->string.string, text->string.length);
+ pango_layout_set_alignment (
+ font->layout,
+ (text->justification == OUTP_RIGHT ? PANGO_ALIGN_RIGHT
+ : text->justification == OUTP_LEFT ? PANGO_ALIGN_LEFT
+ : PANGO_ALIGN_CENTER));
+ pango_layout_set_width (font->layout, text->h == INT_MAX ? -1 : text->h);
+ pango_layout_set_wrap (font->layout, PANGO_WRAP_WORD_CHAR);
+ /* XXX need to limit number of lines to those that fit in text->v. */
+
+ if (draw)
+ {
+ int x = text->x;
+ if (text->justification != OUTP_LEFT && text->h != INT_MAX)
+ {
+ int w, h, excess;
+ pango_layout_get_size (font->layout, &w, &h);
+ excess = text->h - w;
+ if (excess > 0)
+ {
+ if (text->justification == OUTP_CENTER)
+ x += excess / 2;
+ else
+ x += excess;
+ }
+ }
+ cairo_save (ext->cairo);
+ cairo_translate (ext->cairo, xr_to_pt (text->x), xr_to_pt (text->y));
+ pango_cairo_show_layout (ext->cairo, font->layout);
+ cairo_restore (ext->cairo);
+ }
+
+ if (width != NULL || height != NULL)
+ {
+ int w, h;
+ pango_layout_get_size (font->layout, &w, &h);
+ if (width != NULL)
+ *width = w;
+ if (height != NULL)
+ *height = h;
+ }
+}
+
+static void
+xr_text_metrics (struct outp_driver *this, const struct outp_text *t,
+ int *width, int *height)
+{
+ text (this, t, false, width, height);
+}
+
+static void
+xr_text_draw (struct outp_driver *this, const struct outp_text *t)
+{
+ text (this, t, true, NULL, NULL);
+}
+\f
+/* Attempts to load FONT, initializing its other members based on
+ its 'string' member and the information in THIS. Returns true
+ if successful, otherwise false. */
+static bool
+load_font (struct outp_driver *this, struct xr_font *font)
+{
+ struct xr_driver_ext *x = this->ext;
+ PangoContext *context;
+ PangoLanguage *language;
+
+ font->desc = pango_font_description_from_string (font->string);
+ if (font->desc == NULL)
+ {
+ error (0, 0, _("\"%s\": bad font specification"), font->string);
+ return false;
+ }
+ pango_font_description_set_absolute_size (font->desc, this->font_height);
+
+ font->layout = pango_cairo_create_layout (x->cairo);
+ pango_layout_set_font_description (font->layout, font->desc);
+
+ language = pango_language_get_default ();
+ context = pango_layout_get_context (font->layout);
+ font->metrics = pango_context_get_metrics (context, font->desc, language);
+
+ return true;
+}
+
+/* Frees FONT. */
+static void
+free_font (struct xr_font *font)
+{
+ free (font->string);
+ if (font->desc != NULL)
+ pango_font_description_free (font->desc);
+ pango_font_metrics_unref (font->metrics);
+ g_object_unref (font->layout);
+}
+
+/* Cairo driver class. */
+const struct outp_class cairo_class =
+{
+ "cairo",
+ 0,
+
+ xr_open_driver,
+ xr_close_driver,
+
+ xr_open_page,
+ xr_close_page,
+ NULL,
+
+ xr_output_chart,
+
+ NULL,
+
+ xr_line,
+ xr_text_metrics,
+ xr_text_draw,
+};
--- /dev/null
- void chart_geometry_free (cairo_t *);
+/* PSPP - a program for statistical analysis.
+ Copyright (C) 2004, 2009 Free Software Foundation, Inc.
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>. */
+
+#ifndef OUTPUT_CHART_PROVIDER_H
+#define OUTPUT_CHART_PROVIDER_H 1
+
+#include <cairo/cairo.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <output/chart.h>
+
+struct chart_colour
+ {
+ uint8_t red;
+ uint8_t green;
+ uint8_t blue;
+ };
+
+/* The geometry of a chart. */
+struct chart_geometry
+ {
+ int data_top ;
+ int data_right ;
+ int data_bottom;
+ int data_left ;
+
+ int abscissa_top;
+
+ int ordinate_right ;
+
+ int title_bottom ;
+
++ /* Legend. */
+ int legend_left ;
+ int legend_right ;
++ const char **dataset;
++ int n_datasets;
+
+ /* Default font size for the plot. */
+ double font_size;
+
+ struct chart_colour fill_colour;
+
+ /* Stuff Particular to Cartesians (and Boxplots ) */
+ double ordinate_scale;
+ double abscissa_scale;
+ double x_min;
+ double x_max;
+ double y_min;
+ double y_max;
++ bool in_path;
+ };
+
+struct chart_class
+ {
+ void (*draw) (const struct chart *, cairo_t *, struct chart_geometry *);
+ void (*destroy) (struct chart *);
+ };
+
+struct chart
+ {
+ const struct chart_class *class;
+ int ref_cnt;
+ };
+
+void chart_init (struct chart *, const struct chart_class *);
+
+void chart_geometry_init (cairo_t *, struct chart_geometry *,
+ double width, double length);
++void chart_geometry_free (cairo_t *, struct chart_geometry *);
+
+void chart_draw (const struct chart *, cairo_t *, struct chart_geometry *);
+char *chart_draw_png (const struct chart *, const char *file_name_template,
+ int number);
+
+#endif /* output/chart-provider.h */
extern struct som_table_class tab_table_class;
-struct chart *
-chart_create(void)
+void
+chart_init (struct chart *chart, const struct chart_class *class)
{
- struct chart *chart;
- struct outp_driver *d;
-
- d = outp_drivers (NULL);
- if (d == NULL)
- return NULL;
-
- chart = xmalloc (sizeof *chart);
- chart->lp = NULL;
- d->class->initialise_chart(d, chart);
- if (!chart->lp)
- {
- free (chart);
- return NULL;
- }
-
- if (pl_openpl_r (chart->lp) < 0) /* open Plotter */
- return NULL;
-
- pl_fspace_r (chart->lp, 0.0, 0.0, 1000.0, 1000.0); /* set coordinate system */
- pl_flinewidth_r (chart->lp, 0.25); /* set line thickness */
- pl_pencolorname_r (chart->lp, "black");
-
- pl_erase_r (chart->lp); /* erase graphics display */
- pl_filltype_r(chart->lp,0);
-
- pl_savestate_r(chart->lp);
-
- /* Set default chartetry */
- chart->data_top = 900;
- chart->data_right = 800;
- chart->data_bottom = 120;
- chart->data_left = 150;
- chart->abscissa_top = 70;
- chart->ordinate_right = 120;
- chart->title_bottom = 920;
- chart->legend_left = 810;
- chart->legend_right = 1000;
- chart->font_size = 0;
- chart->in_path = false;
- chart->dataset = NULL;
- chart->n_datasets = 0;
- strcpy(chart->fill_colour,"red");
-
- /* Get default font size */
- if ( !chart->font_size)
- chart->font_size = pl_fontsize_r(chart->lp, -1);
-
- /* Draw the data area */
- pl_box_r(chart->lp,
- chart->data_left, chart->data_bottom,
- chart->data_right, chart->data_top);
+ chart->class = class;
+ chart->ref_cnt = 1;
+}
- return chart;
+void
+chart_geometry_init (cairo_t *cr, struct chart_geometry *geom,
+ double width, double length)
+{
+ /* Set default chartetry. */
+ geom->data_top = 0.900 * length;
+ geom->data_right = 0.800 * width;
+ geom->data_bottom = 0.120 * length;
+ geom->data_left = 0.150 * width;
+ geom->abscissa_top = 0.070 * length;
+ geom->ordinate_right = 0.120 * width;
+ geom->title_bottom = 0.920 * length;
+ geom->legend_left = 0.810 * width;
+ geom->legend_right = width;
+ geom->font_size = 15.0;
++ geom->in_path = false;
++ geom->dataset = NULL;
++ geom->n_datasets = 0;
+
+ geom->fill_colour.red = 255;
+ geom->fill_colour.green = 0;
+ geom->fill_colour.blue = 0;
+
+ cairo_set_line_width (cr, 1.0);
+
+ cairo_rectangle (cr, geom->data_left, geom->data_bottom,
+ geom->data_right - geom->data_left,
+ geom->data_top - geom->data_bottom);
+ cairo_stroke (cr);
}
void
- chart_geometry_free (cairo_t *cr UNUSED)
-chart_submit(struct chart *chart)
++chart_geometry_free (cairo_t *cr UNUSED, struct chart_geometry *geom)
{
- struct som_entity s;
- struct outp_driver *d;
-
- if ( ! chart )
- return ;
-
- pl_restorestate_r(chart->lp);
-
- s.class = &tab_table_class;
- s.ext = chart;
- s.type = SOM_CHART;
- som_submit (&s);
+ int i;
- if (pl_closepl_r (chart->lp) < 0) /* close Plotter */
- {
- fprintf (stderr, "Couldn't close Plotter\n");
- }
-
- pl_deletepl_r(chart->lp);
+
++ for (i = 0 ; i < geom->n_datasets; ++i)
++ free (geom->dataset[i]);
++ free (geom->dataset);
+}
- pl_deleteplparams(chart->pl_params);
+void
+chart_draw (const struct chart *chart, cairo_t *cr,
+ struct chart_geometry *geom)
+{
+ chart->class->draw (chart, cr, geom);
+}
- d = outp_drivers (NULL);
- d->class->finalise_chart(d, chart);
+char *
+chart_draw_png (const struct chart *chart, const char *file_name_template,
+ int number)
+{
+ const int width = 640;
+ const int length = 480;
+
+ struct chart_geometry geom;
+ cairo_surface_t *surface;
+ cairo_status_t status;
+ const char *number_pos;
+ char *file_name;
+ cairo_t *cr;
+
+ number_pos = strchr (file_name_template, '#');
+ if (number_pos != NULL)
+ file_name = xasprintf ("%.*s%d%s", (int) (number_pos - file_name_template),
+ file_name_template, number, number_pos + 1);
+ else
+ file_name = xstrdup (file_name_template);
+
+ surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, width, length);
+ cr = cairo_create (surface);
+
+ cairo_translate (cr, 0.0, length);
+ cairo_scale (cr, 1.0, -1.0);
+
+ cairo_save (cr);
+ cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
+ cairo_rectangle (cr, 0, 0, width, length);
+ cairo_fill (cr);
+ cairo_restore (cr);
+
+ cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
+
+ chart_geometry_init (cr, &geom, width, length);
+ chart_draw (chart, cr, &geom);
- chart_geometry_free (cr);
++ chart_geometry_free (cr, &geom);
+
+ status = cairo_surface_write_to_png (surface, file_name);
+ if (status != CAIRO_STATUS_SUCCESS)
+ error (0, 0, _("writing output file \"%s\": %s"),
+ file_name, cairo_status_to_string (status));
+
+ cairo_destroy (cr);
+ cairo_surface_destroy (surface);
+
+ return file_name;
+}
- for (i = 0 ; i < chart->n_datasets; ++i)
- free (chart->dataset[i]);
- free (chart->dataset);
- free(chart);
+struct chart *
+chart_ref (const struct chart *chart_)
+{
+ struct chart *chart = CONST_CAST (struct chart *, chart_);
+ chart->ref_cnt++;
+ return chart;
}
void
#include <assert.h>
#include <output/chart.h>
-
+#include <output/chart-provider.h>
#include <output/charts/plot-chart.h>
-#include <output/charts/cartesian.h>
#include <libpspp/compiler.h>
- struct dataset
++#include "xalloc.h"
+
+ /* Start a new vector called NAME */
+ void
-chart_vector_start (struct chart *ch, const char *name)
++chart_vector_start (cairo_t *cr, struct chart_geometry *geom, const char *name)
{
- int n_data;
- const char *label;
- };
- if ( ! ch )
- return ;
++ const struct chart_colour *colour;
- pl_savestate_r (ch->lp);
++ cairo_save (cr);
- #define DATASETS 2
- pl_colorname_r (ch->lp, data_colour [ch->n_datasets % N_CHART_COLOURS]);
++ colour = &data_colour[geom->n_datasets % N_CHART_COLOURS];
++ cairo_set_source_rgb (cr,
++ colour->red / 255.0,
++ colour->green / 255.0,
++ colour->blue / 255.0);
- static const struct dataset dataset[DATASETS] =
- {
- { 13, "male"},
- { 11, "female"},
- };
- ch->n_datasets++;
- ch->dataset = xrealloc (ch->dataset, ch->n_datasets * sizeof (*ch->dataset));
++ geom->n_datasets++;
++ geom->dataset = xrealloc (geom->dataset,
++ geom->n_datasets * sizeof (*geom->dataset));
- ch->dataset[ch->n_datasets - 1] = strdup (name);
++ geom->dataset[geom->n_datasets - 1] = strdup (name);
+ }
/* Plot a data point */
void
-chart_datum (struct chart *ch, int dataset UNUSED, double x, double y)
+chart_datum (cairo_t *cr, const struct chart_geometry *geom,
+ int dataset UNUSED, double x, double y)
{
- if ( ! ch )
- return ;
-
- {
- const double x_pos =
- (x - ch->x_min) * ch->abscissa_scale + ch->data_left ;
+ double x_pos = (x - geom->x_min) * geom->abscissa_scale + geom->data_left;
+ double y_pos = (y - geom->y_min) * geom->ordinate_scale + geom->data_bottom;
- const double y_pos =
- (y - ch->y_min) * ch->ordinate_scale + ch->data_bottom ;
-
- pl_savestate_r(ch->lp);
-
- pl_fmarker_r(ch->lp, x_pos, y_pos, 6, 15);
-
- pl_restorestate_r(ch->lp);
- }
+ chart_draw_marker (cr, x_pos, y_pos, MARKER_SQUARE, 15);
}
-chart_vector_end (struct chart *ch)
+ void
- pl_endpath_r (ch->lp);
- pl_colorname_r (ch->lp, "black");
- ch->in_path = false;
- pl_restorestate_r (ch->lp);
++chart_vector_end (cairo_t *cr, struct chart_geometry *geom)
+ {
-chart_vector (struct chart *ch, double x, double y)
++ cairo_stroke (cr);
++ cairo_restore (cr);
++ geom->in_path = false;
+ }
+
+ /* Plot a data point */
+ void
- if ( ! ch )
- return ;
-
- {
- const double x_pos =
- (x - ch->x_min) * ch->abscissa_scale + ch->data_left ;
-
- const double y_pos =
- (y - ch->y_min) * ch->ordinate_scale + ch->data_bottom ;
-
- if ( ch->in_path)
- pl_fcont_r (ch->lp, x_pos, y_pos);
- else
- {
- pl_fmove_r (ch->lp, x_pos, y_pos);
- ch->in_path = true;
- }
- }
++chart_vector (cairo_t *cr, struct chart_geometry *geom, double x, double y)
+ {
++ const double x_pos =
++ (x - geom->x_min) * geom->abscissa_scale + geom->data_left ;
++
++ const double y_pos =
++ (y - geom->y_min) * geom->ordinate_scale + geom->data_bottom ;
++
++ if (geom->in_path)
++ cairo_line_to (cr, x_pos, y_pos);
++ else
++ {
++ cairo_move_to (cr, x_pos, y_pos);
++ geom->in_path = true;
++ }
+ }
+
+
+
/* Draw a line with slope SLOPE and intercept INTERCEPT.
between the points limit1 and limit2.
If lim_dim is CHART_DIM_Y then the limit{1,2} are on the
CHART_DIM_Y
};
+struct chart_geometry;
-void chart_vector_start (struct chart *ch, const char *name);
-void chart_vector (struct chart *ch, double x, double y);
-void chart_vector_end (struct chart *ch);
++void chart_vector_start (cairo_t *, struct chart_geometry *,
++ const char *name);
++void chart_vector_end (cairo_t *, struct chart_geometry *);
++void chart_vector (cairo_t *, struct chart_geometry *, double x, double y);
+
/* Plot a data point */
-void chart_datum (struct chart *ch, int dataset UNUSED, double x, double y);
+void chart_datum(cairo_t *, const struct chart_geometry *,
+ int dataset UNUSED, double x, double y);
/* Draw a line with slope SLOPE and intercept INTERCEPT.
between the points limit1 and limit2.
double y;
const double tick_interval =
- chart_rounded_tick( (smax - smin) / (double) ticks);
+ chart_rounded_tick ((smax - smin) / (double) ticks);
- if ( !ch )
- return;
+ geom->y_max = ceil (smax / tick_interval) * tick_interval;
+ geom->y_min = floor (smin / tick_interval) * tick_interval;
- ch->y_max = ceil ( smax / tick_interval ) * tick_interval ;
- ch->y_min = floor ( smin / tick_interval ) * tick_interval ;
+ geom->ordinate_scale =
+ (fabs (geom->data_top - geom->data_bottom)
+ / fabs (geom->y_max - geom->y_min));
- ch->ordinate_scale =
- fabs(ch->data_top - ch->data_bottom) / fabs(ch->y_max - ch->y_min) ;
-
- for(y = ch->y_min ; y <= ch->y_max; y += tick_interval )
- {
- draw_tick (ch, TICK_ORDINATE,
- (y - ch->y_min) * ch->ordinate_scale, "%g", y);
- }
+ for (y = geom->y_min; y <= geom->y_max; y += tick_interval)
+ draw_tick (cr, geom, TICK_ORDINATE,
+ (y - geom->y_min) * geom->ordinate_scale, "%g", y);
}
-
/* Write the abscissa label */
void
-chart_write_xlabel(struct chart *ch, const char *label)
+chart_write_xlabel (cairo_t *cr, const struct chart_geometry *geom,
+ const char *label)
{
- if ( ! ch )
- return ;
-
- pl_savestate_r(ch->lp);
-
- pl_move_r(ch->lp,ch->data_left, ch->abscissa_top);
- pl_alabel_r(ch->lp,0,'t',label);
-
- pl_restorestate_r(ch->lp);
-
+ cairo_move_to (cr, geom->data_left, geom->abscissa_top);
+ chart_label (cr, 'l', 't', geom->font_size, label);
}
-
-
/* Write the ordinate label */
void
-chart_write_ylabel(struct chart *ch, const char *label)
+chart_write_ylabel (cairo_t *cr, const struct chart_geometry *geom,
+ const char *label)
{
- if ( ! ch )
- return ;
-
- pl_savestate_r(ch->lp);
-
- pl_move_r(ch->lp, ch->data_bottom, ch->ordinate_right);
- pl_textangle_r(ch->lp, 90);
- pl_alabel_r(ch->lp, 0, 0, label);
-
- pl_restorestate_r(ch->lp);
+ cairo_save (cr);
+ cairo_translate (cr, -geom->data_bottom, -geom->ordinate_right);
+ cairo_move_to (cr, 0, 0);
+ cairo_rotate (cr, M_PI / 2.0);
+ chart_label (cr, 'l', 'x', geom->font_size, label);
+ cairo_restore (cr);
}
-chart_write_legend (struct chart *ch)
+
+
+ void
- const int vstep = ch->font_size * 2;
++chart_write_legend (cairo_t *cr, const struct chart_geometry *geom)
+ {
+ int i;
- const int legend_top = ch->data_top;
++ const int vstep = geom->font_size * 2;
+ const int xpad = 10;
+ const int ypad = 10;
+ const int swatch = 20;
- (vstep * ch->n_datasets + 2 * ypad );
++ const int legend_top = geom->data_top;
+ const int legend_bottom = legend_top -
- if ( ! ch )
- return ;
++ (vstep * geom->n_datasets + 2 * ypad );
+
- pl_savestate_r (ch->lp);
++ cairo_save (cr);
+
- pl_box_r (ch->lp, ch->legend_left, legend_top,
- ch->legend_right - xpad, legend_bottom);
-
- for (i = 0 ; i < ch->n_datasets ; ++i )
++ cairo_rectangle (cr, geom->legend_left, legend_top,
++ geom->legend_right - xpad - geom->legend_left,
++ legend_bottom - legend_top);
++ cairo_stroke (cr);
+
- const int ypos = vstep * (i + 1);
- const int xpos = ch->legend_left + xpad;
- pl_move_r (ch->lp, xpos, legend_top - ypos);
-
- pl_savestate_r(ch->lp);
- pl_fillcolorname_r (ch->lp, data_colour [ i % N_CHART_COLOURS]);
-
- pl_pentype_r (ch->lp, 1);
- pl_filltype_r (ch->lp, 1);
- pl_boxrel_r (ch->lp, 0, 0, swatch, swatch);
-
-
- pl_moverel_r (ch->lp, swatch, 0);
- pl_alabel_r (ch->lp, 0, 0, ch->dataset[i]);
-
- pl_restorestate_r (ch->lp);
++ for (i = 0 ; i < geom->n_datasets ; ++i )
+ {
- pl_restorestate_r (ch->lp);
++ const int ypos = legend_top - vstep * (i + 1);
++ const int xpos = geom->legend_left + xpad;
++ const struct chart_colour *colour;
++
++ cairo_move_to (cr, xpos, ypos);
++
++ cairo_save (cr);
++ colour = &data_colour [ i % N_CHART_COLOURS];
++ cairo_set_source_rgb (cr,
++ colour->red / 255.0,
++ colour->green / 255.0,
++ colour->blue / 255.0);
++ cairo_rectangle (cr, xpos, ypos, swatch, swatch);
++ cairo_fill_preserve (cr);
++ cairo_stroke (cr);
++ cairo_restore (cr);
++
++ cairo_move_to (cr, xpos + swatch * 1.5, ypos);
++ chart_label (cr, 'l', 'x', geom->font_size, geom->dataset[i]);
+ }
+
++ cairo_restore (cr);
+ }
/* Set the scale for the ordinate */
-void chart_write_yscale(struct chart *ch, double smin, double smax, int ticks);
+void chart_write_yscale(cairo_t *, struct chart_geometry *,
+ double smin, double smax, int ticks);
-void chart_write_xlabel(struct chart *ch, const char *label) ;
+void chart_write_xlabel(cairo_t *, const struct chart_geometry *,
+ const char *label) ;
/* Write the ordinate label */
-void chart_write_ylabel(struct chart *ch, const char *label);
+void chart_write_ylabel(cairo_t *, const struct chart_geometry *,
+ const char *label);
-void chart_write_legend (struct chart *ch);
++void chart_write_legend (cairo_t *, const struct chart_geometry *);
+
#endif
--- /dev/null
--- /dev/null
++/* PSPP - a program for statistical analysis.
++ Copyright (C) 2009 Free Software Foundation, Inc.
++
++ This program is free software: you can redistribute it and/or modify
++ it under the terms of the GNU General Public License as published by
++ the Free Software Foundation, either version 3 of the License, or
++ (at your option) any later version.
++
++ This program is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++ GNU General Public License for more details.
++
++ You should have received a copy of the GNU General Public License
++ along with this program. If not, see <http://www.gnu.org/licenses/>. */
++
++#include <config.h>
++
++#include <output/charts/roc-chart.h>
++
++#include <output/chart-provider.h>
++#include <output/charts/cartesian.h>
++#include <output/charts/plot-chart.h>
++#include <data/casereader.h>
++#include <language/stats/roc.h>
++
++#include "xalloc.h"
++
++#include "gettext.h"
++#define _(msgid) gettext (msgid)
++
++struct roc_var
++ {
++ char *name;
++ struct casereader *cutpoint_reader;
++ };
++
++struct roc_chart
++ {
++ struct chart chart;
++ bool reference;
++ struct roc_var *vars;
++ size_t n_vars;
++ size_t allocated_vars;
++ };
++
++static const struct chart_class roc_chart_class;
++
++struct roc_chart *
++roc_chart_create (bool reference)
++{
++ struct roc_chart *rc = xmalloc (sizeof *rc);
++ chart_init (&rc->chart, &roc_chart_class);
++ rc->reference = reference;
++ rc->vars = NULL;
++ rc->n_vars = 0;
++ rc->allocated_vars = 0;
++ return rc;
++}
++
++void
++roc_chart_add_var (struct roc_chart *rc, const char *var_name,
++ const struct casereader *cutpoint_reader)
++{
++ struct roc_var *rv;
++
++ if (rc->n_vars >= rc->allocated_vars)
++ rc->vars = x2nrealloc (rc->vars, &rc->allocated_vars, sizeof *rc->vars);
++
++ rv = &rc->vars[rc->n_vars++];
++ rv->name = xstrdup (var_name);
++ rv->cutpoint_reader = casereader_clone (cutpoint_reader);
++}
++
++struct chart *
++roc_chart_get_chart (struct roc_chart *rc)
++{
++ return &rc->chart;
++}
++
++static void
++roc_chart_draw (const struct chart *chart, cairo_t *cr,
++ struct chart_geometry *geom)
++{
++ const struct roc_chart *rc = UP_CAST (chart, struct roc_chart, chart);
++ size_t i;
++
++ chart_write_title (cr, geom, _("ROC Curve"));
++ chart_write_xlabel (cr, geom, _("1 - Specificity"));
++ chart_write_ylabel (cr, geom, _("Sensitivity"));
++
++ chart_write_xscale (cr, geom, 0, 1, 5);
++ chart_write_yscale (cr, geom, 0, 1, 5);
++
++ if ( rc->reference )
++ {
++ chart_line (cr, geom, 1.0, 0,
++ 0.0, 1.0,
++ CHART_DIM_X);
++ }
++
++ for (i = 0; i < rc->n_vars; ++i)
++ {
++ const struct roc_var *rv = &rc->vars[i];
++ struct casereader *r = casereader_clone (rv->cutpoint_reader);
++ struct ccase *cc;
++
++ chart_vector_start (cr, geom, rv->name);
++ for (; (cc = casereader_read (r)) != NULL; case_unref (cc))
++ {
++ double se = case_data_idx (cc, ROC_TP)->f;
++ double sp = case_data_idx (cc, ROC_TN)->f;
++
++ se /= case_data_idx (cc, ROC_FN)->f + case_data_idx (cc, ROC_TP)->f ;
++ sp /= case_data_idx (cc, ROC_TN)->f + case_data_idx (cc, ROC_FP)->f ;
++
++ chart_vector (cr, geom, 1 - sp, se);
++ }
++ chart_vector_end (cr, geom);
++ casereader_destroy (r);
++ }
++
++ chart_write_legend (cr, geom);
++}
++
++static void
++roc_chart_destroy (struct chart *chart)
++{
++ struct roc_chart *rc = UP_CAST (chart, struct roc_chart, chart);
++ size_t i;
++
++ for (i = 0; i < rc->n_vars; i++)
++ {
++ struct roc_var *rv = &rc->vars[i];
++ free (rv->name);
++ casereader_destroy (rv->cutpoint_reader);
++ }
++ free (rc->vars);
++ free (rc);
++}
++
++static const struct chart_class roc_chart_class =
++ {
++ roc_chart_draw,
++ roc_chart_destroy
++ };
++
++
--- /dev/null
--- /dev/null
++/* PSPP - a program for statistical analysis.
++ Copyright (C) 2009 Free Software Foundation, Inc.
++
++ This program is free software: you can redistribute it and/or modify
++ it under the terms of the GNU General Public License as published by
++ the Free Software Foundation, either version 3 of the License, or
++ (at your option) any later version.
++
++ This program is distributed in the hope that it will be useful,
++ but WITHOUT ANY WARRANTY; without even the implied warranty of
++ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++ GNU General Public License for more details.
++
++ You should have received a copy of the GNU General Public License
++ along with this program. If not, see <http://www.gnu.org/licenses/>. */
++
++#ifndef OUTPUT_CHARTS_ROC_CHART_H
++#define OUTPUT_CHARTS_ROC_CHART_H 1
++
++#include <stdbool.h>
++
++struct casereader;
++
++struct roc_chart *roc_chart_create (bool reference);
++void roc_chart_add_var (struct roc_chart *, const char *var_name,
++ const struct casereader *cutpoint_reader);
++struct chart *roc_chart_get_chart (struct roc_chart *);
++
++#endif /* output/charts/roc-chart.h */
static struct comparator *
- value_comparator_create (const struct variable *var, const char *target)
+ value_comparator_create (const struct variable *var, const PsppireDict *dict, const char *target)
{
- const struct fmt_spec *fmt;
- int width ;
struct value_comparator *vc = xzalloc (sizeof (*vc));
- struct comparator *cmptr = (struct comparator *) vc;
+ struct comparator *cmptr = &vc->parent;
cmptr->flags = 0;
cmptr->var = var;