├── .gitignore ├── LICENSE.md ├── README.md ├── demo.gif ├── gradient.go ├── iterm.go ├── main.go └── plot.go /.gitignore: -------------------------------------------------------------------------------- 1 | go-sparkline 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | --------------------- 3 | 4 | Copyright (c) 2015 Mathias Leppich 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # go-sparkline 3 | 4 | ![](demo.gif) 5 | 6 | ## Install 7 | ``` shell 8 | # install via go tools 9 | go get github.com/muhqu/go-sparkline 10 | go install github.com/muhqu/go-sparkline 11 | 12 | # verify 13 | go-sparkline --help 14 | ``` 15 | 16 | ## Help 17 | 18 | ``` 19 | $ go-sparkline --help 20 | usage: go-sparkline [] [] 21 | 22 | Flags: 23 | --help Show help. 24 | -s, --stream stream 25 | --animate start animation 26 | --lazy ignore parse errors 27 | --char-size=7:17 Pixel size of a single character. Can also be set via env 28 | ITERM_CHARACTER_SIZE. The default 7:17 corresponds to 12p Monaco. 29 | --rows=3 height in number of rows 30 | --renderer=sparks available renderers: line, sparks, vlines 31 | 32 | Args: 33 | [] Numeric values to render. Can also be read from stdin. 34 | ``` 35 | 36 | ## Examples 37 | 38 | ### simple numbers 39 | ``` 40 | go-sparkline 16 19 18 12 7 4 7 15 25 33 35 32 26 21 41 | # or 42 | echo 16 19 18 12 7 4 7 15 25 33 35 32 26 21 | go-sparkline 43 | ``` 44 | 45 | ### simple json array 46 | ``` 47 | echo '[16,19,18,12,7,4,7,15,25,33,35,32,26,21]' | go-sparkline 48 | ``` 49 | 50 | ### streamed json arrays 51 | ``` 52 | ( 53 | echo '[16,19,18]'; sleep 1; 54 | echo '[12,7,4]'; sleep 1; 55 | echo '[7,15,25]'; sleep 1; 56 | echo '[33,35,32]'; sleep 1; 57 | echo '[26,21]'; 58 | ) | go-sparkline --stream 59 | ``` 60 | 61 | ### streamed ping graph 62 | ``` 63 | ping -n -i 0.3 localhost \ 64 | | awk 'BEGIN{FS="time=|ms"}/time=/{printf "%d\n",$2*1000;fflush()}' \ 65 | | go-sparkline --stream 66 | ``` 67 | 68 | ### aws cloudwatch metric data 69 | ``` 70 | $ aws cloudwatch get-metric-statistics \ 71 | --namespace AWS/ELB \ 72 | --metric-name RequestCount \ 73 | --end-time "2015-04-15T21:15:00Z" \ 74 | --start-time "2015-04-15T17:45:00Z" \ 75 | --period 900 \ 76 | --statistics Sum \ 77 | | tee cloudwatch.json \ 78 | | go-sparkline 79 | $ head cloudwatch.json; echo '...'; tail cloudwatch.json; 80 | { 81 | "Datapoints": [ 82 | { 83 | "Timestamp": "2015-04-15T17:45:00Z", 84 | "Sum": 37823.0, 85 | "Unit": "Count" 86 | }, 87 | { 88 | "Timestamp": "2015-04-15T11:30:00Z", 89 | "Sum": 12413.0, 90 | ... 91 | "Unit": "Count" 92 | }, 93 | { 94 | "Timestamp": "2015-04-15T21:15:00Z", 95 | "Sum": 29428.0, 96 | "Unit": "Count" 97 | } 98 | ], 99 | "Label": "RequestCount" 100 | } 101 | ``` 102 | 103 | 104 | # FAQ 105 | 106 | ## Q: What?! How does it display inline images directly in the terminal?! 107 | 108 | **A:** [iTerm2][] supports a bunch of [propritary ESC seq][iTerm2seq]. The [nightly build][iTerm2nightly] even includes one to [render images][iTerm2images] directly into the terminal. 109 | ``` 110 | ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G 111 | ``` 112 | 113 | 114 | [iTerm2]: http://iterm2.com/ 115 | [iTerm2seq]: http://iterm2.com/documentation-escape-codes.html 116 | [iTerm2images]: http://iterm2.com/images.html 117 | [iTerm2nightly]: http://iterm2.com/downloads/nightly/ 118 | 119 | # Autor 120 | 121 | 122 | | | | 123 | |---|---| 124 | | ![](http://gravatar.com/avatar/0ad964bc2b83e0977d8f70816eda1c70) | © 2015 by Mathias Leppich
[github.com/muhqu](https://github.com/muhqu), [@muhqu](http://twitter.com/muhqu) | 125 | | | | 126 | 127 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muhqu/go-sparkline/78e6fca19da24687ea3cc3dd66c33d1eb00d3973/demo.gif -------------------------------------------------------------------------------- /gradient.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/lucasb-eyer/go-colorful" 4 | 5 | type GradientTable []struct { 6 | Col colorful.Color 7 | Pos float64 8 | } 9 | 10 | var BrightColorGradient = GradientTable{ 11 | {MustParseHex("#EC0047"), 0.0}, 12 | {MustParseHex("#CA001B"), 0.1}, 13 | {MustParseHex("#EF270C"), 0.2}, 14 | {MustParseHex("#FA6A09"), 0.3}, 15 | {MustParseHex("#FEB10D"), 0.4}, 16 | {MustParseHex("#FFFF14"), 0.5}, 17 | {MustParseHex("#CEF82C"), 0.6}, 18 | {MustParseHex("#39DC20"), 0.7}, 19 | {MustParseHex("#26CA75"), 0.8}, 20 | {MustParseHex("#1571F4"), 0.9}, 21 | {MustParseHex("#3900D9"), 1.0}, 22 | // {MustParseHex("#9e0142"), 0.0}, 23 | // {MustParseHex("#d53e4f"), 0.1}, 24 | // {MustParseHex("#f46d43"), 0.2}, 25 | // {MustParseHex("#fdae61"), 0.3}, 26 | // {MustParseHex("#fee090"), 0.4}, 27 | // {MustParseHex("#ffffbf"), 0.5}, 28 | // {MustParseHex("#e6f598"), 0.6}, 29 | // {MustParseHex("#abdda4"), 0.7}, 30 | // {MustParseHex("#66c2a5"), 0.8}, 31 | // {MustParseHex("#3288bd"), 0.9}, 32 | // {MustParseHex("#5e4fa2"), 1.0}, 33 | } 34 | 35 | // This is the meat of the gradient computation. It returns a HCL-blend between 36 | // the two colors around `t`. 37 | // Note: It relies heavily on the fact that the gradient keypoints are sorted. 38 | func (self GradientTable) GetInterpolatedColorFor(t float64) colorful.Color { 39 | for i := 0; i < len(self)-1; i++ { 40 | c1 := self[i] 41 | c2 := self[i+1] 42 | if c1.Pos <= t && t <= c2.Pos { 43 | // We are in between c1 and c2. Go blend them! 44 | t := (t - c1.Pos) / (c2.Pos - c1.Pos) 45 | return c1.Col.BlendHcl(c2.Col, t).Clamped() 46 | } 47 | } 48 | 49 | // Nothing found? Means we're at (or past) the last gradient keypoint. 50 | return self[len(self)-1].Col 51 | } 52 | 53 | // This is a very nice thing Golang forces you to do! 54 | // It is necessary so that we can write out the literal of the colortable below. 55 | func MustParseHex(s string) colorful.Color { 56 | c, err := colorful.Hex(s) 57 | if err != nil { 58 | panic("MustParseHex: " + err.Error()) 59 | } 60 | return c 61 | } 62 | -------------------------------------------------------------------------------- /iterm.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "image" 8 | "image/png" 9 | "io" 10 | "os" 11 | ) 12 | 13 | type FileDescriptor interface { 14 | Stat() (fi os.FileInfo, err error) 15 | } 16 | 17 | func IsTerminal(fd FileDescriptor) bool { 18 | fi, _ := fd.Stat() 19 | return (fi.Mode() & os.ModeCharDevice) != 0 20 | } 21 | 22 | type ITermImage struct { 23 | img image.Image 24 | } 25 | 26 | // WriteTo implements the io.WriterTo interface, writing an iTerm 1337 escaped . 27 | func (i *ITermImage) WriteTo(w io.Writer) (int64, error) { 28 | str, err := ITermEncodePNGToString(i.img, "[iTerm Image]") 29 | if err != nil { 30 | return 0, err 31 | } 32 | n, err := w.Write([]byte(str)) 33 | return int64(n), err 34 | } 35 | 36 | func (i *ITermImage) String() string { 37 | str, err := ITermEncodePNGToString(i.img, "[iTerm Image]") 38 | if err != nil { 39 | return err.Error() 40 | } 41 | return str 42 | } 43 | 44 | func ITermEncodePNGToString(img image.Image, alt string) (str string, err error) { 45 | b := new(bytes.Buffer) 46 | err = png.Encode(b, img) 47 | if err != nil { 48 | return 49 | } 50 | bytes := b.Bytes() 51 | base64str := base64.StdEncoding.EncodeToString(bytes) 52 | str = fmt.Sprintf("\033]1337;File=inline=1;size=%d:%s\a%s\n", len(bytes), base64str, alt) 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "image" 9 | "image/color" 10 | "image/color/palette" 11 | "image/draw" 12 | "image/gif" 13 | "image/png" 14 | "io" 15 | "log" 16 | "os" 17 | "os/signal" 18 | "sort" 19 | "strconv" 20 | "strings" 21 | "time" 22 | 23 | "code.google.com/p/plotinum/plotter" 24 | "gopkg.in/alecthomas/kingpin.v1" 25 | ) 26 | 27 | const ( 28 | ESC_HIDE_CURSOR = "\033[?25l" 29 | ESC_SHOW_CURSOR = "\033[?25h" 30 | ESC_SAVE_POSITION = "\0337" // "\033[s" 31 | ESC_RESTORE_POSITION = "\0338" // "\033[u" 32 | ESC_CLEAR_SCREEN = "\033[H\033[2J" 33 | ) 34 | 35 | type renderer func(xy plotter.XYs) (image.Image, error) 36 | 37 | type valuer func() (plotter.Values, error) 38 | 39 | var ( 40 | optAnimate bool 41 | optIgnoreParseErrors bool 42 | optInlineImages bool 43 | optStream bool 44 | optCharGeo *charGeo 45 | optCharWidth = 7 46 | optCharHeight = 17 47 | optRows = 3 48 | optMaxCols = 80 49 | optVerbose = 1 50 | optRenderer *string 51 | optValues []string 52 | ) 53 | 54 | var renderers = map[string]renderer{} 55 | 56 | func main() { 57 | optCharGeo = new(charGeo) 58 | values := make(plotter.Values, 0) 59 | 60 | kingpin.Flag("stream", "stream").Short('s').BoolVar(&optStream) 61 | kingpin.Flag("animate", "start animation").BoolVar(&optAnimate) 62 | kingpin.Flag("lazy", "ignore parse errors").BoolVar(&optIgnoreParseErrors) 63 | kingpin.Flag("char-size", "Pixel size of a single character. Can also be set via env ITERM_CHARACTER_SIZE. The default 7:17 corresponds to 12p Monaco."). 64 | OverrideDefaultFromEnvar("ITERM_CHARACTER_SIZE"). 65 | Default("7:17"). 66 | SetValue(optCharGeo) 67 | kingpin.Flag("rows", "height in number of rows").Default("3").IntVar(&optRows) 68 | 69 | availRenderers := []string{} 70 | for k := range renderers { 71 | availRenderers = append(availRenderers, k) 72 | } 73 | sort.StringSlice(availRenderers).Sort() 74 | rendererHelp := fmt.Sprintf("available renderers: %s", strings.Join(availRenderers, ", ")) 75 | rendererName := kingpin.Flag("renderer", rendererHelp).Default("sparks").Enum(availRenderers...) 76 | 77 | kingpin.Arg("values", "Numeric values to render. Can also be read from stdin.").SetValue((*argValues)(&values)) 78 | kingpin.Parse() 79 | 80 | optCharHeight = optCharGeo.Height 81 | optCharWidth = optCharGeo.Width 82 | 83 | optInlineImages = IsTerminal(os.Stdout) 84 | 85 | if IsTerminal(os.Stdin) && len(values) == 0 { 86 | kingpin.Usage() 87 | return 88 | } 89 | 90 | var renderFn = renderers[*rendererName] 91 | 92 | b := bufio.NewReader(os.Stdin) 93 | var valuesProvider valuer 94 | if !IsTerminal(os.Stdin) { 95 | firstChar, _ := b.Peek(1) 96 | if string(firstChar) == "[" { 97 | valuesProvider = valuerForJsonArray(b) 98 | } else if string(firstChar) == "{" { 99 | valuesProvider = valuerForCloudWatchJson(b) 100 | } else { 101 | valuesProvider = valuerForPlainNumbers(b) 102 | } 103 | } else { 104 | // null valuer 105 | valuesProvider = func() (plotter.Values, error) { 106 | return nil, io.EOF 107 | } 108 | } 109 | var drawer animationDrawer 110 | if IsTerminal(os.Stdout) { 111 | drawer = &iTermAnimationDrawer{ 112 | out: os.Stdout, 113 | } 114 | } else { 115 | drawer = &gifAnimationDrawer{ 116 | out: os.Stdout, 117 | } 118 | } 119 | 120 | var err error 121 | if optStream { 122 | optInlineImages = true 123 | if IsTerminal(os.Stdin) { 124 | err = fmt.Errorf("expected pipe on stdin when using stream option") 125 | } 126 | err = renderAnimated(drawer, valuesProvider, renderFn, optMaxCols) 127 | } else { 128 | if !IsTerminal(os.Stdin) { 129 | values, err = appendAllValues(values, valuesProvider, optMaxCols) 130 | } 131 | if err == nil { 132 | if optAnimate { 133 | optInlineImages = true 134 | times := 2 135 | t := 0 136 | i := 0 137 | err = renderAnimated(drawer, 138 | func() (plotter.Values, error) { 139 | time.Sleep(100 * time.Millisecond) 140 | for t < times { 141 | if i < len(values) { 142 | v := append(values[i:], values[0:i]...) 143 | i++ 144 | return v, nil 145 | } 146 | t++ 147 | } 148 | if t == times { 149 | t++ 150 | return values, nil 151 | } 152 | return nil, io.EOF 153 | }, renderFn, len(values)) 154 | } else { 155 | var img image.Image 156 | img, err = renderFn(Values2XYs(values)) 157 | if err == nil { 158 | renderImg(img, os.Stdout) 159 | } 160 | } 161 | } 162 | } 163 | 164 | if err != nil { 165 | log.Fatal(err) 166 | } 167 | } 168 | 169 | func appendAllValues(values plotter.Values, valuesProvider valuer, window int) (plotter.Values, error) { 170 | for { 171 | v, err := valuesProvider() 172 | if err == io.EOF { 173 | break 174 | } 175 | if err != nil { 176 | return nil, err 177 | } 178 | values = append(values, v...) 179 | if m := len(values); m > window { 180 | values = values[m-window : m] 181 | } 182 | } 183 | return values, nil 184 | } 185 | 186 | type animationDrawer interface { 187 | Begin() error 188 | DrawFrame(image.Image) error 189 | End() error 190 | } 191 | 192 | type iTermAnimationDrawer struct { 193 | out io.Writer 194 | } 195 | 196 | func (i *iTermAnimationDrawer) Begin() error { 197 | io.WriteString(i.out, ESC_HIDE_CURSOR) 198 | io.WriteString(i.out, strings.Repeat("\n", optRows)) 199 | return nil 200 | } 201 | 202 | func (i *iTermAnimationDrawer) End() error { 203 | io.WriteString(i.out, ESC_SHOW_CURSOR) 204 | return nil 205 | } 206 | 207 | func (i *iTermAnimationDrawer) DrawFrame(img image.Image) error { 208 | b := new(bytes.Buffer) 209 | fmt.Fprintf(b, "\033[%dA", optRows) 210 | itermImg := &ITermImage{img} 211 | _, err := itermImg.WriteTo(b) 212 | if err != nil { 213 | return err 214 | } 215 | b.WriteTo(i.out) 216 | return nil 217 | } 218 | 219 | type gifAnimationDrawer struct { 220 | out io.Writer 221 | last *time.Time 222 | frames []image.Image 223 | delays []int 224 | } 225 | 226 | func (g *gifAnimationDrawer) Begin() error { 227 | fmt.Fprint(os.Stderr, "Start buffering to generate animated GIF...\n") 228 | 229 | return nil 230 | } 231 | func (g *gifAnimationDrawer) End() error { 232 | fmt.Fprint(os.Stderr, "Writing animated GIF...") 233 | 234 | var pFrames []*image.Paletted 235 | if len(g.frames) > 0 { 236 | b := g.frames[len(g.frames)-1].Bounds() 237 | for _, img := range g.frames { 238 | pimg := image.NewPaletted(b, palette.Plan9) 239 | draw.FloydSteinberg.Draw(pimg, b, img, image.ZP) 240 | pFrames = append(pFrames, pimg) 241 | } 242 | } 243 | 244 | return gif.EncodeAll(g.out, &gif.GIF{ 245 | Image: pFrames, 246 | Delay: append(g.delays, 0), 247 | }) 248 | } 249 | func (g *gifAnimationDrawer) DrawFrame(img image.Image) error { 250 | 251 | g.frames = append(g.frames, img) 252 | 253 | curr := time.Now() 254 | if g.last != nil { 255 | delay := Centiseconds(curr.Sub(*g.last)) 256 | //log.Printf("DrawFrame: delay %#v", delay) 257 | g.delays = append(g.delays, delay) 258 | } 259 | // log.Printf("Buffer frame %d", len(g.frames)) 260 | hour := []string{"|", "/", "-", "\\"} 261 | i := len(g.frames) 262 | fmt.Fprintf(os.Stderr, "\033[0K%s buffered %d frames\r", hour[i%4], i) 263 | 264 | g.last = &curr 265 | return nil 266 | } 267 | 268 | func Centiseconds(t time.Duration) int { 269 | return int(float64(10E-8) * float64(t.Nanoseconds())) 270 | } 271 | 272 | func renderAnimated(drawer animationDrawer, valuesProvider valuer, renderFn renderer, window int) error { 273 | values := plotter.Values{} 274 | 275 | redrawCh := time.Tick(100 * time.Millisecond) 276 | errorCh := make(chan error) 277 | valuesCh := make(chan plotter.Values) 278 | signalCh := make(chan os.Signal, 1) 279 | signal.Notify(signalCh, os.Interrupt) 280 | 281 | go func() { 282 | for { 283 | v, err := valuesProvider() 284 | if v != nil { 285 | valuesCh <- v 286 | } 287 | if err != nil { 288 | errorCh <- err 289 | break 290 | } 291 | } 292 | }() 293 | 294 | drawer.Begin() 295 | 296 | valuesChanged := false 297 | var lastError error 298 | 299 | redraw := func() error { 300 | if valuesChanged { 301 | img, err := renderFn(Values2XYs(values)) 302 | if err != nil { 303 | return err 304 | } 305 | if err := drawer.DrawFrame(img); err != nil { 306 | return err 307 | } 308 | valuesChanged = false 309 | } 310 | return nil 311 | } 312 | 313 | loop: 314 | for { 315 | select { 316 | case <-signalCh: 317 | //log.Print("reveived signal: ", s) 318 | break loop 319 | 320 | case v := <-valuesCh: 321 | //log.Print("reveived values: ", v) 322 | values = append(values, v...) 323 | if m := len(values); m > window { 324 | values = values[m-window : m] 325 | } 326 | valuesChanged = true 327 | //log.Print("all values: ", values) 328 | 329 | case err := <-errorCh: 330 | //log.Print("reveived error: ", err) 331 | if err != io.EOF { 332 | lastError = err 333 | // os.Stdout.WriteString(ESC_CLEAR_SCREEN) 334 | // log.Print(err) 335 | } else { 336 | // reached the end.. last trigger last redraw 337 | lastError = redraw() 338 | } 339 | break loop 340 | 341 | case <-redrawCh: 342 | //log.Print("reveived redraw: ", r) 343 | if err := redraw(); err != nil { 344 | lastError = err 345 | break loop 346 | } 347 | } 348 | } 349 | 350 | if err := drawer.End(); err != nil && lastError == nil { 351 | lastError = err 352 | } 353 | 354 | return lastError 355 | } 356 | 357 | func valuerForPlainNumbers(in io.Reader) valuer { 358 | lineScanner := bufio.NewScanner(in) 359 | lineScanner.Split(bufio.ScanLines) 360 | 361 | return func() (plotter.Values, error) { 362 | if lineScanner.Scan() { 363 | values := plotter.Values{} 364 | line := lineScanner.Text() 365 | wordScanner := bufio.NewScanner(strings.NewReader(line)) 366 | wordScanner.Split(bufio.ScanWords) 367 | for wordScanner.Scan() { 368 | val := wordScanner.Text() 369 | f, err := strconv.ParseFloat(val, 64) 370 | if err != nil { 371 | return nil, err 372 | } 373 | values = append(values, f) 374 | } 375 | err := lineScanner.Err() 376 | return values, err 377 | } 378 | return nil, io.EOF 379 | } 380 | } 381 | 382 | func valuerForJsonArray(in io.Reader) valuer { 383 | dec := json.NewDecoder(in) 384 | return func() (plotter.Values, error) { 385 | var m plotter.Values 386 | err := dec.Decode(&m) 387 | if err != nil { 388 | return nil, err 389 | } 390 | return m, nil 391 | } 392 | } 393 | 394 | func valuerForCloudWatchJson(in io.Reader) valuer { 395 | dec := json.NewDecoder(in) 396 | type CloudWatchDatapoint struct { 397 | Timestamp string 398 | Sum *float64 399 | Maximum *float64 400 | Minimum *float64 401 | SampleCount *float64 402 | Average *float64 403 | Unit string 404 | } 405 | type CloudWatchData struct { 406 | Label string 407 | Datapoints []*CloudWatchDatapoint 408 | } 409 | return func() (plotter.Values, error) { 410 | var m *CloudWatchData 411 | err := dec.Decode(&m) 412 | if err != nil { 413 | return nil, err 414 | } 415 | values := plotter.Values{} 416 | for _, d := range m.Datapoints { 417 | if d.Sum != nil { 418 | values = append(values, *d.Sum) 419 | } else if d.Average != nil { 420 | values = append(values, *d.Average) 421 | } else if d.Minimum != nil { 422 | values = append(values, *d.Minimum) 423 | } else if d.Maximum != nil { 424 | values = append(values, *d.Maximum) 425 | } else if d.SampleCount != nil { 426 | values = append(values, *d.SampleCount) 427 | } 428 | } 429 | return values, nil 430 | } 431 | } 432 | 433 | type charGeo struct { 434 | Width int 435 | Height int 436 | } 437 | 438 | func (c *charGeo) Set(value string) error { 439 | parts := strings.SplitN(value, ":", 2) 440 | if len(parts) != 2 { 441 | return fmt.Errorf("expected WIDTH:HEIGHT got '%s'", value) 442 | } 443 | width, err := strconv.ParseInt(parts[0], 10, 64) 444 | if err != nil || width <= 0 { 445 | return fmt.Errorf("expected WIDTH:HEIGHT, WIDTH must be a positive number, got '%s'", parts[0]) 446 | } 447 | height, err := strconv.ParseInt(parts[1], 10, 64) 448 | if err != nil || height <= 0 { 449 | return fmt.Errorf("expected WIDTH:HEIGHT, HEIGHT must be a positive number, got '%s'", parts[1]) 450 | } 451 | c.Width = int(width) 452 | c.Height = int(height) 453 | return nil 454 | } 455 | 456 | func (c *charGeo) String() string { 457 | return fmt.Sprintf("%d:%d", c.Width, c.Height) 458 | } 459 | 460 | type argValues plotter.Values 461 | 462 | func (i *argValues) Set(value string) error { 463 | if !IsTerminal(os.Stdin) { 464 | return fmt.Errorf("command line values not allowed when reading from stdin") 465 | } 466 | f, err := strconv.ParseFloat(value, 64) 467 | if err != nil { 468 | return fmt.Errorf("expected NUMERIC VALUE got '%s'", value) 469 | } 470 | *i = append(*i, f) 471 | return nil 472 | } 473 | 474 | func (i *argValues) String() string { 475 | return "" 476 | } 477 | 478 | func (i *argValues) IsCumulative() bool { 479 | return true 480 | } 481 | 482 | func Values2XYs(values plotter.Values) plotter.XYs { 483 | XYs := make(plotter.XYs, 0) 484 | for i, v := range values { 485 | XYs = append(XYs, plotter.XYs{{float64(i), v}}...) 486 | } 487 | return XYs 488 | } 489 | 490 | func init() { 491 | renderers["sparks"] = plotSparks 492 | } 493 | 494 | func plotSparks(xys plotter.XYs) (image.Image, error) { 495 | 496 | border := 4 497 | if optRows == 1 { 498 | border = 1 499 | } else if optRows == 2 { 500 | border = 2 501 | } 502 | height := optCharHeight * optRows 503 | width := (optCharWidth * 2) + (optCharWidth * len(xys)) 504 | img := image.NewRGBA(image.Rect(0, 0, width, height)) 505 | 506 | _, _, ymin, ymax := plotter.XYRange(xys) 507 | ymin = 0 508 | 509 | for i, xy := range xys { 510 | 511 | //xf := (xy.X - xmin) / xmax 512 | yf := (xy.Y - ymin) / ymax 513 | hm := height - int(float64(height-(border*2))*yf) - border 514 | h0 := height - border 515 | 516 | w := ((i + 1) * optCharWidth) 517 | for h := h0; h >= hm; h = h - 2 { 518 | var c color.RGBA 519 | if h == hm || h == hm+1 { 520 | c = color.RGBA{0, 255, 0, 255} 521 | } else if h == h0 { 522 | c = color.RGBA{0, 128, 0, 255} 523 | } else { 524 | p := float64(h-hm)/float64(h0-hm)*0.5 + 0.3 525 | g := uint8(float64(255) - (p * float64(255))) 526 | c = color.RGBA{0, g, 0, 255} 527 | } 528 | for j := 0; j < (optCharWidth - 2); j++ { 529 | img.SetRGBA(w+j, h, c) 530 | } 531 | } 532 | } 533 | 534 | return img, nil 535 | } 536 | 537 | func renderImg(img image.Image, out io.Writer) error { 538 | if optInlineImages { 539 | itermImg := &ITermImage{img} 540 | if _, err := itermImg.WriteTo(out); err != nil { 541 | return err 542 | } 543 | } else { 544 | b := new(bytes.Buffer) 545 | err := png.Encode(b, img) 546 | if err != nil { 547 | return err 548 | } 549 | if _, err := b.WriteTo(out); err != nil { 550 | return err 551 | } 552 | } 553 | return nil 554 | } 555 | -------------------------------------------------------------------------------- /plot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | 7 | "code.google.com/p/plotinum/plot" 8 | "code.google.com/p/plotinum/plotter" 9 | "code.google.com/p/plotinum/vg" 10 | "code.google.com/p/plotinum/vg/vgimg" 11 | ) 12 | 13 | func init() { 14 | renderers["line"] = plotLine 15 | renderers["vlines"] = plotVLines 16 | } 17 | 18 | func plotVLines(xy plotter.XYs) (image.Image, error) { 19 | 20 | p, err := plot.New() 21 | if err != nil { 22 | return nil, err 23 | } 24 | p.HideAxes() 25 | p.BackgroundColor = &color.RGBA{0, 0, 0, 255} 26 | 27 | s, err := NewSparkLines(xy) 28 | if err != nil { 29 | return nil, err 30 | } 31 | s.Color = &color.RGBA{0, 255, 0, 128} 32 | p.Add(s) 33 | 34 | // Draw the plot to an in-memory image. 35 | // _, rows, _ := terminal.GetSize(0) 36 | charWidth := optCharWidth 37 | charHeight := optCharHeight 38 | //width := cols * charWidth 39 | height := optRows * charHeight 40 | 41 | img := image.NewRGBA(image.Rect(0, 0, 5+(len(xy)*charWidth), height)) 42 | canvas := vgimg.NewImage(img) 43 | da := plot.MakeDrawArea(canvas) 44 | p.Draw(da) 45 | 46 | return img, nil 47 | } 48 | 49 | func plotLine(xy plotter.XYs) (image.Image, error) { 50 | 51 | p, err := plot.New() 52 | if err != nil { 53 | return nil, err 54 | } 55 | p.HideAxes() 56 | p.BackgroundColor = &color.RGBA{0, 0, 0, 255} 57 | 58 | //s, err := NewSparkLines(xy) 59 | s, err := plotter.NewLine(xy) 60 | if err != nil { 61 | return nil, err 62 | } 63 | s.Color = &color.RGBA{0, 255, 0, 128} 64 | p.Add(s) 65 | 66 | // Draw the plot to an in-memory image. 67 | // _, rows, _ := terminal.GetSize(0) 68 | charWidth := optCharWidth 69 | charHeight := optCharHeight 70 | //width := cols * charWidth 71 | height := optRows * charHeight 72 | 73 | img := image.NewRGBA(image.Rect(0, 0, 5+(len(xy)*charWidth), height)) 74 | canvas := vgimg.NewImage(img) 75 | da := plot.MakeDrawArea(canvas) 76 | p.Draw(da) 77 | 78 | return img, nil 79 | } 80 | 81 | func NewSparkLines(xy plotter.XYs) (*SparkLines, error) { 82 | s := new(SparkLines) 83 | s.XYs = xy 84 | return s, nil 85 | } 86 | 87 | type SparkLines struct { 88 | XYs plotter.XYs 89 | Color color.Color 90 | } 91 | 92 | func (s *SparkLines) Plot(da plot.DrawArea, plt *plot.Plot) { 93 | trX, trY := plt.Transforms(&da) 94 | 95 | w := vg.Length(1) 96 | 97 | da.SetLineWidth(w) 98 | 99 | _, _, ymin, ymax := s.DataRange() 100 | 101 | for _, d := range s.XYs { 102 | perc := float64(d.Y-ymin) / float64(ymax-ymin) 103 | c := BrightColorGradient.GetInterpolatedColorFor((perc*-1+1)*0.5 + 0.6) 104 | da.SetColor(c) 105 | 106 | // Transform the data x, y coordinate of this bubble 107 | // to the corresponding drawing coordinate. 108 | x := trX(d.X) 109 | y := trY(d.Y * 0.9) 110 | 111 | //rad := vg.Length(10) 112 | var p vg.Path 113 | p.Move(x-w, y) 114 | p.Line(x-w, 0) 115 | //p.Close() 116 | da.Stroke(p) 117 | 118 | //da.StrokeLine2(*sty, x, 0, x, y) 119 | } 120 | } 121 | 122 | func (s *SparkLines) DataRange() (xmin, xmax, ymin, ymax float64) { 123 | return plotter.XYRange(s.XYs) 124 | } 125 | --------------------------------------------------------------------------------