Skip to content

Commit a02bccc

Browse files
committed
add statiscial plot and better UI
1 parent 48bbbb6 commit a02bccc

File tree

10 files changed

+1469
-102
lines changed

10 files changed

+1469
-102
lines changed

README.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
- [Features in Detail](#features-in-detail)
3131
- [Progressive Loading](#progressive-loading)
3232
- [Data Types and Sorting](#data-types-and-sorting)
33+
- [Statistics and Visualization](#statistics-and-visualization)
3334
- [Search](#search)
3435
- [Column Filter](#column-filter)
3536
- [Text Wrapping](#text-wrapping)
@@ -48,7 +49,7 @@ tv brings spreadsheet-like functionality to your terminal with vim-inspired cont
4849
- **Column filtering** - Show only rows matching specific criteria
4950
- **Flexible sorting** - Sort by any column with intelligent type detection
5051
- **Text wrapping** - Wrap long cell content for better readability
51-
- **Statistics** - View column stats (min/max, mean for numbers; frequency for strings)
52+
- **Statistics & plots** - View column statistics with visual distribution charts
5253
- **Vim keybindings** - Navigate naturally with h/j/k/l and more
5354
- **Pipe support** - Read from stdin for seamless integration with shell pipelines
5455

@@ -271,9 +272,25 @@ tv automatically detects column types and provides intelligent sorting.
271272
- **Numbers:** Numeric order (supports integers, floats, scientific notation, thousands separators)
272273
- **Dates:** Chronological order (supports ISO-8601, US format, EU format, and more)
273274

274-
**View Statistics:** Press `i` to open a comprehensive statistics dialog showing:
275-
- **For numeric columns:** count, min, max, range, sum, mean, median, mode, standard deviation, variance, quartiles (Q1, Q2, Q3), and IQR
276-
- **For string columns:** total values, unique values, frequency distribution with percentages for each value
275+
### Statistics and Visualization
276+
277+
Analyze your data with comprehensive statistics and modern ASCII plots.
278+
279+
**View Statistics:** Press `i` on any column to open an interactive statistics dialog that displays:
280+
281+
**For numeric columns:**
282+
- Summary stats: count, min, max, range, sum
283+
- Central tendency: mean, median, mode
284+
- Dispersion: standard deviation, variance
285+
- Quartiles: Q1, Q2, Q3, and IQR
286+
- **Visual distribution:** Histogram plot showing data distribution
287+
288+
**For categorical/string columns:**
289+
- Total values, unique values, missing/empty count
290+
- Frequency distribution with percentages
291+
- **Visual distribution:** Bar chart of top 15 most frequent values
292+
293+
The statistics dialog features a split-pane layout with numerical stats on the left and an ASCII graph visualization on the right, powered by `asciigraph` for modern, clean plots.
277294

278295
### Search
279296

@@ -427,7 +444,7 @@ tv data.txt -s " "
427444
- **Too many columns?** Use `--columns` to show only what you need
428445
- **Long text?** Press `W` to wrap the current column
429446
- **Wrong sort order?** Press `t` to change the column type, then `s` to re-sort
430-
- **Need stats?** Press `i` for comprehensive statistics including mean, median, quartiles, std dev, and frequency distributions
447+
- **Need insights?** Press `i` for comprehensive statistics with visual plots - histograms for numeric data, frequency charts for categorical data
431448

432449
## License
433450

data/test/products-1000.csv

Lines changed: 1001 additions & 0 deletions
Large diffs are not rendered by default.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
require (
1414
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
1515
github.com/gdamore/encoding v1.0.1 // indirect
16+
github.com/guptarohit/asciigraph v0.7.3 // indirect
1617
github.com/inconshreveable/mousetrap v1.1.0 // indirect
1718
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
1819
github.com/mattn/go-colorable v0.1.14 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uh
77
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
88
github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=
99
github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo=
10+
github.com/guptarohit/asciigraph v0.7.3 h1:p05XDDn7cBTWiBqWb30mrwxd6oU0claAjqeytllnsPY=
11+
github.com/guptarohit/asciigraph v0.7.3/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag=
1012
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
1113
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
1214
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=

install.sh

100644100755
File mode changed.

stats.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package main
33
import (
44
"sort"
55

6+
"github.com/guptarohit/asciigraph"
67
"github.com/montanaflynn/stats"
78
)
89

910
type statsSummary interface {
1011
summary(a []string)
1112
getSummaryData() [][]string
1213
getSummaryStr(a []string) string
14+
getPlot() string
1315
}
1416

1517
type DiscreteStats struct {
@@ -133,6 +135,51 @@ func (s *ContinuousStats) getSummaryStr(a []string) string {
133135
return result
134136
}
135137

138+
// getPlot generates a histogram visualization for continuous data
139+
func (s *ContinuousStats) getPlot() string {
140+
if len(s.data) == 0 {
141+
return "No data to plot"
142+
}
143+
144+
// Create histogram bins
145+
numBins := 20
146+
if len(s.data) < 100 {
147+
numBins = 10
148+
}
149+
if len(s.data) < 20 {
150+
numBins = 5
151+
}
152+
153+
// Calculate bin width
154+
binWidth := (s.max - s.min) / float64(numBins)
155+
if binWidth == 0 {
156+
return "All values are identical"
157+
}
158+
159+
// Initialize bins
160+
bins := make([]float64, numBins)
161+
162+
// Count values in each bin
163+
for _, v := range s.data {
164+
binIndex := int((v - s.min) / binWidth)
165+
if binIndex >= numBins {
166+
binIndex = numBins - 1
167+
}
168+
if binIndex < 0 {
169+
binIndex = 0
170+
}
171+
bins[binIndex]++
172+
}
173+
174+
// Generate the plot
175+
plot := asciigraph.Plot(bins,
176+
asciigraph.Height(15),
177+
asciigraph.Width(60),
178+
asciigraph.Caption("Distribution Histogram"))
179+
180+
return plot
181+
}
182+
136183
func (s *DiscreteStats) summary(a []string) {
137184
s.data = a
138185
s.count = len(a)
@@ -235,3 +282,43 @@ func (s *DiscreteStats) getSummaryStr(a []string) string {
235282

236283
return result
237284
}
285+
286+
// getPlot generates a bar chart visualization for discrete data
287+
func (s *DiscreteStats) getPlot() string {
288+
if len(s.counter) == 0 {
289+
return "No data to plot"
290+
}
291+
292+
// Sort by frequency
293+
type kv struct {
294+
Key string
295+
Value int
296+
}
297+
var ss []kv
298+
for k, v := range s.counter {
299+
ss = append(ss, kv{k, v})
300+
}
301+
sort.Slice(ss, func(i, j int) bool {
302+
return ss[i].Value > ss[j].Value
303+
})
304+
305+
// Take top 15 values for the plot
306+
maxDisplay := 15
307+
if len(ss) < maxDisplay {
308+
maxDisplay = len(ss)
309+
}
310+
311+
// Prepare data for plotting
312+
frequencies := make([]float64, maxDisplay)
313+
for i := 0; i < maxDisplay; i++ {
314+
frequencies[i] = float64(ss[i].Value)
315+
}
316+
317+
// Generate the plot
318+
plot := asciigraph.Plot(frequencies,
319+
asciigraph.Height(12),
320+
asciigraph.Width(60),
321+
asciigraph.Caption("Top Values Frequency"))
322+
323+
return plot
324+
}

stats_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,75 @@ func BenchmarkDiscreteStats(b *testing.B) {
162162
ds.summary(data)
163163
}
164164
}
165+
166+
// ========================================
167+
// Plot Tests
168+
// ========================================
169+
170+
func TestContinuousStats_GetPlot(t *testing.T) {
171+
cs := &ContinuousStats{}
172+
data := []string{"10", "20", "15", "25", "30", "18", "22", "27", "12", "16"}
173+
174+
cs.summary(data)
175+
plot := cs.getPlot()
176+
177+
if len(plot) == 0 {
178+
t.Error("Expected plot output, got empty string")
179+
}
180+
if plot == "No data to plot" {
181+
t.Error("Should generate plot for valid data")
182+
}
183+
t.Logf("Plot output:\n%s", plot)
184+
}
185+
186+
func TestContinuousStats_GetPlot_EmptyData(t *testing.T) {
187+
cs := &ContinuousStats{}
188+
data := []string{}
189+
190+
cs.summary(data)
191+
plot := cs.getPlot()
192+
193+
if plot != "No data to plot" {
194+
t.Errorf("Expected 'No data to plot', got: %s", plot)
195+
}
196+
}
197+
198+
func TestContinuousStats_GetPlot_IdenticalValues(t *testing.T) {
199+
cs := &ContinuousStats{}
200+
data := []string{"42", "42", "42", "42", "42"}
201+
202+
cs.summary(data)
203+
plot := cs.getPlot()
204+
205+
if plot != "All values are identical" {
206+
t.Errorf("Expected 'All values are identical', got: %s", plot)
207+
}
208+
}
209+
210+
func TestDiscreteStats_GetPlot(t *testing.T) {
211+
ds := &DiscreteStats{}
212+
data := []string{"A", "B", "A", "C", "A", "B", "A", "D", "A", "B"}
213+
214+
ds.summary(data)
215+
plot := ds.getPlot()
216+
217+
if len(plot) == 0 {
218+
t.Error("Expected plot output, got empty string")
219+
}
220+
if plot == "No data to plot" {
221+
t.Error("Should generate plot for valid data")
222+
}
223+
t.Logf("Plot output:\n%s", plot)
224+
}
225+
226+
func TestDiscreteStats_GetPlot_EmptyData(t *testing.T) {
227+
ds := &DiscreteStats{}
228+
data := []string{}
229+
230+
ds.summary(data)
231+
plot := ds.getPlot()
232+
233+
if plot != "No data to plot" {
234+
t.Errorf("Expected 'No data to plot', got: %s", plot)
235+
}
236+
}

0 commit comments

Comments
 (0)