diff --git a/docs/user_guide/outputs/asciigraph_output.md b/docs/user_guide/outputs/asciigraph_output.md
new file mode 100644
index 00000000..ea522911
--- /dev/null
+++ b/docs/user_guide/outputs/asciigraph_output.md
@@ -0,0 +1,155 @@
+`gnmic` supports displaying collected metrics as an ASCII graph on the terminal.
+The graph is generated using the [asciigraph](https://github.com/guptarohit/asciigraph) package.
+
+### Configuration sample
+
+```yaml
+
+outputs:
+  output1:
+    # required
+    type: asciigraph
+    # string, the graph caption
+    caption: 
+    # integer, the graph height. If unset, defaults to the terminal height
+    height:
+    # integer, the graph width. If unset, defaults to the terminal width
+    width:
+    # float, the graph minimum value for the vertical axis.
+    lower-bound:
+    # float, the graph minimum value for the vertical axis.
+    upper-bound:
+    # integer, the graph left offset.
+    offset:
+    # integer, the decimal point precision of the label values.
+    precision:
+    # string, the caption color. one of ANSI colors.
+    caption-color:
+    # string, the axis color. one of ANSI colors.
+    axis-color:
+    # string, the label color. one of ANSI colors.
+    label-color:
+    # duration, the graph refresh timer.
+    refresh-timer: 1s
+    # string, one of `overwrite`, `if-not-present`, ``
+    # This field allows populating/changing the value of Prefix.Target in the received message.
+    # if set to ``, nothing changes 
+    # if set to `overwrite`, the target value is overwritten using the template configured under `target-template`
+    # if set to `if-not-present`, the target value is populated only if it is empty, still using the `target-template`
+    add-target: 
+    # string, a GoTemplate that allows for the customization of the target field in Prefix.Target.
+    # it applies only if the previous field `add-target` is not empty.
+    # if left empty, it defaults to:
+    # {{- if index . "subscription-target" -}}
+    # {{ index . "subscription-target" }}
+    # {{- else -}}
+    # {{ index . "source" | host }}
+    # {{- end -}}`
+    # which will set the target to the value configured under `subscription.$subscription-name.target` if any,
+    # otherwise it will set it to the target name stripped of the port number (if present)
+    target-template:
+    # list of processors to apply on the message before writing
+    event-processors: 
+    # bool enable debug
+    debug: false 
+```
+
+### Example
+
+This example shows how to use the `asciigraph` output.
+
+gNMIc config
+
+```shell
+cat gnmic_asciiout.yaml
+```
+
+```yaml
+targets:
+  clab-nfd33-spine1-1:
+    username: admin
+    password: NokiaSrl1!
+    skip-verify: true
+
+subscriptions:
+  sub1:
+    paths:
+      - /interface[name=ethernet-1/3]/statistics/out-octets
+      - /interface[name=ethernet-1/3]/statistics/in-octets
+    stream-mode: sample
+    sample-interval: 1s
+    encoding: ascii
+
+outputs:
+  out1:
+    type: asciigraph
+    caption: in/out octets per second
+    event-processors:
+      - rate
+
+processors:
+  rate:
+    event-starlark:
+      script: rate.star
+```
+
+Starlark processor
+
+```shell
+cat rate.star
+```
+
+```python
+cache = {}
+
+values_names = [
+  '/interface/statistics/out-octets',
+  '/interface/statistics/in-octets'
+]
+
+N=2
+
+def apply(*events):
+  for e in events:
+    for value_name in values_names:
+      v = e.values.get(value_name)
+      # check if v is not None and is a digit to proceed
+      if not v:
+        continue
+      if not v.isdigit():
+        continue
+      # update cache with the latest value
+      val_key = "_".join([e.tags["source"], e.tags["interface_name"], value_name])
+      if not cache.get(val_key):
+        # initialize the cache entry if empty
+        cache.update({val_key: []})
+      if len(cache[val_key]) > N:
+        # remove the oldest entry if the number of entries reached N
+        cache[val_key] = cache[val_key][1:]
+      # update cache entry
+      cache[val_key].append((int(v), e.timestamp))
+      # get the list of values
+      val_list = cache[val_key]
+      # calculate rate
+      e.values[value_name+"_rate"] = rate(val_list)
+      e.values.pop(value_name)
+    
+  return events
+
+def rate(vals):
+  previous_value, previous_timestamp = None, None
+  for value, timestamp in vals:
+    if previous_value != None and previous_timestamp != None:
+      time_diff = (timestamp - previous_timestamp) / 1000000000 # 1 000 000 000
+      if time_diff > 0:
+        value_diff = value - previous_value
+        rate = value_diff / time_diff
+        return rate
+
+    previous_value = value
+    previous_timestamp = timestamp
+
+  return 0
+```
+
+<script async id="asciicast-617477" src="https://asciinema.org/a/617477.js"></script>
diff --git a/go.mod b/go.mod
index baf00e2a..7c6103d1 100644
--- a/go.mod
+++ b/go.mod
@@ -18,6 +18,7 @@ require (
 	github.com/gorilla/mux v1.8.0
 	github.com/gosnmp/gosnmp v1.35.0
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
+	github.com/guptarohit/asciigraph v0.5.6
 	github.com/hairyhenderson/gomplate/v3 v3.11.5
 	github.com/hashicorp/consul/api v1.25.1
 	github.com/huandu/xstrings v1.4.0
diff --git a/go.sum b/go.sum
index 255bf172..1bd16a08 100644
--- a/go.sum
+++ b/go.sum
@@ -566,6 +566,8 @@ github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWf
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/guptarohit/asciigraph v0.5.6 h1:0tra3HEhfdj1sP/9IedrCpfSiXYTtHdCgBhBL09Yx6E=
+github.com/guptarohit/asciigraph v0.5.6/go.mod h1:dYl5wwK4gNsnFf9Zp+l06rFiDZ5YtXM6x7SRWZ3KGag=
 github.com/hairyhenderson/go-fsimpl v0.0.0-20220529183339-9deae3e35047 h1:nSSfN9G8O8XXDqB3aDEHJ8K+0llYYToNlTcWOe1Pti8=
 github.com/hairyhenderson/go-fsimpl v0.0.0-20220529183339-9deae3e35047/go.mod h1:30RY4Ey+bg+BGKBufZE2IEmxk7hok9U9mjdgZYomwN4=
 github.com/hairyhenderson/gomplate/v3 v3.11.5 h1:LSDxCw8tWC/ltOzbZaleUNjGJOIEgnR/SN3GM9eClsA=
diff --git a/mkdocs.yml b/mkdocs.yml
index 79e28714..b758c846 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -67,6 +67,7 @@ nav:
           - TCP: user_guide/outputs/tcp_output.md
           - UDP: user_guide/outputs/udp_output.md
           - SNMP: user_guide/outputs/snmp_output.md
+          - ASCII Graph: user_guide/outputs/asciigraph_output.md
           
       - Processors: 
           - Introduction: user_guide/event_processors/intro.md
diff --git a/outputs/all/all.go b/outputs/all/all.go
index 619aa3e2..80ce0bb5 100644
--- a/outputs/all/all.go
+++ b/outputs/all/all.go
@@ -9,6 +9,7 @@
 package all
 
 import (
+	_ "github.com/openconfig/gnmic/outputs/asciigraph_output"
 	_ "github.com/openconfig/gnmic/outputs/file"
 	_ "github.com/openconfig/gnmic/outputs/gnmi_output"
 	_ "github.com/openconfig/gnmic/outputs/influxdb_output"
diff --git a/outputs/asciigraph_output/asciigraph.go b/outputs/asciigraph_output/asciigraph.go
new file mode 100644
index 00000000..6a14e30c
--- /dev/null
+++ b/outputs/asciigraph_output/asciigraph.go
@@ -0,0 +1,537 @@
+// © 2023 Nokia.
+//
+// This code is a Contribution to the gNMIc project (“Work”) made under the Google Software Grant and Corporate Contributor License Agreement (“CLA”) and governed by the Apache License 2.0.
+// No other rights or licenses in or to any of Nokia’s intellectual property are granted for any other purpose.
+// This code is provided on an “as is” basis without any warranties of any kind.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package asciigraph_output
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"math"
+	"os"
+	"sort"
+	"strconv"
+	"strings"
+	"sync"
+	"text/template"
+	"time"
+
+	"github.com/guptarohit/asciigraph"
+	"github.com/nsf/termbox-go"
+	"github.com/openconfig/gnmi/proto/gnmi"
+	"github.com/prometheus/client_golang/prometheus"
+	"google.golang.org/protobuf/proto"
+
+	"github.com/openconfig/gnmic/formatters"
+	"github.com/openconfig/gnmic/outputs"
+	"github.com/openconfig/gnmic/types"
+	"github.com/openconfig/gnmic/utils"
+)
+
+const (
+	loggingPrefix       = "[asciigraph_output:%s] "
+	defaultRefreshTimer = time.Second
+	defaultPrecision    = 2
+	defaultTimeout      = 10 * time.Second
+)
+
+var (
+	defaultLabelColor   = asciigraph.Blue
+	defaultCaptionColor = asciigraph.Default
+	defaultAxisColor    = asciigraph.Default
+)
+
+func init() {
+	outputs.Register("asciigraph", func() outputs.Output {
+		return &asciigraphOutput{
+			cfg:     &cfg{},
+			logger:  log.New(io.Discard, loggingPrefix, utils.DefaultLoggingFlags),
+			eventCh: make(chan *formatters.EventMsg, 100),
+			m:       new(sync.RWMutex),
+			data:    make(map[string]*series),
+			colors:  make(map[asciigraph.AnsiColor]struct{}),
+		}
+	})
+}
+
+// asciigraphOutput //
+type asciigraphOutput struct {
+	cfg     *cfg
+	logger  *log.Logger
+	eventCh chan *formatters.EventMsg
+
+	m       *sync.RWMutex
+	data    map[string]*series
+	colors  map[asciigraph.AnsiColor]struct{}
+	caption string
+
+	captionColor asciigraph.AnsiColor
+	axisColor    asciigraph.AnsiColor
+	labelColor   asciigraph.AnsiColor
+	evps         []formatters.EventProcessor
+
+	targetTpl *template.Template
+}
+
+type series struct {
+	name  string
+	data  []float64
+	color asciigraph.AnsiColor
+}
+
+// cfg //
+type cfg struct {
+	// The caption to be displayed under the graph
+	Caption string `mapstructure:"caption,omitempty" json:"caption,omitempty"`
+	// The graph height
+	Height int `mapstructure:"height,omitempty" json:"height,omitempty"`
+	// The graph width
+	Width int `mapstructure:"width,omitempty" json:"width,omitempty"`
+	// The graph minimum value for the vertical axis
+	LowerBound *float64 `mapstructure:"lower-bound,omitempty" json:"lower-bound,omitempty"`
+	// the graph maximum value for the vertical axis
+	UpperBound *float64 `mapstructure:"upper-bound,omitempty" json:"upper-bound,omitempty"`
+	// The graph offset
+	Offset int `mapstructure:"offset,omitempty" json:"offset,omitempty"`
+	// The decimal point precision of the label values
+	Precision uint `mapstructure:"precision,omitempty" json:"precision,omitempty"`
+	// The caption color
+	CaptionColor string `mapstructure:"caption-color,omitempty" json:"caption-color,omitempty"`
+	// The axis color
+	AxisColor string `mapstructure:"axis-color,omitempty" json:"axis-color,omitempty"`
+	// The label color
+	LabelColor string `mapstructure:"label-color,omitempty" json:"label-color,omitempty"`
+	// The graph refresh timer
+	RefreshTimer time.Duration `mapstructure:"refresh-timer,omitempty" json:"refresh-timer,omitempty"`
+	// Add target the received subscribe responses
+	AddTarget string `mapstructure:"add-target,omitempty" json:"add-target,omitempty"`
+	//
+	TargetTemplate string `mapstructure:"target-template,omitempty" json:"target-template,omitempty"`
+	// list of event processors
+	EventProcessors []string `mapstructure:"event-processors,omitempty" json:"event-processors,omitempty"`
+	// enable extra logging
+	Debug bool `mapstructure:"debug,omitempty" json:"debug,omitempty"`
+}
+
+func (a *asciigraphOutput) String() string {
+	b, err := json.Marshal(a.cfg)
+	if err != nil {
+		return ""
+	}
+	return string(b)
+}
+
+func (a *asciigraphOutput) SetEventProcessors(ps map[string]map[string]interface{},
+	logger *log.Logger,
+	tcs map[string]*types.TargetConfig,
+	acts map[string]map[string]interface{}) {
+	for _, epName := range a.cfg.EventProcessors {
+		if epCfg, ok := ps[epName]; ok {
+			epType := ""
+			for k := range epCfg {
+				epType = k
+				break
+			}
+			if in, ok := formatters.EventProcessors[epType]; ok {
+				ep := in()
+				err := ep.Init(epCfg[epType],
+					formatters.WithLogger(logger),
+					formatters.WithTargets(tcs),
+					formatters.WithActions(acts),
+				)
+				if err != nil {
+					a.logger.Printf("failed initializing event processor '%s' of type='%s': %v", epName, epType, err)
+					continue
+				}
+				a.evps = append(a.evps, ep)
+				a.logger.Printf("added event processor '%s' of type=%s to file output", epName, epType)
+				continue
+			}
+			a.logger.Printf("%q event processor has an unknown type=%q", epName, epType)
+			continue
+		}
+		a.logger.Printf("%q event processor not found!", epName)
+	}
+}
+
+func (a *asciigraphOutput) SetLogger(logger *log.Logger) {
+	if logger != nil && a.logger != nil {
+		a.logger.SetOutput(logger.Writer())
+		a.logger.SetFlags(logger.Flags())
+	}
+}
+
+// Init //
+func (a *asciigraphOutput) Init(ctx context.Context, name string, cfg map[string]interface{}, opts ...outputs.Option) error {
+	err := outputs.DecodeConfig(cfg, a.cfg)
+	if err != nil {
+		return err
+	}
+
+	a.logger.SetPrefix(fmt.Sprintf(loggingPrefix, name))
+
+	for _, opt := range opts {
+		opt(a)
+	}
+
+	if a.cfg.TargetTemplate == "" {
+		a.targetTpl = outputs.DefaultTargetTemplate
+	} else if a.cfg.AddTarget != "" {
+		a.targetTpl, err = utils.CreateTemplate("target-template", a.cfg.TargetTemplate)
+		if err != nil {
+			return err
+		}
+		a.targetTpl = a.targetTpl.Funcs(outputs.TemplateFuncs)
+	}
+	// set defaults
+	err = a.setDefaults()
+	if err != nil {
+		return err
+	}
+	//
+	go a.graph(ctx)
+	a.logger.Printf("initialized asciigraph output: %s", a.String())
+	return nil
+}
+
+func (a *asciigraphOutput) setDefaults() error {
+	a.labelColor = defaultLabelColor
+	if a.cfg.LabelColor != "" {
+		if lc, ok := asciigraph.ColorNames[a.cfg.LabelColor]; ok {
+			a.labelColor = lc
+		} else {
+			return fmt.Errorf("unknown label color %s", a.cfg.LabelColor)
+		}
+	}
+
+	a.captionColor = defaultCaptionColor
+	if a.cfg.CaptionColor != "" {
+		if lc, ok := asciigraph.ColorNames[a.cfg.CaptionColor]; ok {
+			a.captionColor = lc
+		} else {
+			return fmt.Errorf("unknown caption color %s", a.cfg.CaptionColor)
+		}
+	}
+
+	a.axisColor = defaultAxisColor
+	if a.cfg.AxisColor != "" {
+		if lc, ok := asciigraph.ColorNames[a.cfg.AxisColor]; ok {
+			a.axisColor = lc
+		} else {
+			return fmt.Errorf("unknown axis color %s", a.cfg.AxisColor)
+		}
+
+	}
+
+	if a.cfg.RefreshTimer <= 0 {
+		a.cfg.RefreshTimer = defaultRefreshTimer
+	}
+	if a.cfg.Precision <= 0 {
+		a.cfg.Precision = defaultPrecision
+	}
+
+	return a.getTermSize()
+}
+
+// Write //
+func (a *asciigraphOutput) Write(ctx context.Context, rsp proto.Message, meta outputs.Meta) {
+	if rsp == nil {
+		return
+	}
+
+	subRsp, err := outputs.AddSubscriptionTarget(rsp, meta, a.cfg.AddTarget, a.targetTpl)
+	if err != nil {
+		a.logger.Printf("failed to add target to the response: %v", err)
+		return
+	}
+	evs, err := formatters.ResponseToEventMsgs(meta["subscription-name"], subRsp, meta, a.evps...)
+	if err != nil {
+		a.logger.Printf("failed to convert messages to events: %v", err)
+		return
+	}
+	for _, ev := range evs {
+		a.WriteEvent(ctx, ev)
+	}
+}
+
+func (a *asciigraphOutput) WriteEvent(ctx context.Context, ev *formatters.EventMsg) {
+	ctx, cancel := context.WithTimeout(ctx, defaultTimeout)
+	defer cancel()
+	select {
+	case <-ctx.Done():
+		a.logger.Printf("write timeout: %v", ctx.Err())
+	case a.eventCh <- ev:
+	}
+}
+
+// Close //
+func (a *asciigraphOutput) Close() error {
+	return nil
+}
+
+// Metrics //
+func (a *asciigraphOutput) RegisterMetrics(reg *prometheus.Registry) {
+}
+
+func (a *asciigraphOutput) SetName(name string) {}
+
+func (a *asciigraphOutput) SetClusterName(name string) {}
+
+func (a *asciigraphOutput) SetTargetsConfig(map[string]*types.TargetConfig) {}
+
+func (a *asciigraphOutput) graph(ctx context.Context) {
+	for {
+		select {
+		case <-ctx.Done():
+			return
+		case ev, ok := <-a.eventCh:
+			if !ok {
+				return
+			}
+			a.plot(ev)
+		case <-time.After(a.cfg.RefreshTimer):
+			a.plot(nil)
+		}
+	}
+}
+
+func (a *asciigraphOutput) plot(e *formatters.EventMsg) {
+	a.m.Lock()
+	defer a.m.Unlock()
+	a.getTermSize()
+	if e != nil && len(e.Values) > 0 {
+		a.updateData(e)
+	}
+
+	data, colors := a.buildData()
+	if len(data) == 0 {
+		return
+	}
+	opts := []asciigraph.Option{
+		asciigraph.Height(a.cfg.Height),
+		asciigraph.Width(a.cfg.Width),
+		asciigraph.Offset(a.cfg.Offset),
+		asciigraph.Precision(a.cfg.Precision),
+		asciigraph.Caption(a.caption),
+		asciigraph.CaptionColor(a.captionColor),
+		asciigraph.SeriesColors(colors...),
+		asciigraph.AxisColor(a.axisColor),
+		asciigraph.LabelColor(a.labelColor),
+	}
+	if a.cfg.LowerBound != nil {
+		opts = append(opts, asciigraph.LowerBound(*a.cfg.LowerBound))
+	}
+	if a.cfg.UpperBound != nil {
+		opts = append(opts, asciigraph.UpperBound(*a.cfg.UpperBound))
+	}
+	plot := asciigraph.PlotMany(data, opts...)
+	asciigraph.Clear()
+	fmt.Fprintln(os.Stdout, plot)
+}
+
+func (a *asciigraphOutput) updateData(e *formatters.EventMsg) {
+	if e == nil {
+		return
+	}
+	evs := splitEvent(e)
+	for _, ev := range evs {
+		sn := a.buildSeriesName(e)
+		serie := a.getOrCreateSerie(sn)
+		for _, v := range ev.Values {
+			i, err := toFloat(v)
+			if err != nil {
+				continue
+			}
+			serie.data = append(serie.data, i)
+			break
+		}
+	}
+}
+
+func (a *asciigraphOutput) getOrCreateSerie(name string) *series {
+	serie, ok := a.data[name]
+	if ok {
+		return serie
+	}
+	color := a.pickColor()
+	serie = &series{
+		name:  name,
+		data:  make([]float64, 0, a.cfg.Width-a.cfg.Offset),
+		color: color,
+	}
+	a.data[name] = serie
+	a.colors[serie.color] = struct{}{}
+
+	a.setCaption()
+	return serie
+}
+
+func (a *asciigraphOutput) setCaption() {
+	seriesNames := make([]string, 0, len(a.data))
+	for seriesName := range a.data {
+		seriesNames = append(seriesNames, seriesName)
+	}
+	sort.Strings(seriesNames)
+	a.caption = ""
+	if a.cfg.Debug {
+		a.caption = fmt.Sprintf("(h=%d,w=%d)\n", a.cfg.Height, a.cfg.Width)
+	}
+	a.caption = fmt.Sprintf("%s\n", a.cfg.Caption)
+
+	for _, sn := range seriesNames {
+		color := a.data[sn].color
+		a.caption += color.String() + "-+- " + sn + asciigraph.Default.String() + "\n"
+	}
+}
+
+func (a *asciigraphOutput) buildData() ([][]float64, []asciigraph.AnsiColor) {
+	numgraphs := len(a.data)
+	series := make([]*series, 0, numgraphs)
+	// sort series by name
+	for _, serie := range a.data {
+		size := len(serie.data)
+		if size == 0 {
+			continue
+		}
+		if size > a.cfg.Width {
+			serie.data = serie.data[size-a.cfg.Width:]
+		}
+		series = append(series, serie)
+	}
+	sort.Slice(series,
+		func(i, j int) bool {
+			return series[i].name < series[j].name
+		})
+
+	data := make([][]float64, 0, numgraphs)
+	colors := make([]asciigraph.AnsiColor, 0, numgraphs)
+	// get float slices and colors
+	for _, serie := range series {
+		data = append(data, serie.data)
+		colors = append(colors, serie.color)
+	}
+	return data, colors
+}
+
+func splitEvent(e *formatters.EventMsg) []*formatters.EventMsg {
+	numVals := len(e.Values)
+	switch numVals {
+	case 0:
+		return nil
+	case 1:
+		return []*formatters.EventMsg{e}
+	}
+
+	evs := make([]*formatters.EventMsg, 0, numVals)
+	for k, v := range e.Values {
+		ev := &formatters.EventMsg{
+			Name:      e.Name,
+			Timestamp: e.Timestamp,
+			Tags:      e.Tags,
+			Values:    map[string]interface{}{k: v},
+		}
+		evs = append(evs, ev)
+	}
+	return evs
+}
+
+func (a *asciigraphOutput) buildSeriesName(e *formatters.EventMsg) string {
+	sb := &strings.Builder{}
+	sb.WriteString(e.Name)
+	sb.WriteString(":")
+	for k := range e.Values {
+		sb.WriteString(k)
+	}
+	numTags := len(e.Tags)
+	if numTags == 0 {
+		return sb.String()
+	}
+	sb.WriteString("{")
+	tagNames := make([]string, 0, numTags)
+	for k := range e.Tags {
+		tagNames = append(tagNames, k)
+	}
+	sort.Strings(tagNames)
+	for i, tn := range tagNames {
+		fmt.Fprintf(sb, "%s=%s", tn, e.Tags[tn])
+		if numTags != i+1 {
+			sb.WriteString(", ")
+		}
+	}
+	sb.WriteString("}")
+	return sb.String()
+}
+
+func toFloat(v interface{}) (float64, error) {
+	switch i := v.(type) {
+	case float64:
+		return float64(i), nil
+	case float32:
+		return float64(i), nil
+	case int64:
+		return float64(i), nil
+	case int32:
+		return float64(i), nil
+	case int16:
+		return float64(i), nil
+	case int8:
+		return float64(i), nil
+	case uint64:
+		return float64(i), nil
+	case uint32:
+		return float64(i), nil
+	case uint16:
+		return float64(i), nil
+	case uint8:
+		return float64(i), nil
+	case int:
+		return float64(i), nil
+	case uint:
+		return float64(i), nil
+	case string:
+		f, err := strconv.ParseFloat(i, 64)
+		if err != nil {
+			return math.NaN(), err
+		}
+		return f, err
+		//lint:ignore SA1019 still need DecimalVal for backward compatibility
+	case *gnmi.Decimal64:
+		return float64(i.Digits) / math.Pow10(int(i.Precision)), nil
+	default:
+		return math.NaN(), errors.New("getFloat: unknown value is of incompatible type")
+	}
+}
+
+func (a *asciigraphOutput) pickColor() asciigraph.AnsiColor {
+	for _, c := range asciigraph.ColorNames {
+		if _, ok := a.colors[c]; !ok {
+			return c
+		}
+	}
+	return 0
+}
+
+func (a *asciigraphOutput) getTermSize() error {
+	err := termbox.Init()
+	if err != nil {
+		return fmt.Errorf("could not initialize a terminal box: %v", err)
+	}
+	w, h := termbox.Size()
+	termbox.Close()
+	if a.cfg.Width <= 0 || a.cfg.Width > w-10 {
+		a.cfg.Width = w - 10
+	}
+	numSeries := len(a.data)
+	if a.cfg.Height <= 0 || a.cfg.Height > h-(numSeries+1)-5 {
+		a.cfg.Height = h - (numSeries + 1) - 5
+	}
+	return nil
+}
diff --git a/outputs/output.go b/outputs/output.go
index db080c23..4d70cacb 100644
--- a/outputs/output.go
+++ b/outputs/output.go
@@ -19,13 +19,14 @@ import (
 
 	"github.com/mitchellh/mapstructure"
 	"github.com/openconfig/gnmi/proto/gnmi"
+	"github.com/prometheus/client_golang/prometheus"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
+
 	"github.com/openconfig/gnmic/formatters"
 	_ "github.com/openconfig/gnmic/formatters/all"
 	"github.com/openconfig/gnmic/types"
 	"github.com/openconfig/gnmic/utils"
-	"github.com/prometheus/client_golang/prometheus"
-	"google.golang.org/protobuf/proto"
-	"google.golang.org/protobuf/reflect/protoreflect"
 )
 
 type Output interface {
@@ -60,6 +61,7 @@ var OutputTypes = map[string]struct{}{
 	"gnmi":             {},
 	"jetstream":        {},
 	"snmp":             {},
+	"asciigraph":       {},
 }
 
 func Register(name string, initFn Initializer) {