We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
frequency histogram for number is wrong
**Small description**

With [this data](https://github.com/saulpw/visidata/files/5543325/a.txt), when type casting year column to number `#`, and asking for frequency `F`, bins are wrong. I works correctly when not type casting.

**Expected result**

(obtained without type casting and moving 2020 to the top)

```
year	count	percent	histogram
2020	11	7.69	*********************************************
2019	12	8.39	**************************************************
2018	12	8.39	**************************************************
2017	12	8.39	**************************************************
2016	12	8.39	**************************************************
2015	12	8.39	**************************************************
2014	12	8.39	**************************************************
2013	12	8.39	**************************************************
2012	12	8.39	**************************************************
2011	12	8.39	**************************************************
2010	12	8.39	**************************************************
2009	12	8.39	**************************************************
```


**Actual result with screenshot**

(typecasting to number)

```
year	count	percent	histogram
2009	12	8.39	**************************
2010	12	8.39	**************************
2011	12	8.39	**************************
2012	12	8.39	**************************
2013	12	8.39	**************************
2014	12	8.39	**************************
2015	12	8.39	**************************
2016	12	8.39	**************************
2017	12	8.39	**************************
2018	12	8.39	**************************
2019	23	16.08	**************************************************
2020	0	0.00	
```


**Steps to reproduce with sample data and a .vd**

* [data](https://github.com/saulpw/visidata/files/5543325/a.txt)
* [commands](https://github.com/saulpw/visidata/files/5543339/a.vd.txt)

**Additional context**

version `saul.pw/VisiData v2.0.1`

it seems there is a limit to 11 bins and the rest is empty. The more I put data, the more is aggregated in the 11th bin.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `visidata/pivot.py`
Content:
```
1 import collections
2 from visidata import *
3 
4 
5 # discrete_keys = tuple of formatted discrete keys that group the row
6 # numeric_key is a range
7 # sourcerows is list(all source.rows in group)
8 # pivotrows is { pivot_values: list(source.rows in group with pivot_values) }
9 PivotGroupRow = collections.namedtuple('PivotGroupRow', 'discrete_keys numeric_key sourcerows pivotrows'.split())
10 
11 def Pivot(source, groupByCols, pivotCols):
12     return PivotSheet('',
13             groupByCols,
14             pivotCols,
15             source=source)
16 
17 def makeErrorKey(col):
18     if col.type is date:
19         return date.min # date('2000-01-01')
20     else:
21         return col.type()
22 
23 def formatRange(col, numeric_key):
24     a, b = numeric_key
25     nankey = makeErrorKey(col)
26     if a is nankey and b is nankey:
27         return '#ERR'
28     elif a == b:
29         return col.format(a)
30     return ' - '.join(col.format(x) for x in numeric_key)
31 
32 class RangeColumn(Column):
33     def format(self, typedval):
34         if typedval is None:
35             return None
36         return formatRange(self.origcol, typedval)
37 
38 class PivotSheet(Sheet):
39     'Summarize key columns in pivot table and display as new sheet.'
40     rowtype = 'grouped rows'  # rowdef: PivotGroupRow
41     def __init__(self, name, groupByCols, pivotCols, **kwargs):
42         super().__init__(name, **kwargs)
43 
44         self.pivotCols = pivotCols  # whose values become columns
45         self.groupByCols = groupByCols  # whose values become rows
46 
47     def isNumericRange(self, col):
48         return isNumeric(col) and self.source.options.numeric_binning
49 
50     def initCols(self):
51         self.columns = []
52 
53         # add key columns (grouped by)
54         for colnum, c in enumerate(self.groupByCols):
55             if c in self.pivotCols:
56                 continue
57 
58             if self.isNumericRange(c):
59                 newcol = RangeColumn(c.name, origcol=c, width=c.width and c.width*2, getter=lambda c,r: r.numeric_key)
60             else:
61                 newcol = Column(c.name, width=c.width, fmtstr=c.fmtstr,
62                                   type=c.type if c.type in vd.typemap else anytype,
63                                   origcol=c,
64                                   getter=lambda col,row,i=colnum: row.discrete_keys[i],
65                                   setter=lambda col,row,val,i=colnum: setitem(row.discrete_keys, i, val) and col.origcol.setValues(row.sourcerows, val))
66 
67             self.addColumn(newcol)
68 
69         self.setKeys(self.columns)
70 
71     def openRow(self, row):
72         'open sheet of source rows aggregated in current pivot row'
73         vs = copy(self.source)
74         vs.name += "_%s"%"+".join(row.discrete_keys)
75         vs.rows = sum(row.pivotrows.values(), [])
76         return vs
77 
78     def openCell(self, col, row):
79         'open sheet of source rows aggregated in current pivot cell'
80         vs = copy(self.source)
81         vs.name += "_%s"%col.aggvalue
82         vs.rows = row.pivotrows.get(col.aggvalue, [])
83         return vs
84 
85     def reload(self):
86         self.initCols()
87 
88         # two different threads for better interactive display
89         self.addAggregateCols()
90         self.groupRows()
91 
92     @asyncthread
93     def addAggregateCols(self):
94         # add aggregated columns
95         aggcols = {  # [Column] -> list(aggregators)
96             sourcecol: sourcecol.aggregators
97                 for sourcecol in self.source.visibleCols
98                     if hasattr(sourcecol, 'aggregators')
99         } or {  # if pivot given but no aggregators specified
100             sourcecol: [vd.aggregators["count"]]
101                 for sourcecol in self.pivotCols
102         }
103 
104         if not aggcols:
105 #            self.addColumn(ColumnAttr('count', 'sourcerows', type=vlen))
106             return
107 
108         # aggregators without pivot
109         if not self.pivotCols:
110             for aggcol, aggregatorlist in aggcols.items():
111                 for aggregator in aggregatorlist:
112                     aggname = '%s_%s' % (aggcol.name, aggregator.name)
113 
114                     c = Column(aggname,
115                                 type=aggregator.type or aggcol.type,
116                                 getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.sourcerows))
117                     self.addColumn(c)
118 
119         # add pivoted columns
120         for pivotcol in self.pivotCols:
121             allValues = set()
122             for value in Progress(pivotcol.getValues(self.source.rows), 'pivoting', total=len(self.source.rows)):
123                 if value in allValues:
124                     continue
125                 allValues.add(value)
126 
127                 if len(self.pivotCols) > 1:
128                     valname = '%s_%s' % (pivotcol.name, value)
129                 else:
130                     valname = str(value)
131 
132                 for aggcol, aggregatorlist in aggcols.items():
133                     for aggregator in aggregatorlist:
134                         if len(aggcols) > 1: #  if more than one aggregated column, include that column name in the new column name
135                             aggname = '%s_%s' % (aggcol.name, aggregator.name)
136                         else:
137                             aggname = aggregator.name
138 
139 
140                         if len(aggregatorlist) > 1 or len(aggcols) > 1:
141                             colname = '%s_%s' % (aggname, valname)
142                             if not self.name:
143                                 self.name = self.source.name+'_pivot_'+''.join(c.name for c in self.pivotCols)
144                         else:
145                             colname = valname
146                             if not self.name:
147                                 self.name = self.source.name+'_pivot_'+''.join(c.name for c in self.pivotCols) + '_' + aggname
148 
149                         c = Column(colname,
150                                     type=aggregator.type or aggcol.type,
151                                     aggvalue=value,
152                                     getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.pivotrows.get(col.aggvalue, [])))
153                         self.addColumn(c)
154 
155 #                    if aggregator.name != 'count':  # already have count above
156 #                        c = Column('Total_' + aggcol.name,
157 #                                    type=aggregator.type or aggcol.type,
158 #                                    getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.sourcerows))
159 #                        self.addColumn(c)
160 
161     @asyncthread
162     def groupRows(self, rowfunc=None):
163         self.rows = []
164 
165         discreteCols = [c for c in self.groupByCols if not self.isNumericRange(c)]
166 
167         numericCols = [c for c in self.groupByCols if self.isNumericRange(c)]
168 
169         if len(numericCols) > 1:
170             vd.fail('only one numeric column can be binned')
171 
172         numericBins = []
173         degenerateBinning = False
174         if numericCols:
175             nbins = options.histogram_bins or int(len(self.source.rows) ** (1./2))
176             vals = tuple(numericCols[0].getValues(self.source.rows))
177             minval = min(vals)
178             maxval = max(vals)
179             width = (maxval - minval)/nbins
180 
181             if width == 0:
182                 # only one value (and maybe errors)
183                 numericBins = [(minval, maxval)]
184             elif numericCols[0].type in (int, vlen) and nbins > (maxval - minval):
185                 # more bins than int vals, just use the vals
186                 degenerateBinning = True
187                 numericBins = [(minval+i, minval+i) for i in range(maxval-minval+1)]
188             else:
189                 numericBins = [(minval+width*i, minval+width*(i+1)) for i in range(nbins)]
190 
191         # group rows by their keys (groupByCols), and separate by their pivot values (pivotCols)
192         groups = {}  # [formattedDiscreteKeys] -> (numericGroupRows:dict(formattedNumericKeyRange -> PivotGroupRow), groupRow:PivotGroupRow)  # groupRow is main/error row
193 
194         for sourcerow in Progress(self.source.iterrows(), 'grouping', total=self.source.nRows):
195             discreteKeys = list(forward(origcol.getTypedValue(sourcerow)) for origcol in discreteCols)
196 
197             # wrapply will pass-through a key-able TypedWrapper
198             formattedDiscreteKeys = tuple(wrapply(c.format, v) for v, c in zip(discreteKeys, discreteCols))
199 
200             numericGroupRows, groupRow = groups.get(formattedDiscreteKeys, (None, None))
201             if numericGroupRows is None:
202                 # add new group rows
203                 numericGroupRows = {formatRange(numericCols[0], numRange): PivotGroupRow(discreteKeys, numRange, [], {}) for numRange in numericBins}
204                 groups[formattedDiscreteKeys] = (numericGroupRows, None)
205                 for r in numericGroupRows.values():
206                     self.addRow(r)
207 
208             # find the grouprow this sourcerow belongs in, by numericbin
209             if numericCols:
210                 try:
211                     val = numericCols[0].getValue(sourcerow)
212                     if val is not None:
213                         val = numericCols[0].type(val)
214                     if not width:
215                         binidx = 0
216                     elif degenerateBinning:
217                         binidx = val-minval
218                     else:
219                         binidx = int((val-minval)//width)
220                     groupRow = numericGroupRows[formatRange(numericCols[0], numericBins[min(binidx, nbins-1)])]
221                 except Exception as e:
222                     # leave in main/error bin
223                     pass
224 
225             # add the main bin if no numeric bin (error, or no numeric cols)
226             if groupRow is None:
227                 nankey = makeErrorKey(numericCols[0]) if numericCols else 0
228                 groupRow = PivotGroupRow(discreteKeys, (nankey, nankey), [], {})
229                 groups[formattedDiscreteKeys] = (numericGroupRows, groupRow)
230                 self.addRow(groupRow)
231 
232             # add the sourcerow to its all bin
233             groupRow.sourcerows.append(sourcerow)
234 
235             # separate by pivot value
236             for col in self.pivotCols:
237                 varval = col.getTypedValue(sourcerow)
238                 matchingRows = groupRow.pivotrows.get(varval)
239                 if matchingRows is None:
240                     matchingRows = groupRow.pivotrows[varval] = []
241                 matchingRows.append(sourcerow)
242 
243             if rowfunc:
244                 rowfunc(groupRow)
245 
246         # automatically add cache to all columns now that everything is binned
247         for c in self.nonKeyVisibleCols:
248             c.setCache(True)
249 
250 
251 Sheet.addCommand('W', 'pivot', 'vd.push(Pivot(sheet, keyCols, [cursorCol]))', 'open Pivot Table: group rows by key column and summarize current column')
252
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/visidata/pivot.py b/visidata/pivot.py
--- a/visidata/pivot.py
+++ b/visidata/pivot.py
@@ -181,10 +181,11 @@
             if width == 0:
                 # only one value (and maybe errors)
                 numericBins = [(minval, maxval)]
-            elif numericCols[0].type in (int, vlen) and nbins > (maxval - minval):
-                # more bins than int vals, just use the vals
+            elif (numericCols[0].type in (int, vlen) and nbins > (maxval - minval)) or (width == 1):
+                # (more bins than int vals) or (if bins are of width 1), just use the vals as bins
                 degenerateBinning = True
-                numericBins = [(minval+i, minval+i) for i in range(maxval-minval+1)]
+                numericBins = [(val, val) for val in sorted(set(vals))]
+                nbins = len(numericBins)
             else:
                 numericBins = [(minval+width*i, minval+width*(i+1)) for i in range(nbins)]
 
@@ -214,7 +215,8 @@
                     if not width:
                         binidx = 0
                     elif degenerateBinning:
-                        binidx = val-minval
+                        # in degenerate binning, each val has its own bin
+                        binidx = numericBins.index((val, val))
                     else:
                         binidx = int((val-minval)//width)
                     groupRow = numericGroupRows[formatRange(numericCols[0], numericBins[min(binidx, nbins-1)])]

{"golden_diff": "diff --git a/visidata/pivot.py b/visidata/pivot.py\n--- a/visidata/pivot.py\n+++ b/visidata/pivot.py\n@@ -181,10 +181,11 @@\n             if width == 0:\n                 # only one value (and maybe errors)\n                 numericBins = [(minval, maxval)]\n-            elif numericCols[0].type in (int, vlen) and nbins > (maxval - minval):\n-                # more bins than int vals, just use the vals\n+            elif (numericCols[0].type in (int, vlen) and nbins > (maxval - minval)) or (width == 1):\n+                # (more bins than int vals) or (if bins are of width 1), just use the vals as bins\n                 degenerateBinning = True\n-                numericBins = [(minval+i, minval+i) for i in range(maxval-minval+1)]\n+                numericBins = [(val, val) for val in sorted(set(vals))]\n+                nbins = len(numericBins)\n             else:\n                 numericBins = [(minval+width*i, minval+width*(i+1)) for i in range(nbins)]\n \n@@ -214,7 +215,8 @@\n                     if not width:\n                         binidx = 0\n                     elif degenerateBinning:\n-                        binidx = val-minval\n+                        # in degenerate binning, each val has its own bin\n+                        binidx = numericBins.index((val, val))\n                     else:\n                         binidx = int((val-minval)//width)\n                     groupRow = numericGroupRows[formatRange(numericCols[0], numericBins[min(binidx, nbins-1)])]\n", "issue": "frequency histogram for number is wrong\n**Small description**\r\n\r\nWith [this data](https://github.com/saulpw/visidata/files/5543325/a.txt), when type casting year column to number `#`, and asking for frequency `F`, bins are wrong. I works correctly when not type casting.\r\n\r\n**Expected result**\r\n\r\n(obtained without type casting and moving 2020 to the top)\r\n\r\n```\r\nyear\tcount\tpercent\thistogram\r\n2020\t11\t7.69\t*********************************************\r\n2019\t12\t8.39\t**************************************************\r\n2018\t12\t8.39\t**************************************************\r\n2017\t12\t8.39\t**************************************************\r\n2016\t12\t8.39\t**************************************************\r\n2015\t12\t8.39\t**************************************************\r\n2014\t12\t8.39\t**************************************************\r\n2013\t12\t8.39\t**************************************************\r\n2012\t12\t8.39\t**************************************************\r\n2011\t12\t8.39\t**************************************************\r\n2010\t12\t8.39\t**************************************************\r\n2009\t12\t8.39\t**************************************************\r\n```\r\n\r\n\r\n**Actual result with screenshot**\r\n\r\n(typecasting to number)\r\n\r\n```\r\nyear\tcount\tpercent\thistogram\r\n2009\t12\t8.39\t**************************\r\n2010\t12\t8.39\t**************************\r\n2011\t12\t8.39\t**************************\r\n2012\t12\t8.39\t**************************\r\n2013\t12\t8.39\t**************************\r\n2014\t12\t8.39\t**************************\r\n2015\t12\t8.39\t**************************\r\n2016\t12\t8.39\t**************************\r\n2017\t12\t8.39\t**************************\r\n2018\t12\t8.39\t**************************\r\n2019\t23\t16.08\t**************************************************\r\n2020\t0\t0.00\t\r\n```\r\n\r\n\r\n**Steps to reproduce with sample data and a .vd**\r\n\r\n* [data](https://github.com/saulpw/visidata/files/5543325/a.txt)\r\n* [commands](https://github.com/saulpw/visidata/files/5543339/a.vd.txt)\r\n\r\n**Additional context**\r\n\r\nversion `saul.pw/VisiData v2.0.1`\r\n\r\nit seems there is a limit to 11 bins and the rest is empty. The more I put data, the more is aggregated in the 11th bin.\r\n\n", "before_files": [{"content": "import collections\nfrom visidata import *\n\n\n# discrete_keys = tuple of formatted discrete keys that group the row\n# numeric_key is a range\n# sourcerows is list(all source.rows in group)\n# pivotrows is { pivot_values: list(source.rows in group with pivot_values) }\nPivotGroupRow = collections.namedtuple('PivotGroupRow', 'discrete_keys numeric_key sourcerows pivotrows'.split())\n\ndef Pivot(source, groupByCols, pivotCols):\n    return PivotSheet('',\n            groupByCols,\n            pivotCols,\n            source=source)\n\ndef makeErrorKey(col):\n    if col.type is date:\n        return date.min # date('2000-01-01')\n    else:\n        return col.type()\n\ndef formatRange(col, numeric_key):\n    a, b = numeric_key\n    nankey = makeErrorKey(col)\n    if a is nankey and b is nankey:\n        return '#ERR'\n    elif a == b:\n        return col.format(a)\n    return ' - '.join(col.format(x) for x in numeric_key)\n\nclass RangeColumn(Column):\n    def format(self, typedval):\n        if typedval is None:\n            return None\n        return formatRange(self.origcol, typedval)\n\nclass PivotSheet(Sheet):\n    'Summarize key columns in pivot table and display as new sheet.'\n    rowtype = 'grouped rows'  # rowdef: PivotGroupRow\n    def __init__(self, name, groupByCols, pivotCols, **kwargs):\n        super().__init__(name, **kwargs)\n\n        self.pivotCols = pivotCols  # whose values become columns\n        self.groupByCols = groupByCols  # whose values become rows\n\n    def isNumericRange(self, col):\n        return isNumeric(col) and self.source.options.numeric_binning\n\n    def initCols(self):\n        self.columns = []\n\n        # add key columns (grouped by)\n        for colnum, c in enumerate(self.groupByCols):\n            if c in self.pivotCols:\n                continue\n\n            if self.isNumericRange(c):\n                newcol = RangeColumn(c.name, origcol=c, width=c.width and c.width*2, getter=lambda c,r: r.numeric_key)\n            else:\n                newcol = Column(c.name, width=c.width, fmtstr=c.fmtstr,\n                                  type=c.type if c.type in vd.typemap else anytype,\n                                  origcol=c,\n                                  getter=lambda col,row,i=colnum: row.discrete_keys[i],\n                                  setter=lambda col,row,val,i=colnum: setitem(row.discrete_keys, i, val) and col.origcol.setValues(row.sourcerows, val))\n\n            self.addColumn(newcol)\n\n        self.setKeys(self.columns)\n\n    def openRow(self, row):\n        'open sheet of source rows aggregated in current pivot row'\n        vs = copy(self.source)\n        vs.name += \"_%s\"%\"+\".join(row.discrete_keys)\n        vs.rows = sum(row.pivotrows.values(), [])\n        return vs\n\n    def openCell(self, col, row):\n        'open sheet of source rows aggregated in current pivot cell'\n        vs = copy(self.source)\n        vs.name += \"_%s\"%col.aggvalue\n        vs.rows = row.pivotrows.get(col.aggvalue, [])\n        return vs\n\n    def reload(self):\n        self.initCols()\n\n        # two different threads for better interactive display\n        self.addAggregateCols()\n        self.groupRows()\n\n    @asyncthread\n    def addAggregateCols(self):\n        # add aggregated columns\n        aggcols = {  # [Column] -> list(aggregators)\n            sourcecol: sourcecol.aggregators\n                for sourcecol in self.source.visibleCols\n                    if hasattr(sourcecol, 'aggregators')\n        } or {  # if pivot given but no aggregators specified\n            sourcecol: [vd.aggregators[\"count\"]]\n                for sourcecol in self.pivotCols\n        }\n\n        if not aggcols:\n#            self.addColumn(ColumnAttr('count', 'sourcerows', type=vlen))\n            return\n\n        # aggregators without pivot\n        if not self.pivotCols:\n            for aggcol, aggregatorlist in aggcols.items():\n                for aggregator in aggregatorlist:\n                    aggname = '%s_%s' % (aggcol.name, aggregator.name)\n\n                    c = Column(aggname,\n                                type=aggregator.type or aggcol.type,\n                                getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.sourcerows))\n                    self.addColumn(c)\n\n        # add pivoted columns\n        for pivotcol in self.pivotCols:\n            allValues = set()\n            for value in Progress(pivotcol.getValues(self.source.rows), 'pivoting', total=len(self.source.rows)):\n                if value in allValues:\n                    continue\n                allValues.add(value)\n\n                if len(self.pivotCols) > 1:\n                    valname = '%s_%s' % (pivotcol.name, value)\n                else:\n                    valname = str(value)\n\n                for aggcol, aggregatorlist in aggcols.items():\n                    for aggregator in aggregatorlist:\n                        if len(aggcols) > 1: #  if more than one aggregated column, include that column name in the new column name\n                            aggname = '%s_%s' % (aggcol.name, aggregator.name)\n                        else:\n                            aggname = aggregator.name\n\n\n                        if len(aggregatorlist) > 1 or len(aggcols) > 1:\n                            colname = '%s_%s' % (aggname, valname)\n                            if not self.name:\n                                self.name = self.source.name+'_pivot_'+''.join(c.name for c in self.pivotCols)\n                        else:\n                            colname = valname\n                            if not self.name:\n                                self.name = self.source.name+'_pivot_'+''.join(c.name for c in self.pivotCols) + '_' + aggname\n\n                        c = Column(colname,\n                                    type=aggregator.type or aggcol.type,\n                                    aggvalue=value,\n                                    getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.pivotrows.get(col.aggvalue, [])))\n                        self.addColumn(c)\n\n#                    if aggregator.name != 'count':  # already have count above\n#                        c = Column('Total_' + aggcol.name,\n#                                    type=aggregator.type or aggcol.type,\n#                                    getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.sourcerows))\n#                        self.addColumn(c)\n\n    @asyncthread\n    def groupRows(self, rowfunc=None):\n        self.rows = []\n\n        discreteCols = [c for c in self.groupByCols if not self.isNumericRange(c)]\n\n        numericCols = [c for c in self.groupByCols if self.isNumericRange(c)]\n\n        if len(numericCols) > 1:\n            vd.fail('only one numeric column can be binned')\n\n        numericBins = []\n        degenerateBinning = False\n        if numericCols:\n            nbins = options.histogram_bins or int(len(self.source.rows) ** (1./2))\n            vals = tuple(numericCols[0].getValues(self.source.rows))\n            minval = min(vals)\n            maxval = max(vals)\n            width = (maxval - minval)/nbins\n\n            if width == 0:\n                # only one value (and maybe errors)\n                numericBins = [(minval, maxval)]\n            elif numericCols[0].type in (int, vlen) and nbins > (maxval - minval):\n                # more bins than int vals, just use the vals\n                degenerateBinning = True\n                numericBins = [(minval+i, minval+i) for i in range(maxval-minval+1)]\n            else:\n                numericBins = [(minval+width*i, minval+width*(i+1)) for i in range(nbins)]\n\n        # group rows by their keys (groupByCols), and separate by their pivot values (pivotCols)\n        groups = {}  # [formattedDiscreteKeys] -> (numericGroupRows:dict(formattedNumericKeyRange -> PivotGroupRow), groupRow:PivotGroupRow)  # groupRow is main/error row\n\n        for sourcerow in Progress(self.source.iterrows(), 'grouping', total=self.source.nRows):\n            discreteKeys = list(forward(origcol.getTypedValue(sourcerow)) for origcol in discreteCols)\n\n            # wrapply will pass-through a key-able TypedWrapper\n            formattedDiscreteKeys = tuple(wrapply(c.format, v) for v, c in zip(discreteKeys, discreteCols))\n\n            numericGroupRows, groupRow = groups.get(formattedDiscreteKeys, (None, None))\n            if numericGroupRows is None:\n                # add new group rows\n                numericGroupRows = {formatRange(numericCols[0], numRange): PivotGroupRow(discreteKeys, numRange, [], {}) for numRange in numericBins}\n                groups[formattedDiscreteKeys] = (numericGroupRows, None)\n                for r in numericGroupRows.values():\n                    self.addRow(r)\n\n            # find the grouprow this sourcerow belongs in, by numericbin\n            if numericCols:\n                try:\n                    val = numericCols[0].getValue(sourcerow)\n                    if val is not None:\n                        val = numericCols[0].type(val)\n                    if not width:\n                        binidx = 0\n                    elif degenerateBinning:\n                        binidx = val-minval\n                    else:\n                        binidx = int((val-minval)//width)\n                    groupRow = numericGroupRows[formatRange(numericCols[0], numericBins[min(binidx, nbins-1)])]\n                except Exception as e:\n                    # leave in main/error bin\n                    pass\n\n            # add the main bin if no numeric bin (error, or no numeric cols)\n            if groupRow is None:\n                nankey = makeErrorKey(numericCols[0]) if numericCols else 0\n                groupRow = PivotGroupRow(discreteKeys, (nankey, nankey), [], {})\n                groups[formattedDiscreteKeys] = (numericGroupRows, groupRow)\n                self.addRow(groupRow)\n\n            # add the sourcerow to its all bin\n            groupRow.sourcerows.append(sourcerow)\n\n            # separate by pivot value\n            for col in self.pivotCols:\n                varval = col.getTypedValue(sourcerow)\n                matchingRows = groupRow.pivotrows.get(varval)\n                if matchingRows is None:\n                    matchingRows = groupRow.pivotrows[varval] = []\n                matchingRows.append(sourcerow)\n\n            if rowfunc:\n                rowfunc(groupRow)\n\n        # automatically add cache to all columns now that everything is binned\n        for c in self.nonKeyVisibleCols:\n            c.setCache(True)\n\n\nSheet.addCommand('W', 'pivot', 'vd.push(Pivot(sheet, keyCols, [cursorCol]))', 'open Pivot Table: group rows by key column and summarize current column')\n", "path": "visidata/pivot.py"}], "after_files": [{"content": "import collections\nfrom visidata import *\n\n\n# discrete_keys = tuple of formatted discrete keys that group the row\n# numeric_key is a range\n# sourcerows is list(all source.rows in group)\n# pivotrows is { pivot_values: list(source.rows in group with pivot_values) }\nPivotGroupRow = collections.namedtuple('PivotGroupRow', 'discrete_keys numeric_key sourcerows pivotrows'.split())\n\ndef Pivot(source, groupByCols, pivotCols):\n    return PivotSheet('',\n            groupByCols,\n            pivotCols,\n            source=source)\n\ndef makeErrorKey(col):\n    if col.type is date:\n        return date.min # date('2000-01-01')\n    else:\n        return col.type()\n\ndef formatRange(col, numeric_key):\n    a, b = numeric_key\n    nankey = makeErrorKey(col)\n    if a is nankey and b is nankey:\n        return '#ERR'\n    elif a == b:\n        return col.format(a)\n    return ' - '.join(col.format(x) for x in numeric_key)\n\nclass RangeColumn(Column):\n    def format(self, typedval):\n        if typedval is None:\n            return None\n        return formatRange(self.origcol, typedval)\n\nclass PivotSheet(Sheet):\n    'Summarize key columns in pivot table and display as new sheet.'\n    rowtype = 'grouped rows'  # rowdef: PivotGroupRow\n    def __init__(self, name, groupByCols, pivotCols, **kwargs):\n        super().__init__(name, **kwargs)\n\n        self.pivotCols = pivotCols  # whose values become columns\n        self.groupByCols = groupByCols  # whose values become rows\n\n    def isNumericRange(self, col):\n        return isNumeric(col) and self.source.options.numeric_binning\n\n    def initCols(self):\n        self.columns = []\n\n        # add key columns (grouped by)\n        for colnum, c in enumerate(self.groupByCols):\n            if c in self.pivotCols:\n                continue\n\n            if self.isNumericRange(c):\n                newcol = RangeColumn(c.name, origcol=c, width=c.width and c.width*2, getter=lambda c,r: r.numeric_key)\n            else:\n                newcol = Column(c.name, width=c.width, fmtstr=c.fmtstr,\n                                  type=c.type if c.type in vd.typemap else anytype,\n                                  origcol=c,\n                                  getter=lambda col,row,i=colnum: row.discrete_keys[i],\n                                  setter=lambda col,row,val,i=colnum: setitem(row.discrete_keys, i, val) and col.origcol.setValues(row.sourcerows, val))\n\n            self.addColumn(newcol)\n\n        self.setKeys(self.columns)\n\n    def openRow(self, row):\n        'open sheet of source rows aggregated in current pivot row'\n        vs = copy(self.source)\n        vs.name += \"_%s\"%\"+\".join(row.discrete_keys)\n        vs.rows = sum(row.pivotrows.values(), [])\n        return vs\n\n    def openCell(self, col, row):\n        'open sheet of source rows aggregated in current pivot cell'\n        vs = copy(self.source)\n        vs.name += \"_%s\"%col.aggvalue\n        vs.rows = row.pivotrows.get(col.aggvalue, [])\n        return vs\n\n    def reload(self):\n        self.initCols()\n\n        # two different threads for better interactive display\n        self.addAggregateCols()\n        self.groupRows()\n\n    @asyncthread\n    def addAggregateCols(self):\n        # add aggregated columns\n        aggcols = {  # [Column] -> list(aggregators)\n            sourcecol: sourcecol.aggregators\n                for sourcecol in self.source.visibleCols\n                    if hasattr(sourcecol, 'aggregators')\n        } or {  # if pivot given but no aggregators specified\n            sourcecol: [vd.aggregators[\"count\"]]\n                for sourcecol in self.pivotCols\n        }\n\n        if not aggcols:\n#            self.addColumn(ColumnAttr('count', 'sourcerows', type=vlen))\n            return\n\n        # aggregators without pivot\n        if not self.pivotCols:\n            for aggcol, aggregatorlist in aggcols.items():\n                for aggregator in aggregatorlist:\n                    aggname = '%s_%s' % (aggcol.name, aggregator.name)\n\n                    c = Column(aggname,\n                                type=aggregator.type or aggcol.type,\n                                getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.sourcerows))\n                    self.addColumn(c)\n\n        # add pivoted columns\n        for pivotcol in self.pivotCols:\n            allValues = set()\n            for value in Progress(pivotcol.getValues(self.source.rows), 'pivoting', total=len(self.source.rows)):\n                if value in allValues:\n                    continue\n                allValues.add(value)\n\n                if len(self.pivotCols) > 1:\n                    valname = '%s_%s' % (pivotcol.name, value)\n                else:\n                    valname = str(value)\n\n                for aggcol, aggregatorlist in aggcols.items():\n                    for aggregator in aggregatorlist:\n                        if len(aggcols) > 1: #  if more than one aggregated column, include that column name in the new column name\n                            aggname = '%s_%s' % (aggcol.name, aggregator.name)\n                        else:\n                            aggname = aggregator.name\n\n\n                        if len(aggregatorlist) > 1 or len(aggcols) > 1:\n                            colname = '%s_%s' % (aggname, valname)\n                            if not self.name:\n                                self.name = self.source.name+'_pivot_'+''.join(c.name for c in self.pivotCols)\n                        else:\n                            colname = valname\n                            if not self.name:\n                                self.name = self.source.name+'_pivot_'+''.join(c.name for c in self.pivotCols) + '_' + aggname\n\n                        c = Column(colname,\n                                    type=aggregator.type or aggcol.type,\n                                    aggvalue=value,\n                                    getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.pivotrows.get(col.aggvalue, [])))\n                        self.addColumn(c)\n\n#                    if aggregator.name != 'count':  # already have count above\n#                        c = Column('Total_' + aggcol.name,\n#                                    type=aggregator.type or aggcol.type,\n#                                    getter=lambda col,row,aggcol=aggcol,agg=aggregator: agg(aggcol, row.sourcerows))\n#                        self.addColumn(c)\n\n    @asyncthread\n    def groupRows(self, rowfunc=None):\n        self.rows = []\n\n        discreteCols = [c for c in self.groupByCols if not self.isNumericRange(c)]\n\n        numericCols = [c for c in self.groupByCols if self.isNumericRange(c)]\n\n        if len(numericCols) > 1:\n            vd.fail('only one numeric column can be binned')\n\n        numericBins = []\n        degenerateBinning = False\n        if numericCols:\n            nbins = options.histogram_bins or int(len(self.source.rows) ** (1./2))\n            vals = tuple(numericCols[0].getValues(self.source.rows))\n            minval = min(vals)\n            maxval = max(vals)\n            width = (maxval - minval)/nbins\n\n            if width == 0:\n                # only one value (and maybe errors)\n                numericBins = [(minval, maxval)]\n            elif (numericCols[0].type in (int, vlen) and nbins > (maxval - minval)) or (width == 1):\n                # (more bins than int vals) or (if bins are of width 1), just use the vals as bins\n                degenerateBinning = True\n                numericBins = [(val, val) for val in sorted(set(vals))]\n                nbins = len(numericBins)\n            else:\n                numericBins = [(minval+width*i, minval+width*(i+1)) for i in range(nbins)]\n\n        # group rows by their keys (groupByCols), and separate by their pivot values (pivotCols)\n        groups = {}  # [formattedDiscreteKeys] -> (numericGroupRows:dict(formattedNumericKeyRange -> PivotGroupRow), groupRow:PivotGroupRow)  # groupRow is main/error row\n\n        for sourcerow in Progress(self.source.iterrows(), 'grouping', total=self.source.nRows):\n            discreteKeys = list(forward(origcol.getTypedValue(sourcerow)) for origcol in discreteCols)\n\n            # wrapply will pass-through a key-able TypedWrapper\n            formattedDiscreteKeys = tuple(wrapply(c.format, v) for v, c in zip(discreteKeys, discreteCols))\n\n            numericGroupRows, groupRow = groups.get(formattedDiscreteKeys, (None, None))\n            if numericGroupRows is None:\n                # add new group rows\n                numericGroupRows = {formatRange(numericCols[0], numRange): PivotGroupRow(discreteKeys, numRange, [], {}) for numRange in numericBins}\n                groups[formattedDiscreteKeys] = (numericGroupRows, None)\n                for r in numericGroupRows.values():\n                    self.addRow(r)\n\n            # find the grouprow this sourcerow belongs in, by numericbin\n            if numericCols:\n                try:\n                    val = numericCols[0].getValue(sourcerow)\n                    if val is not None:\n                        val = numericCols[0].type(val)\n                    if not width:\n                        binidx = 0\n                    elif degenerateBinning:\n                        # in degenerate binning, each val has its own bin\n                        binidx = numericBins.index((val, val))\n                    else:\n                        binidx = int((val-minval)//width)\n                    groupRow = numericGroupRows[formatRange(numericCols[0], numericBins[min(binidx, nbins-1)])]\n                except Exception as e:\n                    # leave in main/error bin\n                    pass\n\n            # add the main bin if no numeric bin (error, or no numeric cols)\n            if groupRow is None:\n                nankey = makeErrorKey(numericCols[0]) if numericCols else 0\n                groupRow = PivotGroupRow(discreteKeys, (nankey, nankey), [], {})\n                groups[formattedDiscreteKeys] = (numericGroupRows, groupRow)\n                self.addRow(groupRow)\n\n            # add the sourcerow to its all bin\n            groupRow.sourcerows.append(sourcerow)\n\n            # separate by pivot value\n            for col in self.pivotCols:\n                varval = col.getTypedValue(sourcerow)\n                matchingRows = groupRow.pivotrows.get(varval)\n                if matchingRows is None:\n                    matchingRows = groupRow.pivotrows[varval] = []\n                matchingRows.append(sourcerow)\n\n            if rowfunc:\n                rowfunc(groupRow)\n\n        # automatically add cache to all columns now that everything is binned\n        for c in self.nonKeyVisibleCols:\n            c.setCache(True)\n\n\nSheet.addCommand('W', 'pivot', 'vd.push(Pivot(sheet, keyCols, [cursorCol]))', 'open Pivot Table: group rows by key column and summarize current column')\n", "path": "visidata/pivot.py"}]}