From 7dfac5cdb216652cb60f15499a35bb8532e2cf15 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Mon, 16 Jan 2023 20:06:16 -0800 Subject: [PATCH] output: Render pivot tables monolithically. This is a step toward supporting outer borders on tables, which is something that PSPP doesn't do at all now, and which SPSS does very badly. --- doc/dev/spv-file-format.texi | 5 +- src/output/pivot-output.c | 492 ++++++++++++++++++++++------- src/output/pivot-output.h | 4 + src/output/render.c | 301 +++++++++--------- src/output/table.h | 3 +- tests/language/commands/ctables.at | 2 +- tests/output/pivot-table-test.c | 2 - 7 files changed, 538 insertions(+), 271 deletions(-) diff --git a/doc/dev/spv-file-format.texi b/doc/dev/spv-file-format.texi index 523eb556e9..dab0cfd36a 100644 --- a/doc/dev/spv-file-format.texi +++ b/doc/dev/spv-file-format.texi @@ -126,7 +126,10 @@ example, the @code{viewer-tree} schema is associated with namespace additional @file{viewer/}). Under either name, the schema URIs are not resolvable to obtain the schemas themselves. -One may ignore all of the above in interpreting a structure member. +@c XXX The namespaces are important for writing SPV files, so we +@c should note them. + +One may ignore all of the above in interpreting a structure member.p The actual XML has a simple and straightforward form that does not require a reader to take schemas or namespaces into account. A structure member's root is @code{heading} element, which contains diff --git a/src/output/pivot-output.c b/src/output/pivot-output.c index 6d5d8b015c..ca01968e8c 100644 --- a/src/output/pivot-output.c +++ b/src/output/pivot-output.c @@ -112,7 +112,7 @@ table_area_style_override (struct pool *pool, } static void -fill_cell (struct table *t, int x1, int y1, int x2, int y2, +fill_cell (struct table *t, int x, int y, int style_idx, const struct pivot_value *value, bool rotate_label) { @@ -120,14 +120,27 @@ fill_cell (struct table *t, int x1, int y1, int x2, int y2, if (rotate_label) options |= TABLE_CELL_ROTATE; + table_put (t, x, y, x, y, options, value); +} + +static void +fill_cell_spanned (struct table *t, int x1, int y1, int x2, int y2, + int style_idx, const struct pivot_value *value, + bool rotate_label, int options) +{ + options |= style_idx << TABLE_CELL_STYLE_SHIFT; + if (rotate_label) + options |= TABLE_CELL_ROTATE; + table_put (t, x1, y1, x2, y2, options, value); } static void fill_cell_owned (struct table *t, int x1, int y1, int x2, int y2, - int style_idx, struct string *s, bool rotate_label) + int style_idx, struct string *s, bool rotate_label, + int options) { - int options = style_idx << TABLE_CELL_STYLE_SHIFT; + options |= style_idx << TABLE_CELL_STYLE_SHIFT; if (rotate_label) options |= TABLE_CELL_ROTATE; @@ -162,11 +175,12 @@ compose_headings (struct table *t, enum pivot_border cat_col_vert, const size_t *column_enumeration, size_t n_columns, int label_style_idx, - bool rotate_inner_labels, bool rotate_outer_labels) + bool rotate_inner_labels, bool rotate_outer_labels, + int area[TABLE_N_AXES][2]) { const enum table_axis v = !h; const int v_size = h_axis->label_depth; - const int h_ofs = v_axis->label_depth; + const int h_ofs = v_axis->label_depth + area[h][0]; if (!h_axis->n_dimensions || !n_columns || !v_size) return; @@ -176,7 +190,10 @@ compose_headings (struct table *t, /* Below, we're going to iterate through the dimensions. Each dimension occupies one or more rows in the heading. 'top_row' is the top row of these (and 'top_row + d->label_depth - 1' is the bottom row). */ - int top_row = 0; + int top_row = area[v][0]; + + const int h_max = area[h][1]; + const int v_max = area[v][1]; /* We're going to iterate through dimensions and the rows that label them from top to bottom (from outer to inner dimensions). As we move downward, @@ -258,8 +275,6 @@ compose_headings (struct table *t, int y1 = top_row + row_ofs; int y2 = top_row + row_ofs + c->extra_depth + 1; - bool is_outer_row = y1 == 0; - bool is_inner_row = y2 == v_size; if (pivot_category_is_leaf (c) || c->show_label) { int bb[TABLE_N_AXES][2]; @@ -267,10 +282,12 @@ compose_headings (struct table *t, bb[h][1] = x2 + h_ofs - 1; bb[v][0] = y1; bb[v][1] = y2 - 1; + bool is_outer_row = y1 == area[v][0]; + bool is_inner_row = y2 == v_size + area[v][0]; bool rotate = ((rotate_inner_labels && is_inner_row) || (rotate_outer_labels && is_outer_row)); - fill_cell (t, bb[H][0], bb[V][0], bb[H][1], bb[V][1], - label_style_idx, c->name, rotate); + fill_cell_spanned (t, bb[H][0], bb[V][0], bb[H][1], bb[V][1], + label_style_idx, c->name, rotate, 0); /* Draw all the vertical lines in our running example, other than the far left and far right ones. Only the ones that @@ -289,15 +306,15 @@ compose_headings (struct table *t, +-----+-----+-----+-----+-----+-----+-----+-----+-----+ */ enum pivot_border style - = (y1 == v_size - 1 ? cat_col_vert : dim_col_vert); + = (y1 == area[v][0] + v_size - 1 ? cat_col_vert : dim_col_vert); if (!vrules[x2]) { - draw_line (t, style, v, x2 + h_ofs, y1, t->n[v] - 1); + draw_line (t, style, v, x2 + h_ofs, y1, v_max); vrules[x2] = true; } if (!vrules[x1]) { - draw_line (t, style, v, x1 + h_ofs, y1, t->n[v] - 1); + draw_line (t, style, v, x1 + h_ofs, y1, v_max); vrules[x1] = true; } } @@ -324,15 +341,15 @@ compose_headings (struct table *t, } } - if (d->root->show_label_in_corner && h_ofs > 0) + if (d->root->show_label_in_corner && h_ofs > area[h][0]) { int bb[TABLE_N_AXES][2]; - bb[h][0] = 0; + bb[h][0] = area[h][0]; bb[h][1] = h_ofs - 1; bb[v][0] = top_row; bb[v][1] = top_row + d->label_depth - 1; - fill_cell (t, bb[H][0], bb[V][0], bb[H][1], bb[V][1], - PIVOT_AREA_CORNER, d->root->name, false); + fill_cell_spanned (t, bb[H][0], bb[V][0], bb[H][1], bb[V][1], + PIVOT_AREA_CORNER, d->root->name, false, 0); } /* Draw the horizontal line between dimensions, e.g. the ===== line here: @@ -348,7 +365,7 @@ compose_headings (struct table *t, +-----+-----+-----+-----+-----+-----+-----+-----+-----+ */ if (dim_index != h_axis->n_dimensions - 1) - draw_line (t, dim_col_horz, h, top_row, h_ofs, t->n[h] - 1); + draw_line (t, dim_col_horz, h, top_row, h_ofs, h_max); top_row += d->label_depth; } free (vrules); @@ -492,6 +509,102 @@ resolve_border_style (const struct pivot_table_look *look, enum pivot_border b, return style; } +static struct table * +pivot_output_title (const struct pivot_table *pt) +{ + if (!pt->title || !pt->show_title) + return NULL; + struct table *title = create_aux_table (pt, 1, 1, PIVOT_AREA_TITLE); + fill_cell (title, 0, 0, PIVOT_AREA_TITLE, pt->title, false); + return title; +} + +static int +pivot_count_layers (const struct pivot_table *pt) +{ + const struct pivot_axis *layer_axis = &pt->axes[PIVOT_AXIS_LAYER]; + int n_layers = 0; + for (size_t i = 0; i < layer_axis->n_dimensions; i++) + { + const struct pivot_dimension *d = layer_axis->dimensions[i]; + if (d->n_leaves) + n_layers++; + } + return n_layers; +} + +static void +put_layers (struct table *t, const struct pivot_table *pt, + const size_t *layer_indexes, int x1, int y1 UNUSED, int x2, int y2) +{ + assert (y1 + pivot_count_layers (pt) - 1 == y2); + const struct pivot_axis *layer_axis = &pt->axes[PIVOT_AXIS_LAYER]; + for (size_t i = 0; i < layer_axis->n_dimensions; i++) + { + const struct pivot_dimension *d = layer_axis->dimensions[i]; + if (!d->n_leaves) + continue; + + struct string s = DS_EMPTY_INITIALIZER; + pivot_value_format (d->data_leaves[layer_indexes[i]]->name, pt, &s); + int y = y2 - i; + fill_cell_owned (t, x1, y, x2, y, PIVOT_AREA_LAYERS, &s, false, + TABLE_CELL_FULL_WIDTH); + } +} + +static struct table * +pivot_output_layers (const struct pivot_table *pt, const size_t *layer_indexes) +{ + int n_layers = pivot_count_layers (pt); + if (!n_layers) + return NULL; + + struct table *layers = create_aux_table (pt, 1, n_layers, PIVOT_AREA_LAYERS); + put_layers (layers, pt, layer_indexes, 0, 0, 0, n_layers - 1); + return layers; +} + +static struct table * +pivot_output_caption (const struct pivot_table *pt) +{ + if (!pt->caption || !pt->show_caption) + return NULL; + + struct table *caption = create_aux_table (pt, 1, 1, PIVOT_AREA_CAPTION); + fill_cell (caption, 0, 0, PIVOT_AREA_CAPTION, pt->caption, false); + return caption; +} + +static void +put_footnotes (struct table *t, const struct pivot_table *pt, + struct pivot_footnote **f, size_t nf, + int x1, int x2, int y1) +{ + for (size_t i = 0; i < nf; i++) + { + struct string s = DS_EMPTY_INITIALIZER; + pivot_footnote_format_marker (f[i], pt, &s); + ds_put_cstr (&s, ". "); + pivot_value_format (f[i]->content, pt, &s); + int y = y1 + i; + fill_cell_owned (t, x1, y, x2, y, PIVOT_AREA_FOOTER, &s, false, + TABLE_CELL_FULL_WIDTH); + } +} + +static struct table * +pivot_output_footnotes (const struct pivot_table *pt, + struct pivot_footnote **f, size_t nf) +{ + if (!nf) + return NULL; + + struct table *footnotes = create_aux_table (pt, 1, nf, PIVOT_AREA_FOOTER); + put_footnotes (footnotes, pt, f, nf, 0, 0, 0); + return footnotes; +} + void pivot_output (const struct pivot_table *pt, const size_t *layer_indexes, @@ -516,8 +629,8 @@ pivot_output (const struct pivot_table *pt, [H] = pt->axes[PIVOT_AXIS_ROW].label_depth, [V] = pt->axes[PIVOT_AXIS_COLUMN].label_depth, }; - struct table *body = table_create (data[H] + stub[H], - data[V] + stub[V], + + struct table *body = table_create (stub[H] + data[H], stub[V] + data[V], stub[H], stub[V]); for (size_t i = 0; i < PIVOT_N_AREAS; i++) body->styles[i] = table_area_style_override ( @@ -530,6 +643,10 @@ pivot_output (const struct pivot_table *pt, body->borders[i] = resolve_border_style (pt->look, i, printing && pt->show_grid_lines); + int body_area[TABLE_N_AXES][2] = { + [H] = { 0, body->n[H] - 1 }, + [V] = { 0, body->n[V] - 1 }, + }; compose_headings (body, &pt->axes[PIVOT_AXIS_COLUMN], H, &pt->axes[PIVOT_AXIS_ROW], PIVOT_BORDER_DIM_COL_HORZ, @@ -538,7 +655,7 @@ pivot_output (const struct pivot_table *pt, PIVOT_BORDER_CAT_COL_VERT, column_enumeration, data[H], PIVOT_AREA_COLUMN_LABELS, - pt->rotate_outer_row_labels, false); + pt->rotate_outer_row_labels, false, body_area); compose_headings (body, &pt->axes[PIVOT_AXIS_ROW], V, &pt->axes[PIVOT_AXIS_COLUMN], @@ -548,7 +665,7 @@ pivot_output (const struct pivot_table *pt, PIVOT_BORDER_CAT_ROW_HORZ, row_enumeration, data[V], PIVOT_AREA_ROW_LABELS, - false, pt->rotate_inner_column_labels); + false, pt->rotate_inner_column_labels, body_area); size_t *dindexes = XCALLOC (pt->n_dimensions, size_t); size_t y = 0; @@ -562,7 +679,7 @@ pivot_output (const struct pivot_table *pt, { pivot_table_convert_indexes_ptod (pt, pindexes, dindexes); const struct pivot_value *value = pivot_table_get (pt, dindexes); - fill_cell (body, x + stub[H], y + stub[V], x + stub[H], y + stub[V], + fill_cell (body, x + stub[H], y + stub[V], PIVOT_AREA_DATA, value, false); x++; @@ -574,115 +691,278 @@ pivot_output (const struct pivot_table *pt, if ((pt->corner_text || !pt->look->row_labels_in_corner) && stub[H] && stub[V]) - fill_cell (body, 0, 0, stub[H] - 1, stub[V] - 1, - PIVOT_AREA_CORNER, pt->corner_text, false); + fill_cell_spanned (body, 0, 0, stub[H] - 1, stub[V] - 1, + PIVOT_AREA_CORNER, pt->corner_text, false, + TABLE_CELL_FULL_WIDTH); if (body->n[H] && body->n[V]) { - table_hline (body, PIVOT_BORDER_INNER_TOP, 0, body->n[H] - 1, 0); - table_hline (body, PIVOT_BORDER_INNER_BOTTOM, 0, body->n[H] - 1, - body->n[V]); - table_vline (body, PIVOT_BORDER_INNER_LEFT, 0, 0, body->n[V] - 1); - table_vline (body, PIVOT_BORDER_INNER_RIGHT, body->n[H], 0, - body->n[V] - 1); + int inner[TABLE_N_AXES][2] = { + [H] = { 0, body->n[H] }, + [V] = { 0, body->n[V] }, + }; + table_hline (body, PIVOT_BORDER_INNER_TOP, + inner[H][0], inner[H][1] - 1, inner[V][0]); + table_hline (body, PIVOT_BORDER_INNER_BOTTOM, + inner[H][0], inner[H][1] - 1, inner[V][1]); + table_vline (body, PIVOT_BORDER_INNER_LEFT, + inner[H][0], inner[V][0], inner[V][1] - 1); + table_vline (body, PIVOT_BORDER_INNER_LEFT, + inner[H][1], inner[V][0], inner[V][1] - 1); if (stub[V]) - table_hline (body, PIVOT_BORDER_DATA_TOP, 0, body->n[H] - 1, stub[V]); + table_hline (body, PIVOT_BORDER_DATA_TOP, + inner[H][0], inner[H][1] - 1, stub[V]); if (stub[H]) - table_vline (body, PIVOT_BORDER_DATA_LEFT, stub[H], 0, body->n[V] - 1); - + table_vline (body, PIVOT_BORDER_DATA_LEFT, + stub[H], inner[V][0], inner[V][1] - 1); } + free (column_enumeration); free (row_enumeration); - /* Title. */ - struct table *title; - if (pt->title && pt->show_title && titlep) - { - title = create_aux_table (pt, 1, 1, PIVOT_AREA_TITLE); - fill_cell (title, 0, 0, 0, 0, PIVOT_AREA_TITLE, pt->title, false); - } - else - title = NULL; - - /* Layers. */ - const struct pivot_axis *layer_axis = &pt->axes[PIVOT_AXIS_LAYER]; - int n_layers = 0; + *bodyp = body; + if (titlep) + *titlep = pivot_output_title (pt); if (layersp) - for (size_t i = 0; i < layer_axis->n_dimensions; i++) - { - const struct pivot_dimension *d = layer_axis->dimensions[i]; - if (d->n_leaves) - n_layers++; - } + *layersp = pivot_output_layers (pt, layer_indexes); + if (captionp) + *captionp = pivot_output_caption (pt); - struct table *layers; - if (n_layers > 0) + if (fp || footnotesp) { - layers = create_aux_table (pt, 1, n_layers, PIVOT_AREA_LAYERS); - size_t y = n_layers - 1; - for (size_t i = 0; i < layer_axis->n_dimensions; i++) + size_t nf; + struct pivot_footnote **f = collect_footnotes ( + pt, titlep ? *titlep : NULL, layersp ? *layersp : NULL, body, + captionp ? *captionp : NULL, &nf); + + if (footnotesp) + *footnotesp = pivot_output_footnotes (pt, f, nf); + + if (fp) { - const struct pivot_dimension *d = layer_axis->dimensions[i]; - if (!d->n_leaves) - continue; - - struct string s = DS_EMPTY_INITIALIZER; - pivot_value_format (d->data_leaves[layer_indexes[i]]->name, pt, &s); - fill_cell_owned (layers, 0, y, 0, y, PIVOT_AREA_LAYERS, &s, false); - y--; + *fp = f; + *nfp = nf; } + else + free (f); } - else - layers = NULL; +} + +struct table * +pivot_output_monolithic (const struct pivot_table *pt, + const size_t *layer_indexes, + bool printing) +{ + /* Table structure: + + #============ Frame ============# + # Title # + # Layers # + # +--- Body ------------------+ # + # | Stub | # + # | +--------------------+ # + # | | Data | # + # | | | # + # | | | # + # | | | # + # +------+--------------------+ # + # Caption # + # Footnotes # + #===============================# + + The regions are the following size: + + - Frame: Either 0 or 1 row or column wide, depending on whether the table + style has an outer border on each side. The frame cells are always + empty and marked as TABLE_CELL_PADDING. + + - Title: Either 0 or 1 row high. + + - Layers: Usually, one row for each layer dimension, but less if a layer + dimension is empty (has no value). + + - Body and data: Variable (and can be empty). + + - Caption: Either 0 or 1 row high. + + - Footnotes: From zero rows up to the number of footnotes in PT (only + footnotes referenced in the other regions are shown). - /* Caption. */ - struct table *caption; - if (pt->caption && pt->show_caption && captionp) + The title, layers, caption, and footnote Region join all the body's + columns into single cells. + */ + + /* Size of data region. */ + size_t data[TABLE_N_AXES]; + size_t *column_enumeration = pivot_table_enumerate_axis ( + pt, PIVOT_AXIS_COLUMN, layer_indexes, pt->look->omit_empty, &data[H]); + size_t *row_enumeration = pivot_table_enumerate_axis ( + pt, PIVOT_AXIS_ROW, layer_indexes, pt->look->omit_empty, &data[V]); + + /* Size of stub region. */ + int stub[TABLE_N_AXES] = { + [H] = pt->axes[PIVOT_AXIS_ROW].label_depth, + [V] = pt->axes[PIVOT_AXIS_COLUMN].label_depth, + }; + + int n_title = pt->title && pt->show_title; + int n_layers = pivot_count_layers (pt); + int n_caption = pt->caption && pt->show_caption; + int max_footnotes = pt->n_footnotes; + + int min_body_width = n_title || n_layers || n_caption; + int n[TABLE_N_AXES] = { + [H] = MAX (min_body_width, stub[H] + data[H]), + [V] = n_title + n_layers + stub[V] + data[V] + n_caption + max_footnotes, + }; + + struct table *t = table_create (n[H], n[V], + stub[H], n_title + n_layers + stub[V]); + for (size_t i = 0; i < PIVOT_N_AREAS; i++) + t->styles[i] = table_area_style_override ( + t->container, &pt->look->areas[i], NULL, NULL, false); + + t->n_borders = PIVOT_N_BORDERS; + t->borders = pool_nmalloc (t->container, PIVOT_N_BORDERS, sizeof *t->borders); + for (size_t i = 0; i < PIVOT_N_BORDERS; i++) + t->borders[i] = resolve_border_style (pt->look, i, + printing && pt->show_grid_lines); + + int body[TABLE_N_AXES][2] = { + [H] = { 0, n[H] - 1 }, + [V] = { + n_title + n_layers, + n_title + n_layers + stub[V] + data[V] - 1 + }, + }; + + if (n_layers) + put_layers (t, pt, layer_indexes, + body[H][0], n_title, + body[H][1], n_title + n_layers - 1); + + compose_headings (t, + &pt->axes[PIVOT_AXIS_COLUMN], H, &pt->axes[PIVOT_AXIS_ROW], + PIVOT_BORDER_DIM_COL_HORZ, + PIVOT_BORDER_DIM_COL_VERT, + PIVOT_BORDER_CAT_COL_HORZ, + PIVOT_BORDER_CAT_COL_VERT, + column_enumeration, data[H], + PIVOT_AREA_COLUMN_LABELS, + pt->rotate_outer_row_labels, false, body); + + compose_headings (t, + &pt->axes[PIVOT_AXIS_ROW], V, &pt->axes[PIVOT_AXIS_COLUMN], + PIVOT_BORDER_DIM_ROW_VERT, + PIVOT_BORDER_DIM_ROW_HORZ, + PIVOT_BORDER_CAT_ROW_VERT, + PIVOT_BORDER_CAT_ROW_HORZ, + row_enumeration, data[V], + PIVOT_AREA_ROW_LABELS, + false, pt->rotate_inner_column_labels, body); + + int data_ofs[TABLE_N_AXES] = { + [H] = body[H][0] + stub[H], + [V] = body[V][0] + stub[V], + }; + + const size_t *pindexes[PIVOT_N_AXES] = { [PIVOT_AXIS_LAYER] = layer_indexes }; + size_t *dindexes = XCALLOC (pt->n_dimensions, size_t); + int y = data_ofs[V]; + PIVOT_ENUMERATION_FOR_EACH (pindexes[PIVOT_AXIS_ROW], row_enumeration, + &pt->axes[PIVOT_AXIS_ROW]) { - caption = create_aux_table (pt, 1, 1, PIVOT_AREA_CAPTION); - fill_cell (caption, 0, 0, 0, 0, PIVOT_AREA_CAPTION, pt->caption, false); + int x = data_ofs[H]; + PIVOT_ENUMERATION_FOR_EACH (pindexes[PIVOT_AXIS_COLUMN], + column_enumeration, + &pt->axes[PIVOT_AXIS_COLUMN]) + { + pivot_table_convert_indexes_ptod (pt, pindexes, dindexes); + const struct pivot_value *value = pivot_table_get (pt, dindexes); + fill_cell (t, x, y, PIVOT_AREA_DATA, value, false); + x++; + } + + y++; } - else - caption = NULL; + free (dindexes); - /* Footnotes. */ - size_t nf; - struct pivot_footnote **f = collect_footnotes (pt, title, layers, body, - caption, &nf); - struct table *footnotes; - if (nf && footnotesp) + if ((pt->corner_text || !pt->look->row_labels_in_corner) + && stub[H] && stub[V]) + fill_cell_spanned (t, body[H][0], body[V][0], + body[H][0] + stub[H] - 1, body[V][0] + stub[V] - 1, + PIVOT_AREA_CORNER, pt->corner_text, false, 0); + + if (body[H][1] >= body[H][0] && body[V][1] >= body[V][0]) { - footnotes = create_aux_table (pt, 1, nf, PIVOT_AREA_FOOTER); + table_hline (t, PIVOT_BORDER_INNER_TOP, + body[H][0], body[H][1], body[V][0]); + table_hline (t, PIVOT_BORDER_INNER_BOTTOM, + body[H][0], body[H][1], body[V][1] + 1); + table_vline (t, PIVOT_BORDER_INNER_LEFT, + body[H][0], body[V][0], body[V][1]); + table_vline (t, PIVOT_BORDER_INNER_RIGHT, + body[H][1] + 1, body[V][0], body[V][1]); - for (size_t i = 0; i < nf; i++) - { - struct string s = DS_EMPTY_INITIALIZER; - pivot_footnote_format_marker (f[i], pt, &s); - ds_put_cstr (&s, ". "); - pivot_value_format (f[i]->content, pt, &s); - fill_cell_owned (footnotes, 0, i, 0, i, PIVOT_AREA_FOOTER, &s, - false); - } + if (stub[V]) + table_hline (t, PIVOT_BORDER_DATA_TOP, + body[H][0], body[H][1], data_ofs[V]); + if (stub[H]) + table_vline (t, PIVOT_BORDER_DATA_LEFT, + data_ofs[H], body[V][0], body[V][1]); } - else - footnotes = NULL; - *titlep = title; - if (layersp) - *layersp = layers; - *bodyp = body; - if (captionp) - *captionp = caption; - if (footnotesp) - *footnotesp = footnotes; - if (fp) + if (n_caption) + fill_cell_spanned (t, + body[H][0], body[V][1] + 1, + body[H][1], body[V][1] + 1, + PIVOT_AREA_CAPTION, pt->caption, false, + TABLE_CELL_FULL_WIDTH); + + size_t nf; + struct pivot_footnote **f = collect_footnotes (pt, t, NULL, NULL, NULL, &nf); + assert (nf <= max_footnotes); + put_footnotes (t, pt, f, nf, body[H][0], body[H][1], + body[V][1] + 1 + n_caption); + t->n[V] = body[V][1] + 1 + n_caption + nf; + free (f); + + if (n_title) { - *fp = f; - *nfp = nf; +#if 0 + printf ("%d,%d - %d,%d\n", + body[H][0], 0, + body[H][1], 0); +#endif + fill_cell_spanned (t, + body[H][0], 0, + body[H][1], 0, + PIVOT_AREA_TITLE, pt->title, false, + TABLE_CELL_FULL_WIDTH); } - else - free (f); + + free (column_enumeration); + free (row_enumeration); + +#if 0 + printf ("output page:\n"); + for (int y = 0; y < t->n[V]; y++) + for (int x = 0; x < t->n[H]; x++) + { + struct table_cell cell; + + table_get_cell (t, x, y, &cell); + char *s = pivot_value_to_string (cell.value, NULL); + printf ("%d,%d - %d,%d: %s\n", + cell.d[H][0], cell.d[V][0], + cell.d[H][1], cell.d[V][1], + s); + free (s); + } +#endif + + return t; } void diff --git a/src/output/pivot-output.h b/src/output/pivot-output.h index 4013c1bc45..6620cf7618 100644 --- a/src/output/pivot-output.h +++ b/src/output/pivot-output.h @@ -40,4 +40,8 @@ void pivot_output (const struct pivot_table *, struct table **footnotesp, struct pivot_footnote ***fp, size_t *nfp); +struct table *pivot_output_monolithic (const struct pivot_table *, + const size_t *layer_indexes, + bool printing); + #endif /* output/pivot-output.h */ diff --git a/src/output/render.c b/src/output/render.c index fbf055029a..a7dcfa6856 100644 --- a/src/output/render.c +++ b/src/output/render.c @@ -160,7 +160,7 @@ struct render_page }; static struct render_page *render_page_create (const struct render_params *, - struct table *, int min_width, + struct table *, const struct pivot_table_look *); struct render_page *render_page_ref (const struct render_page *page_); @@ -510,25 +510,27 @@ measure_rule (const struct render_params *params, const struct table *table, } /* Allocates and returns a new render_page using PARAMS and TABLE. Allocates - space for rendering a table with dimensions given in N. The caller must - initialize most of the members itself. */ + space for rendering a table with dimensions given in N, headers in H, and + content in R. The caller must initialize most of the members itself. */ static struct render_page * render_page_allocate__ (const struct render_params *params, - struct table *table, int n[TABLE_N_AXES]) + struct table *table, + const int n[TABLE_N_AXES], + const int h[TABLE_N_AXES], + const int r[TABLE_N_AXES][2]) { struct render_page *page = xmalloc (sizeof *page); - page->params = params; - page->table = table; - page->ref_cnt = 1; - page->n[H] = n[H]; - page->n[V] = n[V]; - - for (int i = 0; i < TABLE_N_AXES; i++) - page->cp[i] = xcalloc ((2 * n[i] + 2) , sizeof *page->cp[i]); - - hmap_init (&page->overflows); - memset (page->is_edge_cutoff, 0, sizeof page->is_edge_cutoff); - + *page = (struct render_page) { + .params = params, + .table = table, + .ref_cnt = 1, + .n = { [H] = n[H], [V] = n[V] }, + .h = { [H] = h[H], [V] = h[V] }, + .r = { [H] = { r[H][0], r[H][1] }, [V] = { r[V][0], r[V][1] } }, + .cp = { [H] = xcalloc (2 * n[H] + 2, sizeof *page->cp[H]), + [V] = xcalloc (2 * n[V] + 2, sizeof *page->cp[V]) }, + .overflows = HMAP_INITIALIZER (page->overflows), + }; return page; } @@ -538,14 +540,15 @@ render_page_allocate__ (const struct render_params *params, static struct render_page * render_page_allocate (const struct render_params *params, struct table *table) { - struct render_page *page = render_page_allocate__ (params, table, table->n); + int h[TABLE_N_AXES]; + int r[TABLE_N_AXES][2]; for (enum table_axis a = 0; a < TABLE_N_AXES; a++) { - page->h[a] = table->h[a]; - page->r[a][0] = table->h[a]; - page->r[a][1] = table->n[a]; + h[a] = table->h[a]; + r[a][0] = table->h[a]; + r[a][1] = table->n[a]; } - return page; + return render_page_allocate__ (params, table, table->n, h, r); } /* Allocates and returns a new render_page for PARAMS and TABLE, initializing @@ -668,6 +671,12 @@ render_get_cell (const struct render_page *page, int x, int y, cell->d[a][0] = MAX (cell->d[a][0], m->p0); cell->d[a][1] = MIN (cell->d[a][1], m->p0 + m->n); } + + if (cell->options & TABLE_CELL_FULL_WIDTH) + { + cell->d[H][0] = 0; + cell->d[H][1] = page->n[H]; + } } /* Creates and returns a new render_page for rendering TABLE with the given @@ -678,7 +687,7 @@ render_get_cell (const struct render_page *page, int x, int y, up to fit on such a device, using the render_break abstraction. */ static struct render_page * render_page_create (const struct render_params *params, struct table *table, - int min_width, const struct pivot_table_look *look) + const struct pivot_table_look *look) { enum { MIN, MAX }; @@ -714,40 +723,44 @@ render_page_create (const struct render_params *params, struct table *table, struct table_cell cell; table_get_cell (table, x, y, &cell); - if (y == cell.d[V][0]) + if (y == cell.d[V][0] + && table_cell_colspan (&cell) == 1) { - if (table_cell_colspan (&cell) == 1) + int w[2]; + params->ops->measure_cell_width (params->aux, &cell, + &w[MIN], &w[MAX]); + + if (cell.options & TABLE_CELL_FULL_WIDTH) { - int w[2]; - params->ops->measure_cell_width (params->aux, &cell, - &w[MIN], &w[MAX]); + int rule_width = rules[H][0] + rules[H][table->n[H]]; + w[MIN] = w[MAX] = MAX (0, w[MIN] - rule_width); + } - if (params->px_size) + if (params->px_size) + { + const int *wr = (x < table->h[H] ? row_heading_width_range + : y < table->h[V] ? col_heading_width_range + : NULL); + if (wr) { - const int *wr = (x < table->h[H] ? row_heading_width_range - : y < table->h[V] ? col_heading_width_range - : NULL); - if (wr) + if (w[0] < wr[0]) { - if (w[0] < wr[0]) - { - w[0] = wr[0]; - if (w[0] > w[1]) - w[1] = w[0]; - } - else if (w[1] > wr[1]) - { - w[1] = wr[1]; - if (w[1] < w[0]) - w[0] = w[1]; - } + w[0] = wr[0]; + if (w[0] > w[1]) + w[1] = w[0]; + } + else if (w[1] > wr[1]) + { + w[1] = wr[1]; + if (w[1] < w[0]) + w[0] = w[1]; } } - - for (int i = 0; i < 2; i++) - if (columns[i][x].unspanned < w[i]) - columns[i][x].unspanned = w[i]; } + + for (int i = 0; i < 2; i++) + if (columns[i][x].unspanned < w[i]) + columns[i][x].unspanned = w[i]; } x = cell.d[H][1]; } @@ -762,12 +775,19 @@ render_page_create (const struct render_params *params, struct table *table, struct table_cell cell; table_get_cell (table, x, y, &cell); - if (y == cell.d[V][0] && table_cell_colspan (&cell) > 1) + if (y == cell.d[V][0] + && table_cell_colspan (&cell) > 1 + && !(cell.options & TABLE_CELL_FULL_WIDTH)) { int w[2]; params->ops->measure_cell_width (params->aux, &cell, &w[MIN], &w[MAX]); + if (cell.options & TABLE_CELL_FULL_WIDTH) + { + int rule_width = rules[H][0] + rules[H][table->n[H]]; + w[MIN] = w[MAX] = MAX (0, w[MIN] - rule_width); + } for (int i = 0; i < 2; i++) distribute_spanned_width (w[i], &columns[i][cell.d[H][0]], @@ -776,9 +796,6 @@ render_page_create (const struct render_params *params, struct table *table, } x = cell.d[H][1]; } - if (min_width > 0) - for (int i = 0; i < 2; i++) - distribute_spanned_width (min_width, &columns[i][0], rules[H], nc); /* In pathological cases, spans can cause the minimum width of a column to exceed the maximum width. This bollixes our interpolation algorithm @@ -827,6 +844,8 @@ render_page_create (const struct render_params *params, struct table *table, if (y == cell.d[V][0] && table_cell_rowspan (&cell) == 1) { int w = joined_width (page, H, cell.d[H][0], cell.d[H][1]); + if (cell.options & TABLE_CELL_FULL_WIDTH) + w += rules[H][0] + rules[H][table->n[H]]; int h = params->ops->measure_cell_height (params->aux, &cell, w); if (h > r->unspanned) @@ -847,6 +866,8 @@ render_page_create (const struct render_params *params, struct table *table, if (y == cell.d[V][0] && table_cell_rowspan (&cell) > 1) { int w = joined_width (page, H, cell.d[H][0], cell.d[H][1]); + if (cell.options & TABLE_CELL_FULL_WIDTH) + w += rules[H][0] + rules[H][table->n[H]]; int h = params->ops->measure_cell_height (params->aux, &cell, w); distribute_spanned_width (h, &rows[cell.d[V][0]], @@ -1078,18 +1099,30 @@ render_cell (const struct render_page *page, const int ofs[TABLE_N_AXES], } int bb[TABLE_N_AXES][2]; - int clip[TABLE_N_AXES][2]; - bb[H][0] = clip[H][0] = ofs[H] + page->cp[H][cell->d[H][0] * 2 + 1]; - bb[H][1] = clip[H][1] = ofs[H] + page->cp[H][cell->d[H][1] * 2]; - if (page->params->rtl) + if (!(cell->options & TABLE_CELL_FULL_WIDTH)) { - int temp = bb[H][0]; - bb[H][0] = clip[H][0] = render_page_get_size (page, H) - bb[H][1]; - bb[H][1] = clip[H][1] = render_page_get_size (page, H) - temp; + bb[H][0] = ofs[H] + page->cp[H][cell->d[H][0] * 2 + 1]; + bb[H][1] = ofs[H] + page->cp[H][cell->d[H][1] * 2]; + if (page->params->rtl) + { + int temp = bb[H][0]; + bb[H][0] = render_page_get_size (page, H) - bb[H][1]; + bb[H][1] = render_page_get_size (page, H) - temp; + } } - bb[V][0] = clip[V][0] = ofs[V] + page->cp[V][cell->d[V][0] * 2 + 1]; - bb[V][1] = clip[V][1] = ofs[V] + page->cp[V][cell->d[V][1] * 2]; + else + { + bb[H][0] = ofs[H]; + bb[H][1] = ofs[H] + page->cp[H][cell->d[H][1] * 2 + 1]; + } + bb[V][0] = ofs[V] + page->cp[V][cell->d[V][0] * 2 + 1]; + bb[V][1] = ofs[V] + page->cp[V][cell->d[V][1] * 2]; + + int clip[TABLE_N_AXES][2] = { + [H] = { bb[H][0], bb[H][1] }, + [V] = { bb[V][0], bb[V][1] }, + }; enum table_valign valign = cell->cell_style->valign; int valign_offset = 0; @@ -1474,31 +1507,16 @@ struct render_pager const struct render_params *params; double scale; - /* An array of "render_page"s to be rendered, in order, vertically. There - may be up to 5 pages, for the pivot table's title, layers, body, - captions, and footnotes. */ - struct render_page *pages[5]; - size_t n_pages; + struct render_page *page; - size_t cur_page; struct render_break x_break; struct render_break y_break; }; -static void -render_pager_add_table (struct render_pager *p, struct table *table, - int min_width, const struct pivot_table_look *look) -{ - if (table) - p->pages[p->n_pages++] = render_page_create (p->params, table, min_width, - look); -} - static void render_pager_start_page (struct render_pager *p) { - render_break_init (&p->x_break, render_page_ref (p->pages[p->cur_page++]), - H); + render_break_init (&p->x_break, render_page_ref (p->page), H); render_break_init_empty (&p->y_break); } @@ -1512,26 +1530,24 @@ render_pager_create (const struct render_params *params, if (!layer_indexes) layer_indexes = pt->current_layer; - struct table *title, *layers, *body, *caption, *footnotes; - pivot_output (pt, layer_indexes, params->printing, - &title, &layers, &body, &caption, &footnotes, NULL, NULL); + struct table *table = pivot_output_monolithic (pt, layer_indexes, + params->printing); - /* Figure out the width of the body of the table. Use this to determine the - base scale. */ - struct render_page *body_page = render_page_create (params, body, 0, pt->look); - int body_width = table_width (body_page, H); + /* Measure the table width and use it to determine the base scale. */ + struct render_page *page = render_page_create (params, table, pt->look); + int width = table_width (page, H); double scale = 1.0; - if (body_width > params->size[H]) + if (width > params->size[H]) { if (pt->look->shrink_to_fit[H] && params->ops->scale) - scale = params->size[H] / (double) body_width; + scale = params->size[H] / (double) width; else { struct render_break b; - render_break_init (&b, render_page_ref (body_page), H); + render_break_init (&b, render_page_ref (page), H); struct render_page *subpage = render_break_next (&b, params->size[H]); - body_width = subpage ? subpage->cp[H][2 * subpage->n[H] + 1] : 0; + width = subpage ? subpage->cp[H][2 * subpage->n[H] + 1] : 0; render_page_unref (subpage); render_break_destroy (&b); } @@ -1539,13 +1555,7 @@ render_pager_create (const struct render_params *params, /* Create the pager. */ struct render_pager *p = xmalloc (sizeof *p); - *p = (struct render_pager) { .params = params, .scale = scale }; - render_pager_add_table (p, title, body_width, pt->look); - render_pager_add_table (p, layers, body_width, pt->look); - p->pages[p->n_pages++] = body_page; - render_pager_add_table (p, caption, 0, pt->look); - render_pager_add_table (p, footnotes, 0, pt->look); - assert (p->n_pages <= sizeof p->pages / sizeof *p->pages); + *p = (struct render_pager) { .params = params, .scale = scale, .page = page }; /* If we're shrinking tables to fit the page length, then adjust the scale factor. @@ -1557,11 +1567,9 @@ render_pager_create (const struct render_params *params, necessary would require an iterative search. */ if (pt->look->shrink_to_fit[V] && params->ops->scale) { - int total_height = 0; - for (size_t i = 0; i < p->n_pages; i++) - total_height += table_width (p->pages[i], V); - if (total_height * p->scale >= params->size[V]) - p->scale *= params->size[V] / (double) total_height; + double height = table_width (p->page, V); + if (height * p->scale >= params->size[V]) + p->scale *= params->size[V] / height; } render_pager_start_page (p); @@ -1577,8 +1585,7 @@ render_pager_destroy (struct render_pager *p) { render_break_destroy (&p->x_break); render_break_destroy (&p->y_break); - for (size_t i = 0; i < p->n_pages; i++) - render_page_unref (p->pages[i]); + render_page_unref (p->page); free (p); } } @@ -1596,13 +1603,9 @@ render_pager_has_next (const struct render_pager *p_) if (!render_break_has_next (&p->x_break)) { render_break_destroy (&p->x_break); - if (p->cur_page >= p->n_pages) - { - render_break_init_empty (&p->x_break); - render_break_init_empty (&p->y_break); - return false; - } - render_pager_start_page (p); + render_break_init_empty (&p->x_break); + render_break_init_empty (&p->y_break); + return false; } else render_break_init ( @@ -1628,22 +1631,17 @@ render_pager_draw_next (struct render_pager *p, int space) } int ofs[TABLE_N_AXES] = { 0, 0 }; - size_t start_page = SIZE_MAX; - while (render_pager_has_next (p)) + if (render_pager_has_next (p)) { - if (start_page == p->cur_page) - break; - start_page = p->cur_page; - struct render_page *page = render_break_next (&p->y_break, space - ofs[V]); - if (!page) - break; - - render_page_draw (page, ofs); - ofs[V] += render_page_get_size (page, V); - render_page_unref (page); + if (page) + { + render_page_draw (page, ofs); + ofs[V] += render_page_get_size (page, V); + render_page_unref (page); + } } if (p->scale != 1.0) @@ -1671,18 +1669,14 @@ render_pager_draw_region (const struct render_pager *p, clip[H][0] = x; clip[H][1] = x + w; - for (size_t i = 0; i < p->n_pages; i++) - { - const struct render_page *page = p->pages[i]; - int size = render_page_get_size (page, V); + int size = render_page_get_size (p->page, V); - clip[V][0] = MAX (y, ofs[V]) - ofs[V]; - clip[V][1] = MIN (y + h, ofs[V] + size) - ofs[V]; - if (clip[V][1] > clip[V][0]) - render_page_draw_region (page, ofs, clip); + clip[V][0] = MAX (y, ofs[V]) - ofs[V]; + clip[V][1] = MIN (y + h, ofs[V] + size) - ofs[V]; + if (clip[V][1] > clip[V][0]) + render_page_draw_region (p->page, ofs, clip); - ofs[V] += size; - } + ofs[V] += size; } /* Returns the size of P's content along AXIS; i.e. the content's width if AXIS @@ -1690,32 +1684,16 @@ render_pager_draw_region (const struct render_pager *p, int render_pager_get_size (const struct render_pager *p, enum table_axis axis) { - int size = 0; - - for (size_t i = 0; i < p->n_pages; i++) - { - int subsize = render_page_get_size (p->pages[i], axis); - size = axis == H ? MAX (size, subsize) : size + subsize; - } - - return size; + return render_page_get_size (p->page, axis); } int render_pager_get_best_breakpoint (const struct render_pager *p, int height) { - int y = 0; - size_t i; - - for (i = 0; i < p->n_pages; i++) - { - int size = render_page_get_size (p->pages[i], V); - if (y + size >= height) - return render_page_get_best_breakpoint (p->pages[i], height - y) + y; - y += size; - } - - return height; + int size = render_page_get_size (p->page, V); + return (size < height + ? height + : render_page_get_best_breakpoint (p->page, height)); } /* render_page_select() and helpers. */ @@ -1772,18 +1750,21 @@ render_page_select (const struct render_page *page, enum table_axis axis, /* Allocate subpage. */ int trim[2] = { z0 - page->h[a], page->n[a] - z1 }; + int n[TABLE_N_AXES] = { [H] = page->n[H], [V] = page->n[V] }; n[a] -= trim[0] + trim[1]; - struct render_page *subpage = render_page_allocate__ ( - page->params, table_ref (page->table), n); + + int r[TABLE_N_AXES][2]; for (enum table_axis k = 0; k < TABLE_N_AXES; k++) { - subpage->h[k] = page->h[k]; - subpage->r[k][0] = page->r[k][0]; - subpage->r[k][1] = page->r[k][1]; + r[k][0] = page->r[k][0]; + r[k][1] = page->r[k][1]; } - subpage->r[a][0] += trim[0]; - subpage->r[a][1] -= trim[1]; + r[a][0] += trim[0]; + r[a][1] -= trim[1]; + + struct render_page *subpage = render_page_allocate__ ( + page->params, table_ref (page->table), n, page->h, r); /* An edge is cut off if it was cut off in PAGE or if we're trimming pixels off that side of the page and there are no headers. */ diff --git a/src/output/table.h b/src/output/table.h index 42bdf299e7..032ed5ba08 100644 --- a/src/output/table.h +++ b/src/output/table.h @@ -183,7 +183,8 @@ enum { TABLE_CELL_ROTATE = 1 << 0, /* Rotate cell contents 90 degrees. */ TABLE_CELL_JOIN = 1 << 1, /* Joined cell (internal use only). */ - TABLE_CELL_STYLE_SHIFT = 2, + TABLE_CELL_FULL_WIDTH = 1 << 2, /* Spans full width, ignoring headers. */ + TABLE_CELL_STYLE_SHIFT = 3, TABLE_CELL_STYLE_MASK = 7 << TABLE_CELL_STYLE_SHIFT, }; diff --git a/tests/language/commands/ctables.at b/tests/language/commands/ctables.at index 22e1380a58..27656e26e8 100644 --- a/tests/language/commands/ctables.at +++ b/tests/language/commands/ctables.at @@ -2006,7 +2006,7 @@ CTABLES /TABLE=qn105ba [COUNT, ROWPCT] BY qns1 /CATEGORIES VARIABLES=qns1 [1, 2, SUBTOTAL, 3, 4, 5, &x, &y, SUBTOTAL] TOTAL=YES ]]) -AT_CHECK([pspp ctables.sps -O box=unicode -O width=140], [0], [dnl +AT_CHECK([pspp ctables.sps -o - -O box=unicode -O width=140 -o ctables.pdf -o ctables.spv], [0], [dnl Custom Tables ╭───────────────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ │ S1. Including yourself, how many members of this household are age 16 or older? │ diff --git a/tests/output/pivot-table-test.c b/tests/output/pivot-table-test.c index 8bed7550eb..2d02ed71ac 100644 --- a/tests/output/pivot-table-test.c +++ b/tests/output/pivot-table-test.c @@ -127,7 +127,6 @@ configure_drivers (int width, int length UNUSED, int min_break) string_map_insert (&options, "box", box); register_driver (&options, "-"); - /* Render to .pdf. */ string_map_insert (&options, "top-margin", "0"); string_map_insert (&options, "bottom-margin", "0"); @@ -154,7 +153,6 @@ configure_drivers (int width, int length UNUSED, int min_break) register_driver (&options, "%s.spv", output_base); register_driver (&options, "%s.html", output_base); register_driver (&options, "%s.tex", output_base); - string_map_destroy (&options); } -- 2.30.2