A pivot created on its own sheet widens its columns to the results until a width is set by hand. PivotFilterValues lists a source column's values the way a filter's values list does, sharing its code. XLSX downloads write pivot results as values.
- commit
- a40bf99103deef78ff6da878ff8b4b7b16a4e177
- parent
- 901e837a5e952f9058fbe6edc4c1e869b3083643
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 31 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 14 | ++++++++++---- |
| M | internal/ | 29 | +++++++++++++++++++++++++--- |
| M | internal/ | 26 | +++++++++++++++++++++++++ |
| M | internal/ | 18 | +++++++++++++++++ |
| M | internal/ | 1 | + |
| M | internal/ | 2 | +- |
@@ -251,6 +251,37 @@ func TestExcelFormulas(t *testing.T) {
251251 }252252 }253253 254+// A pivot table goes out as the values it shows: Excel gets no pivot255+// cache, and its results read back as plain cells.256+func TestXLSXPivotAsValues(t *testing.T) {257+ src := build(t, map[string]string{"A1": "Item", "B1": "Qty", "A2": "Pen", "B2": "3", "A3": "Ink", "B3": "4", "A4": "Pen", "B4": "5"})258+ r := sheet.NewRect(addr(t, "A1"), addr(t, "B4"))259+ p := sheet.NewPivot(src, r)260+ p.Rows = []sheet.PivotGroup{{Col: 0}}261+ p.Values = []sheet.PivotValue{{Col: 1, Summarize: sheet.SumBy}}262+ pv, err := src.Book().CreatePivot(src, r, "", p)263+ if err != nil {264+ t.Fatal(err)265+ }266+ name := filepath.Join(t.TempDir(), "pivot.xlsx")267+ if _, err := Export(context.Background(), name, XLSX, SnapBook(pv), ExportOptions{}); err != nil {268+ t.Fatal(err)269+ }270+ x, err := excelize.OpenFile(name)271+ if err != nil {272+ t.Fatal(err)273+ }274+ defer x.Close()275+ for cell, want := range map[string]string{"A1": "Item", "B1": "SUM of Qty", "A3": "Pen", "B3": "8", "B4": "12"} {276+ if got, _ := x.GetCellValue("Pivot Table 1", cell); got != want {277+ t.Errorf("%s = %q, want %q", cell, got, want)278+ }279+ if f, _ := x.GetCellFormula("Pivot Table 1", cell); f != "" {280+ t.Errorf("%s has formula %q", cell, f)281+ }282+ }283+}284+254285 // Every sheet goes out to Excel and comes back, with references between255286 // sheets, the sheet shown and named ranges.256287 func TestXLSXWorkbookRoundTrip(t *testing.T) {@@ -334,15 +334,21 @@ func (s *Sheet) FilterValues(col int) []FilterValue {
334334 if f == nil {335335 return nil336336 }337- tests := s.criteriaTests(f, col)337+ return s.valuesList(f.Range, col, s.criteriaTests(f, col), f.Cols[col].Hidden, false)338+}339+340+// valuesList lists the distinct values shown in column col of r's data341+// rows among those passing tests, with hidden unchecked, as a filter's342+// values list. With skipBlank, rows blank across r are left out.343+func (s *Sheet) valuesList(r Rect, col int, tests []colTest, hide []string, skipBlank bool) []FilterValue {338344 hidden := map[string]bool{}339- for _, h := range f.Cols[col].Hidden {345+ for _, h := range hide {340346 hidden[h] = true341347 }342348 counts := map[string]int{}343349 values := map[string]Value{}344- for row := f.Range.From.Row + 1; row <= f.Range.To.Row; row++ {345- if !s.rowPasses(row, tests) {350+ for row := r.From.Row + 1; row <= r.To.Row; row++ {351+ if !s.rowPasses(row, tests) || skipBlank && s.rowBlank(row, r) {346352 continue347353 }348354 a := Addr{Col: col, Row: row}@@ -105,6 +105,11 @@ type pivotState struct {
105105 stale bool // recompute at the end of the current change106106 out Rect // what the last result covered, from A1107107 err string // why the pivot shows #REF!, or ""108+ // fit has the result's columns widened to fit, while the sheet is109+ // the pivot's own: from its creation until a width is set by hand.110+ // The widths aren't undo steps; undoing the creation removes them111+ // with the sheet.112+ fit bool108113 }109114 110115 // clone returns a deep copy, so a stored pivot never shares slices.@@ -195,9 +200,7 @@ func (w *Workbook) CreatePivot(src *Sheet, r Rect, name string, p Pivot) (*Sheet
195200 w.change(s, "create "+name, Rect{}, func() {196201 w.recordSheets()197202 w.insert(s, w.Index(src)+1)198- s.pivot.def = p.clone()199- s.pivot.stale = true200- s.pivot.out = Rect{}203+ s.pivot = pivotState{def: p.clone(), stale: true, fit: true}201204 })202205 return s, nil203206 }@@ -356,6 +359,26 @@ func (w *Workbook) DefaultSummarize(p Pivot, col int) Summarize {
356359 return CountABy357360 }358361 362+// PivotFilterValues lists the distinct values of column col of the363+// pivot's source, among the rows its other filters let through, as a364+// filter's values list does.365+func (w *Workbook) PivotFilterValues(p Pivot, col int) []FilterValue {366+ src := w.Lookup(p.Source)367+ if src == nil || p.Lost {368+ return nil369+ }370+ var others []PivotFilter371+ var hidden []string372+ for _, f := range p.Filters {373+ if f.Col == col {374+ hidden = f.Criteria.Hidden375+ } else {376+ others = append(others, f)377+ }378+ }379+ return src.valuesList(p.Range, col, pivotTests(others), hidden, true)380+}381+359382 // NewPivot starts a pivot over r on src with nothing chosen yet and both360383 // grand totals on, as Sheets' new pivot tables.361384 func NewPivot(src *Sheet, r Rect) Pivot {@@ -77,6 +77,32 @@ func TestPivotGroupsRows(t *testing.T) {
7777 if c := s.Cell(at("A5")); !c.Style.Bold {7878 t.Error("the Grand Total row isn't bold")7979 }80+ // A new pivot sheet's columns fit the results, until set by hand.81+ if s.ColWidth(0) != 13 || s.ColWidth(2) != 17 {82+ t.Errorf("widths %d %d, want 13 17", s.ColWidth(0), s.ColWidth(2))83+ }84+ s.SetColWidth(0, 20)85+ w.Lookup("Sales").Set(at("A2"), "Eastern seaboard region")86+ if s.ColWidth(0) != 20 {87+ t.Errorf("width %d after setting it by hand", s.ColWidth(0))88+ }89+}90+91+func TestPivotFilterValues(t *testing.T) {92+ w := sales(t)93+ s := newPivot(t, w, func(p *Pivot) {94+ p.Filters = []PivotFilter{95+ {Col: 0, Criteria: Criteria{Hidden: []string{"West"}}},96+ {Col: 1, Criteria: Criteria{Hidden: []string{"Ink"}}},97+ }98+ })99+ p, _ := s.Pivot()100+ // Region's values among the rows Product's filter lets through.101+ got := w.PivotFilterValues(p, 0)102+ want := []FilterValue{{"East", 1, true}, {"North", 1, true}, {"West", 1, false}}103+ if !reflect.DeepEqual(got, want) {104+ t.Errorf("values %v, want %v", got, want)105+ }80106 }81107 82108 func TestPivotColumnsAndSubtotals(t *testing.T) {@@ -281,6 +281,9 @@ func (w *Workbook) refreshPivot(s *Sheet) []loc {
281281 s.pivot.out = union(s.pivot.out, Rect{From: pc.a, To: pc.a})282282 }283283 changed := s.writeDerived(want)284+ if s.pivot.fit {285+ s.fitPivot(want)286+ }284287 if OnPivot != nil {285288 info := PivotInfo{Cells: len(want), Failed: err != nil, Duration: time.Since(start)}286289 if calc != nil {@@ -394,6 +397,21 @@ func (s *Sheet) setDerived(a Addr, c *Cell) {
394397 }395398 }396399 400+// maxPivotWidth caps how wide fitting makes a pivot's column.401+const maxPivotWidth = 30402+403+// fitPivot widens the result's columns to their widest text, never below404+// the default width.405+func (s *Sheet) fitPivot(cells []pivotCell) {406+ widest := map[int]int{}407+ for _, pc := range cells {408+ widest[pc.a.Col] = max(widest[pc.a.Col], len([]rune(s.ShownText(pc.a))))409+ }410+ for col, w := range widest {411+ s.setWidth(col, clampInt(w+2, DefaultWidth, maxPivotWidth))412+ }413+}414+397415 // derivedInput is what the formula bar shows for a result: the value.398416 func derivedInput(v Value) string {399417 if v.Kind == Text {@@ -144,6 +144,7 @@ func (s *Sheet) ColWidth(c int) int {
144144 145145 // SetColWidth sets column c's width; w <= 0 resets it to the default.146146 func (s *Sheet) SetColWidth(c, w int) {147+ s.pivot.fit = false // the user's widths win over the pivot's147148 s.change("column width", colRect(c, c), func() { s.setWidth(c, w) })148149 }149150 @@ -223,7 +223,7 @@ func (w *Workbook) DuplicateSheet(s *Sheet) (*Sheet, error) {
223223 cp.view = s.view224224 cp.view.filter = s.view.filter.clone()225225 cp.charts = slices.Clone(s.charts)226- cp.pivot = pivotState{def: s.pivot.def.clone(), stale: s.pivot.def != nil, out: s.pivot.out}226+ cp.pivot = pivotState{def: s.pivot.def.clone(), stale: s.pivot.def != nil, out: s.pivot.out, fit: s.pivot.fit}227227 w.change(cp, "duplicate "+s.name, Rect{}, func() {228228 w.recordSheets()229229 w.insert(cp, w.Index(s)+1)