diff --git a/CMakeLists.txt b/CMakeLists.txt
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,5 @@
 #
-# Copyright © 2018 Hasan Yavuz Özderya
+# Copyright © 2019 Hasan Yavuz Özderya
 #
 # This file is part of serialplot.
 #
@@ -44,31 +44,11 @@ else (BUILD_QWT)
     find_package(Qwt 6.1 REQUIRED)
 endif (BUILD_QWT)
 
-# If set, cmake will download QtColorWidgets over git, build and use it as a static library.
-set(BUILD_QTCOLORWIDGETS true CACHE BOOL "Download and build QtColorWidgets library automatically.")
-if (BUILD_QTCOLORWIDGETS)
-  include(BuildQColorWidgets)
-else ()
-  find_package(QtColorWidgets REQUIRED)
-endif ()
-
-set(BUILD_LEDWIDGET true CACHE BOOL "Download and build LedWidget automatically.")
-if (BUILD_LEDWIDGET)
-  include(BuildLedWidget)
-else (BUILD_LEDWIDGET)
-  include(FindLedWidget)
-endif (BUILD_LEDWIDGET)
-
 # includes
 include_directories("./src"
   ${QWT_INCLUDE_DIR}
-  ${QTCOLORWIDGETS_INCLUDE_DIRS}
-  ${LEDWIDGET_INCLUDE_DIR}
   )
 
-# flags
-set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${QTCOLORWIDGETS_FLAGS}")
-
 # wrap UI and resource files
 qt5_wrap_ui(UI_FILES
   src/mainwindow.ui
@@ -87,6 +67,7 @@ qt5_wrap_ui(UI_FILES
   src/framedreadersettings.ui
   src/demoreadersettings.ui
   src/updatecheckdialog.ui
+  src/datatextview.ui
   )
 
 if (WIN32)
@@ -126,6 +107,7 @@ add_executable(${PROGRAM_NAME} WIN32
   src/ringbuffer.cpp
   src/ringbuffer.cpp
   src/indexbuffer.cpp
+  src/linindexbuffer.cpp
   src/readonlybuffer.cpp
   src/framebufferseries.cpp
   src/numberformatbox.cpp
@@ -152,6 +134,9 @@ add_executable(${PROGRAM_NAME} WIN32
   src/source.cpp
   src/sink.cpp
   src/samplecounter.cpp
+  src/ledwidget.cpp
+  src/datatextview.cpp
+  src/bpslabel.cpp
   misc/windows_icon.rc
   ${UI_FILES}
   ${RES_FILES}
@@ -160,8 +145,6 @@ add_executable(${PROGRAM_NAME} WIN32
 # Use the Widgets module from Qt 5.
 target_link_libraries(${PROGRAM_NAME}
   ${QWT_LIBRARY}
-  ${QTCOLORWIDGETS_LIBRARIES}
-  ${LEDWIDGET_LIBRARY}
   )
 qt5_use_modules(${PROGRAM_NAME} Widgets SerialPort Network)
 
@@ -169,15 +152,6 @@ if (BUILD_QWT)
   add_dependencies(${PROGRAM_NAME} QWT)
 endif ()
 
-if (BUILD_QTCOLORWIDGETS)
-  add_dependencies(${PROGRAM_NAME} QCW)
-endif ()
-
-if (BUILD_LEDWIDGET)
-  add_dependencies(${PROGRAM_NAME} LEDW)
-endif (BUILD_LEDWIDGET)
-
-
 # set compiler flags
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
 
@@ -205,6 +179,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} 
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_MINOR=${VERSION_MINOR} ")
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_PATCH=${VERSION_PATCH} ")
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_REVISION=\\\"${VERSION_REVISION}\\\" ")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DPROGRAM_NAME=\\\"${PROGRAM_NAME}\\\" ")
 
 # add make run target
 add_custom_target(run
diff --git a/cmake/modules/BuildLedWidget.cmake b/cmake/modules/BuildLedWidget.cmake
deleted file mode 100644
--- a/cmake/modules/BuildLedWidget.cmake
+++ /dev/null
@@ -1,30 +0,0 @@
-#
-# Copyright © 2017 Hasan Yavuz Özderya
-#
-# This file is part of serialplot.
-#
-# serialplot is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# serialplot is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with serialplot.  If not, see .
-#
-
-include(ExternalProject)
-
-ExternalProject_Add(LEDW
-  PREFIX ledw
-  HG_REPOSITORY https://bitbucket.org/hyOzd/ledwidget
-  UPDATE_COMMAND ""
-  INSTALL_COMMAND "")
-
-ExternalProject_Get_Property(LEDW binary_dir source_dir)
-set(LEDWIDGET_INCLUDE_DIR ${source_dir}/src)
-set(LEDWIDGET_LIBRARY ${binary_dir}/libledwidget.a)
diff --git a/cmake/modules/BuildQColorWidgets.cmake b/cmake/modules/BuildQColorWidgets.cmake
deleted file mode 100644
--- a/cmake/modules/BuildQColorWidgets.cmake
+++ /dev/null
@@ -1,36 +0,0 @@
-#
-# Copyright © 2018 Hasan Yavuz Özderya
-#
-# This file is part of serialplot.
-#
-# serialplot is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# serialplot is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with serialplot.  If not, see .
-#
-
-include(ExternalProject)
-
-ExternalProject_Add(QCW
-  PREFIX qcw
-  GIT_REPOSITORY https://gitlab.com/mattia.basaglia/Qt-Color-Widgets.git
-  GIT_TAG 2c49e1bb4e1f591e720e2132cc2aaeef3ba73f14
-  CMAKE_CACHE_ARGS "-DCMAKE_CXX_FLAGS:string=-D QTCOLORWIDGETS_STATICALLY_LINKED"
-  UPDATE_COMMAND ""
-  INSTALL_COMMAND "")
-
-ExternalProject_Get_Property(QCW binary_dir source_dir)
-set(QTCOLORWIDGETS_FLAGS "-D QTCOLORWIDGETS_STATICALLY_LINKED")
-set(QTCOLORWIDGETS_LIBRARY ${binary_dir}/libColorWidgets-qt5.a)
-set(QTCOLORWIDGETS_INCLUDE_DIR ${source_dir}/include)
-
-set(QTCOLORWIDGETS_LIBRARIES ${QTCOLORWIDGETS_LIBRARY})
-set(QTCOLORWIDGETS_INCLUDE_DIRS ${QTCOLORWIDGETS_INCLUDE_DIR})
diff --git a/cmake/modules/BuildQwt.cmake b/cmake/modules/BuildQwt.cmake
--- a/cmake/modules/BuildQwt.cmake
+++ b/cmake/modules/BuildQwt.cmake
@@ -1,5 +1,5 @@
 #
-# Copyright © 2016 Hasan Yavuz Özderya
+# Copyright © 2019 Hasan Yavuz Özderya
 #
 # This file is part of serialplot.
 #
@@ -21,7 +21,8 @@ include(ExternalProject)
 
 ExternalProject_Add(QWT
   PREFIX qwt
-  SVN_REPOSITORY svn://svn.code.sf.net/p/qwt/code/branches/qwt-6.1
+  # SVN_REPOSITORY svn://svn.code.sf.net/p/qwt/code/branches/qwt-6.1
+  URL https://sourceforge.net/projects/qwt/files/qwt/6.1.4/qwt-6.1.4.tar.bz2
   # disable QwtDesigner plugin and enable static build
   PATCH_COMMAND sed -i -r -e "s/QWT_CONFIG\\s*\\+=\\s*QwtDesigner/#&/"
                           -e "s/QWT_CONFIG\\s*\\+=\\s*QwtDll/#&/"
diff --git a/cmake/modules/FindQtColorWidgets.cmake b/cmake/modules/FindQtColorWidgets.cmake
deleted file mode 100644
--- a/cmake/modules/FindQtColorWidgets.cmake
+++ /dev/null
@@ -1,35 +0,0 @@
-#
-# Copyright © 2017 Hasan Yavuz Özderya
-#
-# This file is part of serialplot.
-#
-# serialplot is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# serialplot is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with serialplot.  If not, see .
-#
-
-# Note: this script is intended for the debian package created for serialplot.
-
-find_library(QTCOLORWIDGETS_LIBRARY "libColorWidgets-qt5.a")
-find_path(QTCOLORWIDGETS_INCLUDE_DIR "color_preview.hpp" PATHS "/usr/include/qtcolorwidgets/" NO_DEFAULT_PATH)
-
-mark_as_advanced(QTCOLORWIDGETS_LIBRARY QTCOLORWIDGETS_INCLUDE_DIR)
-
-include(FindPackageHandleStandardArgs)
-find_package_handle_standard_args(QtColorWidgets DEFAULT_MSG QTCOLORWIDGETS_LIBRARY QTCOLORWIDGETS_INCLUDE_DIR)
-
-if (QTCOLORWIDGETS_FOUND)
-  set(QTCOLORWIDGETS_FLAGS "-D QTCOLORWIDGETS_STATICALLY_LINKED")
-  mark_as_advanced(QTCOLORWIDGETS_FLAGS)
-  set(QTCOLORWIDGETS_LIBRARIES ${QTCOLORWIDGETS_LIBRARY})
-  set(QTCOLORWIDGETS_INCLUDE_DIRS ${QTCOLORWIDGETS_INCLUDE_DIR})
-endif (QTCOLORWIDGETS_FOUND)
diff --git a/cmake/modules/FindQwt.cmake b/cmake/modules/FindQwt.cmake
--- a/cmake/modules/FindQwt.cmake
+++ b/cmake/modules/FindQwt.cmake
@@ -1,5 +1,5 @@
 #
-# Copyright © 2017 Hasan Yavuz Özderya
+# Copyright © 2019 Hasan Yavuz Özderya
 #
 # This file is part of serialplot.
 #
@@ -66,8 +66,7 @@ endif(qwt_roots)
 
 if(QWT_ROOT)
   set(QWT_INCLUDE_DIR "${QWT_ROOT}/include")
-  find_library(QWT_LIBRARY "qwt-qt5"
-	PATHS "${QWT_ROOT}/lib")
+  find_library(QWT_LIBRARY NAMES "qwt-qt5" "qwt" PATHS "${QWT_ROOT}/lib")
 else (QWT_ROOT)
   ## Look into system locations
   find_path(QWT_INCLUDE_DIR qwt_plot.h PATHS /usr/include/qwt)
@@ -90,7 +89,7 @@ else (QWT_ROOT)
 	endif(qwt_version_string)
   endif (QWT_INCLUDE_DIR)
   # look into system locations for lib file
-  find_library(QWT_LIBRARY "qwt-qt5" PATHS /usr/lib)
+  find_library(QWT_LIBRARY NAMES "qwt-qt5" "qwt" PATHS /usr/lib)
 endif(QWT_ROOT)
 
 # set version variables
@@ -103,8 +102,24 @@ if(QWT_VERSION)
 	QWT_PATCH_VERSION ${QWT_VERSION})
 endif(QWT_VERSION)
 
+# check Qwt library 'Qt' version
+if (QWT_LIBRARY)
+  include(GetPrerequisites)
+  GET_PREREQUISITES(${QWT_LIBRARY} qwt_lib_deps 0 0 "" "")
+  set(qwt_is_qt5 FALSE)
+  foreach (dep ${qwt_lib_deps})
+    if (${dep} MATCHES "libQt5Gui")
+      set(qwt_is_qt5 TRUE)
+    endif()
+  endforeach ()
+  if (NOT qwt_is_qt5)
+    message(WARNING "Found qwt library (${QWT_LIBRARY}) isn't compiled with Qt5!")
+    LIST_PREREQUISITES(${QWT_LIBRARY})
+  endif()
+endif (QWT_LIBRARY)
+
 # set QWT_FOUND
-if(QWT_INCLUDE_DIR AND QWT_LIBRARY)
+if(QWT_INCLUDE_DIR AND QWT_LIBRARY AND qwt_is_qt5)
   set(QWT_INCLUDE_DIRS ${QWT_INCLUDE_DIR})
   set(QWT_LIBRARIES ${QWT_LIBRARY})
   set(QWT_FOUND true)
@@ -114,6 +129,9 @@ endif()
 
 # errors
 if(NOT QWT_FOUND)
+  unset(QWT_INCLUDE_DIR CACHE)
+  unset(QWT_LIBRARY CACHE)
+
   if(Qwt_FIND_QUIET)
 	message(WARNING "Couldn't find Qwt.")
   elseif(Qwt_FIND_REQUIRED)
diff --git a/serialplot.pro b/serialplot.pro
--- a/serialplot.pro
+++ b/serialplot.pro
@@ -1,5 +1,5 @@
 #
-# Copyright © 2015 Hasan Yavuz Özderya
+# Copyright © 2019 Hasan Yavuz Özderya
 #
 # This file is part of serialplot.
 #
@@ -23,7 +23,7 @@
 #
 #-------------------------------------------------
 
-QT       += core gui serialport
+QT       += core gui serialport network svg
 
 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
 
@@ -31,47 +31,72 @@ TARGET = serialplot
 TEMPLATE = app
 
 CONFIG += qwt
+# LIBS += -lqwt # enable this line if qwt pri files aren't installed
+
+DEFINES += PROGRAM_NAME="\\\"serialplot\\\""
+
+DEFINES += VERSION_MAJOR=10 VERSION_MINOR=0 VERSION_PATCH=0 VERSION_STRING=\\\"10.0.0\\\"
 
 SOURCES += \
-    src/main.cpp\
+    src/main.cpp \
     src/mainwindow.cpp \
     src/portcontrol.cpp \
     src/plot.cpp \
     src/zoomer.cpp \
+    src/scrollzoomer.cpp \
+    src/scrollbar.cpp \
     src/hidabletabwidget.cpp \
-    src/framebuffer.cpp \
     src/scalepicker.cpp \
     src/scalezoomer.cpp \
     src/portlist.cpp \
+    src/snapshot.cpp \
     src/snapshotview.cpp \
     src/snapshotmanager.cpp \
-    src/snapshot.cpp \
     src/plotsnapshotoverlay.cpp \
     src/commandpanel.cpp \
     src/commandwidget.cpp \
     src/commandedit.cpp \
     src/dataformatpanel.cpp \
+    src/plotcontrolpanel.cpp \
+    src/recordpanel.cpp \
+    src/datarecorder.cpp \
     src/tooltipfilter.cpp \
     src/sneakylineedit.cpp \
-    src/channelmanager.cpp \
+    src/stream.cpp \
+    src/streamchannel.cpp \
+    src/channelinfomodel.cpp \
+    src/ringbuffer.cpp \
+    src/indexbuffer.cpp \
+    src/linindexbuffer.cpp \
+    src/readonlybuffer.cpp \
     src/framebufferseries.cpp \
-    src/plotcontrolpanel.cpp \
     src/numberformatbox.cpp \
     src/endiannessbox.cpp \
-    src/framedreadersettings.cpp \
     src/abstractreader.cpp \
     src/binarystreamreader.cpp \
     src/binarystreamreadersettings.cpp \
+    src/asciireader.cpp \
     src/asciireadersettings.cpp \
-    src/asciireader.cpp \
     src/demoreader.cpp \
+    src/demoreadersettings.cpp \
     src/framedreader.cpp \
+    src/framedreadersettings.cpp \
     src/plotmanager.cpp \
+    src/plotmenu.cpp \
+    src/barplot.cpp \
+    src/barchart.cpp \
+    src/barscaledraw.cpp \
     src/numberformat.cpp \
-    src/recordpanel.cpp \
     src/updatechecker.cpp \
+    src/versionnumber.cpp \
     src/updatecheckdialog.cpp \
-    src/demoreadersettings.cpp
+    src/samplepack.cpp \
+    src/source.cpp \
+    src/sink.cpp \
+    src/samplecounter.cpp \
+    src/ledwidget.cpp \
+    src/datatextview.cpp \
+    src/bpslabel.cpp
 
 HEADERS += \
     src/mainwindow.h \
@@ -79,7 +104,6 @@ HEADERS += \
     src/portcontrol.h \
     src/floatswap.h \
     src/plot.h \
-    src/zoomer.h \
     src/hidabletabwidget.h \
     src/framebuffer.h \
     src/scalepicker.h \
@@ -95,7 +119,6 @@ HEADERS += \
     src/dataformatpanel.h \
     src/tooltipfilter.h \
     src/sneakylineedit.h \
-    src/channelmanager.h \
     src/framebufferseries.h \
     src/plotcontrolpanel.h \
     src/numberformatbox.h \
@@ -114,7 +137,32 @@ HEADERS += \
     src/recordpanel.h \
     src/updatechecker.h \
     src/updatecheckdialog.h \
-    src/demoreadersettings.h
+    src/demoreadersettings.h \
+    src/datatextview.h \
+    src/bpslabel.h \
+    src/barchart.h \
+    src/barplot.h \
+    src/barscaledraw.h \
+    src/channelinfomodel.h \
+    src/datarecorder.h \
+    src/defines.h \
+    src/indexbuffer.h \
+    src/ledwidget.h \
+    src/linindexbuffer.h \
+    src/plotmenu.h \
+    src/readonlybuffer.h \
+    src/ringbuffer.h \
+    src/samplecounter.h \
+    src/samplepack.h \
+    src/scrollbar.h \
+    src/scrollzoomer.h \
+    src/sink.h \
+    src/source.h \
+    src/streamchannel.h \
+    src/stream.h \
+    src/version.h \
+    src/versionnumber.h \
+    src/zoomer.h
 
 FORMS += \
     src/mainwindow.ui \
@@ -132,7 +180,8 @@ FORMS += \
     src/asciireadersettings.ui \
     src/recordpanel.ui \
     src/updatecheckdialog.ui \
-    src/demoreadersettings.ui
+    src/demoreadersettings.ui \
+    src/datatextview.ui
 
 INCLUDEPATH += qmake/ src/
 
diff --git a/src/abstractreader.cpp b/src/abstractreader.cpp
--- a/src/abstractreader.cpp
+++ b/src/abstractreader.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -23,6 +23,7 @@ AbstractReader::AbstractReader(QIODevice
     QObject(parent)
 {
     _device = device;
+    bytesRead = 0;
 }
 
 void AbstractReader::pause(bool enabled)
@@ -43,3 +44,15 @@ void AbstractReader::enable(bool enabled
         disconnectSinks();
     }
 }
+
+void AbstractReader::onDataReady()
+{
+    bytesRead += readData();
+}
+
+unsigned AbstractReader::getBytesRead()
+{
+    unsigned r = bytesRead;
+    bytesRead = 0;
+    return r;
+}
diff --git a/src/abstractreader.h b/src/abstractreader.h
--- a/src/abstractreader.h
+++ b/src/abstractreader.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -49,6 +49,9 @@ public:
     /// None of the current readers support X channel at the moment
     bool hasX() const final { return false; };
 
+    /// Read and 'zero' the byte counter
+    unsigned getBytesRead();
+
 signals:
     // TODO: should we keep this?
     void numOfChannelsChanged(unsigned);
@@ -63,12 +66,25 @@ public slots:
     void pause(bool enabled);
 
 protected:
+    /// Reader should read from this device in `readData()` function.
     QIODevice* _device;
+
+    /// Reader should check this variable to determine if reading is
+    /// paused in `readData()`
     bool paused;
 
-protected slots:
-    /// all derived readers has to override this function
-    virtual void onDataReady() = 0;
+    /**
+     * Called when `readyRead` is signaled by the device. This is
+     * where the implementors should read the data and return the
+     * exact number of bytes read from the device.
+     */
+    virtual unsigned readData() = 0;
+
+private:
+    unsigned bytesRead;
+
+private slots:
+    void onDataReady();
 };
 
 #endif // ABSTRACTREADER_H
diff --git a/src/asciireader.cpp b/src/asciireader.cpp
--- a/src/asciireader.cpp
+++ b/src/asciireader.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -70,21 +70,20 @@ void AsciiReader::enable(bool enabled)
     if (enabled)
     {
         firstReadAfterEnable = true;
-        QObject::connect(_device, &QIODevice::readyRead,
-                         this, &AsciiReader::onDataReady);
     }
-    else
-    {
-        QObject::disconnect(_device, 0, this, 0);
-        disconnectSinks();
-    }
+
+    AbstractReader::enable(enabled);
 }
 
-void AsciiReader::onDataReady()
+unsigned AsciiReader::readData()
 {
+    unsigned numBytesRead = 0;
+
     while(_device->canReadLine())
     {
-        QString line = QString(_device->readLine());
+        QByteArray bytes = _device->readLine();
+        QString line = QString(bytes);
+        numBytesRead += bytes.size();
 
         // discard only once when we just started reading
         if (firstReadAfterEnable)
@@ -127,8 +126,11 @@ void AsciiReader::onDataReady()
 
             // commit data
             feedOut(*samples);
+            delete samples;
         }
     }
+
+    return numBytesRead;
 }
 
 SamplePack* AsciiReader::parseLine(const QString& line) const
diff --git a/src/asciireader.h b/src/asciireader.h
--- a/src/asciireader.h
+++ b/src/asciireader.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -50,8 +50,10 @@ private:
 
     bool firstReadAfterEnable = false;
 
+    unsigned readData() override;
+
 private slots:
-    void onDataReady() override;
+
     /**
      * Parses given line and returns sample pack.
      *
diff --git a/src/asciireadersettings.cpp b/src/asciireadersettings.cpp
--- a/src/asciireadersettings.cpp
+++ b/src/asciireadersettings.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -21,6 +21,7 @@
 #include 
 
 #include "utils.h"
+#include "defines.h"
 #include "setting_defines.h"
 
 #include "asciireadersettings.h"
@@ -35,6 +36,8 @@ AsciiReaderSettings::AsciiReaderSettings
     auto validator = new QRegularExpressionValidator(QRegularExpression("[^\\d]?"), this);
     ui->leDelimiter->setValidator(validator);
 
+    ui->spNumOfChannels->setMaximum(MAX_NUM_CHANNELS);
+
     connect(ui->rbComma, &QAbstractButton::toggled,
             this, &AsciiReaderSettings::delimiterToggled);
     connect(ui->rbSpace, &QAbstractButton::toggled,
diff --git a/src/binarystreamreader.cpp b/src/binarystreamreader.cpp
--- a/src/binarystreamreader.cpp
+++ b/src/binarystreamreader.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -107,50 +107,58 @@ void BinaryStreamReader::onNumOfChannels
     emit numOfChannelsChanged(value);
 }
 
-void BinaryStreamReader::onDataReady()
+unsigned BinaryStreamReader::readData()
 {
     // a package is a set of channel data like {CHAN0_SAMPLE, CHAN1_SAMPLE...}
-    int packageSize = sampleSize * _numChannels;
-    int bytesAvailable = _device->bytesAvailable();
+    unsigned packageSize = sampleSize * _numChannels;
+    unsigned bytesAvailable = _device->bytesAvailable();
+    unsigned totalRead = 0;
 
     // skip 1 byte if requested
     if (skipByteRequested && bytesAvailable > 0)
     {
         _device->read(1);
+        totalRead++;
         skipByteRequested = false;
         bytesAvailable--;
     }
 
     // skip 1 sample (channel) if requested
-    if (skipSampleRequested && bytesAvailable >= (int) sampleSize)
+    if (skipSampleRequested && bytesAvailable >= sampleSize)
     {
         _device->read(sampleSize);
+        totalRead += sampleSize;
         skipSampleRequested = false;
         bytesAvailable -= sampleSize;
     }
 
-    if (bytesAvailable < packageSize) return;
+    if (bytesAvailable < packageSize) return totalRead;
 
-    int numOfPackagesToRead =
+    unsigned numOfPackagesToRead =
         (bytesAvailable - (bytesAvailable % packageSize)) / packageSize;
+    unsigned numBytesToRead = numOfPackagesToRead * packageSize;
+
+    totalRead += numBytesToRead;
 
     if (paused)
     {
         // read and discard data
-        _device->read(numOfPackagesToRead*packageSize);
-        return;
+        _device->read(numBytesToRead);
+        return totalRead;
     }
 
     // actual reading
     SamplePack samples(numOfPackagesToRead, _numChannels);
-    for (int i = 0; i < numOfPackagesToRead; i++)
+    for (unsigned i = 0; i < numOfPackagesToRead; i++)
     {
-        for (unsigned int ci = 0; ci < _numChannels; ci++)
+        for (unsigned ci = 0; ci < _numChannels; ci++)
         {
             samples.data(ci)[i] = (this->*readSample)();
         }
     }
     feedOut(samples);
+
+    return totalRead;
 }
 
 template double BinaryStreamReader::readSampleAs()
diff --git a/src/binarystreamreader.h b/src/binarystreamreader.h
--- a/src/binarystreamreader.h
+++ b/src/binarystreamreader.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -60,10 +60,11 @@ private:
      */
     template double readSampleAs();
 
+    unsigned readData() override;
+
 private slots:
     void onNumberFormatChanged(NumberFormat numberFormat);
     void onNumOfChannelsChanged(unsigned value);
-    void onDataReady() override;
 };
 
 #endif // BINARYSTREAMREADER_H
diff --git a/src/binarystreamreadersettings.cpp b/src/binarystreamreadersettings.cpp
--- a/src/binarystreamreadersettings.cpp
+++ b/src/binarystreamreadersettings.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2016 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -21,6 +21,7 @@
 #include "ui_binarystreamreadersettings.h"
 
 #include "utils.h"
+#include "defines.h"
 #include "setting_defines.h"
 
 BinaryStreamReaderSettings::BinaryStreamReaderSettings(QWidget *parent) :
@@ -29,6 +30,8 @@ BinaryStreamReaderSettings::BinaryStream
 {
     ui->setupUi(this);
 
+    ui->spNumOfChannels->setMaximum(MAX_NUM_CHANNELS);
+
     // Note: if directly connected we get a runtime warning on incompatible signal arguments
     connect(ui->spNumOfChannels, SELECT::OVERLOAD_OF(&QSpinBox::valueChanged),
             [this](int value)
diff --git a/src/bpslabel.cpp b/src/bpslabel.cpp
new file mode 100644
--- /dev/null
+++ b/src/bpslabel.cpp
@@ -0,0 +1,80 @@
+/*
+  Copyright © 2019 Hasan Yavuz Özderya
+
+  This file is part of serialplot.
+
+  serialplot is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  serialplot is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with serialplot.  If not, see .
+*/
+
+#include "bpslabel.h"
+
+const char* BPS_TOOLTIP = "bits per second";
+const char* BPS_TOOLTIP_ERR = "Maximum baud rate may be reached!";
+
+BPSLabel::BPSLabel(PortControl* portControl,
+                   DataFormatPanel* dataFormatPanel,
+                   QWidget *parent) :
+    QLabel(parent)
+{
+    _portControl = portControl;
+    _dataFormatPanel = dataFormatPanel;
+    prevBytesRead = 0;
+
+    setText("0bps");
+    setToolTip(tr(BPS_TOOLTIP));
+
+    connect(&bpsTimer, &QTimer::timeout,
+            this, &BPSLabel::onBpsTimeout);
+
+    connect(portControl, &PortControl::portToggled,
+            this, &BPSLabel::onPortToggled);
+}
+
+void BPSLabel::onBpsTimeout()
+{
+    uint64_t curBytesRead = _dataFormatPanel->bytesRead();
+    uint64_t bytesRead = curBytesRead - prevBytesRead;
+    prevBytesRead = curBytesRead;
+
+    unsigned bits = bytesRead * 8;
+    unsigned maxBps = _portControl->maxBitRate();
+    QString str;
+    if (bits >= maxBps)
+    {
+        // TODO: an icon for bps warning
+        str = QString(tr("!%1/%2bps")).arg(bits).arg(maxBps);
+        setToolTip(tr(BPS_TOOLTIP_ERR));
+    }
+    else
+    {
+        str = QString(tr("%1bps")).arg(bits);
+        setToolTip(tr(BPS_TOOLTIP));
+    }
+    setText(str);
+}
+
+void BPSLabel::onPortToggled(bool open)
+{
+    if (open)
+    {
+        bpsTimer.start(1000);
+    }
+    else
+    {
+        bpsTimer.stop();
+        // if not cleared last displayed value is stuck
+        setText("0bps");
+        setToolTip(tr(BPS_TOOLTIP));
+    }
+}
diff --git a/src/bpslabel.h b/src/bpslabel.h
new file mode 100644
--- /dev/null
+++ b/src/bpslabel.h
@@ -0,0 +1,55 @@
+/*
+  Copyright © 2019 Hasan Yavuz Özderya
+
+  This file is part of serialplot.
+
+  serialplot is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  serialplot is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with serialplot.  If not, see .
+*/
+
+#ifndef BPSLABEL_H
+#define BPSLABEL_H
+
+#include 
+#include 
+
+#include "portcontrol.h"
+#include "dataformatpanel.h"
+
+/**
+ * Displays bits per second read from device.
+ *
+ * Displays a warning if maximum bit rate is reached.
+ */
+class BPSLabel : public QLabel
+{
+    Q_OBJECT
+
+public:
+    explicit BPSLabel(PortControl* portControl,
+                      DataFormatPanel* dataFormatPanel,
+                      QWidget *parent = 0);
+
+private:
+    PortControl* _portControl;
+    DataFormatPanel* _dataFormatPanel;
+    QTimer bpsTimer;
+
+    uint64_t prevBytesRead;
+
+private slots:
+    void onBpsTimeout();
+    void onPortToggled(bool open);
+};
+
+#endif // BPSLABEL_H
diff --git a/src/dataformatpanel.cpp b/src/dataformatpanel.cpp
--- a/src/dataformatpanel.cpp
+++ b/src/dataformatpanel.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -39,6 +39,7 @@ DataFormatPanel::DataFormatPanel(QSerial
     serialPort = port;
     paused = false;
     readerBeforeDemo = nullptr;
+    _bytesRead = 0;
 
     // initalize default reader
     currentReader = &bsReader;
@@ -130,6 +131,12 @@ void DataFormatPanel::selectReader(Abstr
     emit sourceChanged(currentReader);
 }
 
+uint64_t DataFormatPanel::bytesRead()
+{
+    _bytesRead += currentReader->getBytesRead();
+    return _bytesRead;
+}
+
 void DataFormatPanel::saveSettings(QSettings* settings)
 {
     settings->beginGroup(SettingGroup_DataFormat);
diff --git a/src/dataformatpanel.h b/src/dataformatpanel.h
--- a/src/dataformatpanel.h
+++ b/src/dataformatpanel.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -20,9 +20,9 @@
 #ifndef DATAFORMATPANEL_H
 #define DATAFORMATPANEL_H
 
+#include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -50,6 +50,8 @@ public:
     unsigned numChannels() const;
     /// Returns active source (reader)
     Source* activeSource();
+    /// Returns total number of bytes read
+    uint64_t bytesRead();
     /// Stores data format panel settings into a `QSettings`
     void saveSettings(QSettings* settings);
     /// Loads data format panel settings from a `QSettings`.
@@ -77,6 +79,7 @@ private:
     void selectReader(AbstractReader* reader);
 
     bool paused;
+    uint64_t _bytesRead;
 
     DemoReader demoReader;
     AbstractReader* readerBeforeDemo;
diff --git a/src/datatextview.cpp b/src/datatextview.cpp
new file mode 100644
--- /dev/null
+++ b/src/datatextview.cpp
@@ -0,0 +1,110 @@
+/*
+  Copyright © 2019 Hasan Yavuz Özderya
+
+  This file is part of serialplot.
+
+  serialplot is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  serialplot is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with serialplot.  If not, see .
+*/
+
+#include "datatextview.h"
+#include "ui_datatextview.h"
+
+#include "setting_defines.h"
+#include "utils.h"
+
+class DataTextViewSink : public Sink
+{
+public:
+    DataTextViewSink(DataTextView* textView)
+    {
+        _textView = textView;
+    }
+
+protected:
+    virtual void feedIn(const SamplePack& data) override
+    {
+        _textView->addData(data);
+    };
+
+private:
+    DataTextView* _textView;
+};
+
+DataTextView::DataTextView(Stream* stream, QWidget *parent) :
+    QWidget(parent),
+    ui(new Ui::DataTextView)
+{
+    _stream = stream;
+    ui->setupUi(this);
+    sink = new DataTextViewSink(this);
+
+    connect(ui->cbEnable, &QCheckBox::toggled, [this](bool checked)
+            {
+                if (checked)
+                {
+                    _stream->connectFollower(sink);
+                }
+                else
+                {
+                    _stream->disconnectFollower(sink);
+                }
+            });
+
+    ui->textView->setMaximumBlockCount(ui->spNumLines->value());
+    connect(ui->spNumLines, SELECT::OVERLOAD_OF(&QSpinBox::valueChanged),
+            [this](int value)
+            {
+                ui->textView->setMaximumBlockCount(value);
+            });
+
+    connect(ui->pbClear, &QPushButton::clicked, ui->textView, &QPlainTextEdit::clear);
+}
+
+DataTextView::~DataTextView()
+{
+    delete sink;
+    delete ui;
+}
+
+void DataTextView::addData(const SamplePack& data)
+{
+    for (unsigned int i = 0; i < data.numSamples(); i++)
+    {
+        QString str;
+        for (unsigned ci = 0; ci < data.numChannels(); ci++)
+        {
+            str += QString::number(data.data(ci)[i], 'f', ui->spDecimals->value());
+            if (ci != data.numChannels()-1) str += " ";
+        }
+        ui->textView->appendPlainText(str);
+    }
+}
+
+void DataTextView::saveSettings(QSettings* settings)
+{
+    settings->beginGroup(SettingGroup_Plot);
+    settings->setValue(SG_TextView_NumLines, ui->spNumLines->value());
+    settings->setValue(SG_TextView_Decimals, ui->spDecimals->value());
+    settings->endGroup();
+}
+
+void DataTextView::loadSettings(QSettings* settings)
+{
+    settings->beginGroup(SettingGroup_Plot);
+    ui->spNumLines->setValue(
+        settings->value(SG_TextView_NumLines, ui->spNumLines->value()).toInt());
+    ui->spDecimals->setValue(
+        settings->value(SG_TextView_Decimals, ui->spDecimals->value()).toInt());
+    settings->endGroup();
+}
diff --git a/src/datatextview.h b/src/datatextview.h
new file mode 100644
--- /dev/null
+++ b/src/datatextview.h
@@ -0,0 +1,57 @@
+/*
+  Copyright © 2019 Hasan Yavuz Özderya
+
+  This file is part of serialplot.
+
+  serialplot is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  serialplot is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with serialplot.  If not, see .
+*/
+
+#ifndef DATATEXTVIEW_H
+#define DATATEXTVIEW_H
+
+#include 
+
+#include "stream.h"
+
+namespace Ui {
+class DataTextView;
+}
+
+class DataTextViewSink;
+
+class DataTextView : public QWidget
+{
+    Q_OBJECT
+
+public:
+    explicit DataTextView(Stream* stream, QWidget *parent = 0);
+    ~DataTextView();
+
+    /// Stores settings into a `QSettings`
+    void saveSettings(QSettings* settings);
+    /// Loads settings from a `QSettings`.
+    void loadSettings(QSettings* settings);
+
+protected:
+    void addData(const SamplePack& data);
+
+    friend DataTextViewSink;
+
+private:
+    Ui::DataTextView *ui;
+    DataTextViewSink* sink;
+    Stream* _stream;
+};
+
+#endif // DATATEXTVIEW_H
diff --git a/src/datatextview.ui b/src/datatextview.ui
new file mode 100644
--- /dev/null
+++ b/src/datatextview.ui
@@ -0,0 +1,99 @@
+
+
+ DataTextView
+ 
+  
+   
+    0
+    0
+    451
+    212
+   
+  
+  
+   Form
+  
+  
+   - 
+    
+     
- 
+      
+       
+        Enable display of plotted data as text.
+       
+       
+        Enable
+       
+      
+     
 
+     - 
+      
+       
+        Num. Lines:
+       
+      
+     
 
+     - 
+      
+       
+        1
+       
+       
+        10000
+       
+       
+        1000
+       
+      
+     
 
+     - 
+      
+       
+        Decimals:
+       
+      
+     
 
+     - 
+      
+       
+        9
+       
+       
+        6
+       
+      
+     
 
+     - 
+      
+       
+        Qt::Vertical
+       
+       
+        
+         20
+         1
+        
+       
+      
+     
 
+     - 
+      
+       
+        Clear
+       
+      
+     
 
+    
+    
+   - 
+    
+     
+      true
+     
+    
+   
 
+  
+ 
+ 
+ 
+
diff --git a/src/defines.h b/src/defines.h
--- a/src/defines.h
+++ b/src/defines.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2016 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -17,4 +17,12 @@
   along with serialplot.  If not, see .
 */
 
-const char* BUG_REPORT_URL = "https://bitbucket.org/hyOzd/serialplot/issues/new";
+#ifndef DEFINES_H
+#define DEFINES_H
+
+const char BUG_REPORT_URL[] = "https://bitbucket.org/hyOzd/serialplot/issues/new";
+
+/// Maximum number of channels that can be set by user
+const unsigned MAX_NUM_CHANNELS = 64;
+
+#endif  // DEFINES_H
diff --git a/src/demoreader.cpp b/src/demoreader.cpp
--- a/src/demoreader.cpp
+++ b/src/demoreader.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -53,8 +53,9 @@ void DemoReader::enable(bool enabled)
     else
     {
         timer.stop();
-        disconnectSinks();
     }
+
+    AbstractReader::enable(enabled);
 }
 
 unsigned DemoReader::numChannels() const
@@ -91,7 +92,8 @@ void DemoReader::onNumChannelsChanged(un
     updateNumChannels();
 }
 
-void DemoReader::onDataReady()
+unsigned DemoReader::readData()
 {
     // intentionally empty, required by AbstractReader
+    return 0;
 }
diff --git a/src/demoreader.h b/src/demoreader.h
--- a/src/demoreader.h
+++ b/src/demoreader.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -55,10 +55,11 @@ private:
     QTimer timer;
     int count;
 
+    unsigned readData() override;
+
 private slots:
     void demoTimerTimeout();
     void onNumChannelsChanged(unsigned value);
-    void onDataReady() override;
 };
 
 #endif // DEMOREADER_H
diff --git a/src/demoreadersettings.cpp b/src/demoreadersettings.cpp
--- a/src/demoreadersettings.cpp
+++ b/src/demoreadersettings.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -21,6 +21,7 @@
 #include "ui_demoreadersettings.h"
 
 #include "utils.h"
+#include "defines.h"
 
 DemoReaderSettings::DemoReaderSettings(QWidget *parent) :
     QWidget(parent),
@@ -28,6 +29,8 @@ DemoReaderSettings::DemoReaderSettings(Q
 {
     ui->setupUi(this);
 
+    ui->spNumChannels->setMaximum(MAX_NUM_CHANNELS);
+
     connect(ui->spNumChannels, SELECT::OVERLOAD_OF(&QSpinBox::valueChanged),
             [this](int value)
             {
diff --git a/src/floatswap.h b/src/floatswap.h
--- a/src/floatswap.h
+++ b/src/floatswap.h
@@ -1,5 +1,7 @@
 #include 
 
+#if (QT_VERSION < QT_VERSION_CHECK(5, 12, 0))
+
 template <> inline float qbswap(float source)
 {
     float result;
@@ -11,3 +13,5 @@ template <> inline float qbswap(f
     t[3] = s[0];
     return result;
 }
+
+#endif
diff --git a/src/framebuffer.h b/src/framebuffer.h
--- a/src/framebuffer.h
+++ b/src/framebuffer.h
@@ -61,4 +61,24 @@ class WFrameBuffer : public ResizableBuf
     virtual void clear() = 0;
 };
 
+/**
+ * Abstract base class for X buffers.
+ *
+ * These buffers only contain increasing or equal (to previous) values.
+ */
+class XFrameBuffer : public ResizableBuffer
+{
+public:
+    enum Index {OUT_OF_RANGE = -1};
+
+    /**
+     * Finds index for given value.
+     *
+     * If given value is bigger than max or smaller than minimum
+     * returns `OUT_OF_RANGE`. If it's in between values, smaller
+     * index is returned (not closer one).
+     */
+    virtual int findIndex(double value) const = 0;
+};
+
 #endif // FRAMEBUFFER_H
diff --git a/src/framebufferseries.cpp b/src/framebufferseries.cpp
--- a/src/framebufferseries.cpp
+++ b/src/framebufferseries.cpp
@@ -20,75 +20,64 @@
 #include 
 #include "framebufferseries.h"
 
-FrameBufferSeries::FrameBufferSeries(const FrameBuffer* buffer)
+FrameBufferSeries::FrameBufferSeries(const XFrameBuffer* x, const FrameBuffer* y)
 {
-    xAsIndex = true;
-    _xmin = 0;
-    _xmax = 1;
-    _buffer = buffer;
+    _x = x;
+    _y = y;
+
     int_index_start = 0;
-    int_index_end = _buffer->size();
+    int_index_end = _y->size();
 }
 
-void FrameBufferSeries::setXAxis(bool asIndex, double xmin, double xmax)
+void FrameBufferSeries::setX(const XFrameBuffer* x)
 {
-    xAsIndex = asIndex;
-    _xmin = xmin;
-    _xmax = xmax;
+    _x = x;
 }
 
 size_t FrameBufferSeries::size() const
 {
-    return int_index_end - int_index_start;
+    return int_index_end - int_index_start + 1;
 }
 
 QPointF FrameBufferSeries::sample(size_t i) const
 {
     i += int_index_start;
-    if (xAsIndex)
-    {
-        return QPointF(i, _buffer->sample(i));
-    }
-    else
-    {
-        return QPointF(i * (_xmax - _xmin) / _buffer->size() + _xmin, _buffer->sample(i));
-    }
+    return QPointF(_x->sample(i), _y->sample(i));
 }
 
 QRectF FrameBufferSeries::boundingRect() const
 {
     QRectF rect;
-    auto yLim = _buffer->limits();
+    auto yLim = _y->limits();
+    auto xLim = _x->limits();
     rect.setBottom(yLim.start);
     rect.setTop(yLim.end);
-    if (xAsIndex)
-    {
-        rect.setLeft(0);
-        rect.setRight(size());
-    }
-    else
-    {
-        rect.setLeft(_xmin);
-        rect.setRight(_xmax);
-    }
+    rect.setLeft(xLim.start);
+    rect.setRight(xLim.end);
+
     return rect.normalized();
 }
 
 void FrameBufferSeries::setRectOfInterest(const QRectF& rect)
 {
-    if (xAsIndex)
+    int_index_start = _x->findIndex(rect.left());
+    int_index_end = _x->findIndex(rect.right());
+
+    if (int_index_start == XFrameBuffer::OUT_OF_RANGE)
     {
-        int_index_start = floor(rect.left())-1;
-        int_index_end = ceil(rect.right())+1;
+        int_index_start = 0;
     }
-    else
+    else if (int_index_start > 0)
     {
-        double xsize = _xmax - _xmin;
-        size_t bsize = _buffer->size();
-        int_index_start =  floor(bsize * (rect.left()-_xmin) / xsize)-1;
-        int_index_end = ceil(bsize * (rect.right()-_xmin) / xsize)+1;
+        int_index_start -= 1;
     }
 
-    int_index_start = std::max(int_index_start, 0);
-    int_index_end = std::min((int) _buffer->size(), int_index_end);
+    if (int_index_end == XFrameBuffer::OUT_OF_RANGE)
+    {
+        int_index_end = _x->size()-1;
+    }
+    else if (int_index_end < (int)_x->size()-1)
+    {
+        int_index_end += 1;
+    }
 }
diff --git a/src/framebufferseries.h b/src/framebufferseries.h
--- a/src/framebufferseries.h
+++ b/src/framebufferseries.h
@@ -35,10 +35,9 @@
 class FrameBufferSeries : public QwtSeriesData
 {
 public:
-    FrameBufferSeries(const FrameBuffer* buffer);
+    FrameBufferSeries(const XFrameBuffer* x, const FrameBuffer* y);
 
-    /// Behavior of X axis
-    void setXAxis(bool asIndex, double xmin, double xmax);
+    void setX(const XFrameBuffer* x);
 
     // QwtSeriesData implementations
     size_t size() const;
@@ -47,10 +46,8 @@ public:
     void setRectOfInterest(const QRectF& rect);
 
 private:
-    const FrameBuffer* _buffer;
-    bool xAsIndex;
-    double _xmin;
-    double _xmax;
+    const XFrameBuffer* _x;
+    const FrameBuffer* _y;
 
     int int_index_start; ///< starting index of "rectangle of interest"
     int int_index_end;   ///< ending index of "rectangle of interest"
diff --git a/src/framedreader.cpp b/src/framedreader.cpp
--- a/src/framedreader.cpp
+++ b/src/framedreader.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2020 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -159,6 +159,7 @@ void FramedReader::onNumOfChannelsChange
     _numChannels = value;
     checkSettings();
     reset();
+    updateNumChannels();
     emit numOfChannelsChanged(value);
 }
 
@@ -184,9 +185,11 @@ void FramedReader::onFrameSizeChanged(un
     reset();
 }
 
-void FramedReader::onDataReady()
+unsigned FramedReader::readData()
 {
-    if (settingsInvalid) return;
+    unsigned numBytesRead = 0;
+
+    if (settingsInvalid) return numBytesRead;
 
     // loop until we run out of bytes or more bytes is required
     unsigned bytesAvailable;
@@ -196,6 +199,7 @@ void FramedReader::onDataReady()
         {
             char c;
             _device->getChar(&c);
+            numBytesRead++;
             if (c == syncWord[sync_i]) // correct sync byte?
             {
                 sync_i++;
@@ -213,6 +217,7 @@ void FramedReader::onDataReady()
         {
             frameSize = 0;
             _device->getChar((char*) &frameSize);
+            numBytesRead++;
 
             if (frameSize == 0) // check size
             {
@@ -242,10 +247,13 @@ void FramedReader::onDataReady()
             else // read data bytes and checksum
             {
                 readFrameDataAndCheck();
+                numBytesRead += checksumEnabled ? frameSize+1 : frameSize;
                 reset();
             }
         }
     }
+
+    return numBytesRead;
 }
 
 void FramedReader::reset()
@@ -263,7 +271,7 @@ void FramedReader::readFrameDataAndCheck
     // if paused just read and waste data
     if (paused)
     {
-        _device->read((checksumEnabled ? frameSize+1 : frameSize));
+        _device->read(checksumEnabled ? frameSize+1 : frameSize);
         return;
     }
 
diff --git a/src/framedreader.h b/src/framedreader.h
--- a/src/framedreader.h
+++ b/src/framedreader.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -79,8 +79,9 @@ private:
     /// @note should be called only if there are enough bytes on device
     void readFrameDataAndCheck();
 
+    unsigned readData() override;
+
 private slots:
-    void onDataReady() override;
 
     void onNumberFormatChanged(NumberFormat numberFormat);
     void onNumOfChannelsChanged(unsigned value);
diff --git a/src/framedreadersettings.cpp b/src/framedreadersettings.cpp
--- a/src/framedreadersettings.cpp
+++ b/src/framedreadersettings.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2016 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -20,6 +20,7 @@
 #include 
 
 #include "utils.h"
+#include "defines.h"
 #include "setting_defines.h"
 #include "framedreadersettings.h"
 #include "ui_framedreadersettings.h"
@@ -32,6 +33,7 @@ FramedReaderSettings::FramedReaderSettin
 
     ui->leSyncWord->setMode(false); // hex mode
     ui->leSyncWord->setText("AA BB");
+    ui->spNumOfChannels->setMaximum(MAX_NUM_CHANNELS);
 
     connect(ui->cbChecksum, &QCheckBox::toggled,
             [this](bool enabled)
diff --git a/src/indexbuffer.cpp b/src/indexbuffer.cpp
--- a/src/indexbuffer.cpp
+++ b/src/indexbuffer.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -47,3 +47,15 @@ Range IndexBuffer::limits() const
 {
     return Range{0, _size-1.};
 }
+
+int IndexBuffer::findIndex(double value) const
+{
+    if (value < 0 || value > size() - 1)
+    {
+        return OUT_OF_RANGE;
+    }
+    else
+    {
+        return value;
+    }
+}
diff --git a/src/indexbuffer.h b/src/indexbuffer.h
--- a/src/indexbuffer.h
+++ b/src/indexbuffer.h
@@ -26,15 +26,16 @@
 /// sample value.
 ///
 /// @note This buffer isn't for storing data.
-class IndexBuffer : public ResizableBuffer
+class IndexBuffer : public XFrameBuffer
 {
 public:
     IndexBuffer(unsigned n);
 
-    unsigned size() const;
-    double sample(unsigned i) const;
-    Range limits() const;
-    void resize(unsigned n);
+    unsigned size() const override;
+    double sample(unsigned i) const override;
+    Range limits() const override;
+    void resize(unsigned n) override;
+    int findIndex(double value) const override;
 
 private:
     unsigned _size;
diff --git a/src/ledwidget.cpp b/src/ledwidget.cpp
new file mode 100644
--- /dev/null
+++ b/src/ledwidget.cpp
@@ -0,0 +1,129 @@
+/*
+  Copyright © 2018 Hasan Yavuz Özderya
+
+  This file is part of serialplot.
+
+  ledwidget is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  ledwidget is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with ledwidget.  If not, see .
+*/
+
+#include "ledwidget.h"
+#include 
+#include 
+#include 
+
+LedWidget::LedWidget(QWidget* parent) : QWidget(parent),
+    m_color(107,223,51),
+    m_on(true)
+{
+    resize(20,20);
+}
+
+QSize LedWidget::sizeHint() const
+{
+    return QSize(20,20);
+}
+
+QSize LedWidget::minimumSizeHint() const
+{
+    return QSize(10, 10);
+}
+
+void LedWidget::setColor(QColor ledColor)
+{
+    if (m_color == ledColor) return;
+    m_color = ledColor;
+    update();
+    emit colorChanged(m_color);
+}
+
+bool LedWidget::isOn()
+{
+    return m_on;
+}
+
+void LedWidget::setOn(bool on)
+{
+    if (on == m_on) return;
+    m_on = on;
+    update();
+    emit onChanged(on);
+}
+
+void LedWidget::turnOn()
+{
+    setOn(true);
+}
+
+void LedWidget::turnOff()
+{
+    setOn(false);
+}
+
+void LedWidget::toggle()
+{
+    setOn(!m_on);
+}
+
+void LedWidget::paintEvent(QPaintEvent* event)
+{
+    Q_UNUSED(event)
+
+    const qreal r = std::min(width(), height()) / 2; // maximum radius including glow
+    const qreal glowOffset = std::max(2., r/5.);
+    const qreal borderOffset = std::max(1., r/10.);
+    const qreal shineOffset = std::max(1., r/20.);
+    const QPointF center(width()/2, height()/2);
+
+    const qreal gr = r;
+    const qreal br = gr - glowOffset;   // border shape radius
+    const qreal ir = br - borderOffset; // inner fill radius
+    const qreal sr = ir - shineOffset;
+
+    QColor borderColor(130,130,130);
+    QColor shineColor(255, 255, 255, m_on ? 200 : 50);
+    QColor fillColor(m_color);
+
+    QPainter painter(this);
+    painter.setRenderHints(QPainter::Antialiasing);
+
+    // draw border
+    painter.setPen(Qt::NoPen);
+    painter.setBrush(borderColor);
+    painter.drawEllipse(center, br, br);
+
+    // draw infill
+    if (!m_on) fillColor = fillColor.darker();
+    painter.setBrush(fillColor);
+    painter.drawEllipse(center, ir, ir);
+
+    // draw glow
+    if (m_on)
+    {
+        QColor glowColor(m_color);
+        glowColor.setAlphaF(0.5);
+        QRadialGradient glowGradient(center, gr, center);
+        glowGradient.setColorAt(0, glowColor);
+        glowGradient.setColorAt((r-glowOffset)/r, glowColor);
+        glowGradient.setColorAt(1, Qt::transparent);
+        painter.setBrush(glowGradient);
+        painter.drawEllipse(center, gr, gr);
+    }
+
+    // draw shine
+    QRadialGradient shineGradient(center, sr, center-QPoint(sr/2,sr/2));
+    shineGradient.setColorAt(0, shineColor);
+    shineGradient.setColorAt(1, Qt::transparent);
+    painter.setBrush(shineGradient);
+    painter.drawEllipse(center, sr, sr);
+}
diff --git a/src/ledwidget.h b/src/ledwidget.h
new file mode 100644
--- /dev/null
+++ b/src/ledwidget.h
@@ -0,0 +1,61 @@
+/*
+  Copyright © 2018 Hasan Yavuz Özderya
+
+  This file is part of serialplot.
+
+  ledwidget is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  ledwidget is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with ledwidget.  If not, see .
+*/
+
+#ifndef LEDWIDGET_H
+#define LEDWIDGET_H
+
+#include 
+#include 
+#include 
+
+class LedWidget : public QWidget
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QColor color MEMBER m_color WRITE setColor NOTIFY colorChanged)
+    Q_PROPERTY(bool on READ isOn WRITE setOn NOTIFY onChanged)
+
+public:
+    explicit LedWidget(QWidget *parent = 0);
+
+    void setColor(QColor ledColor);
+    bool isOn();
+
+    QSize sizeHint() const Q_DECL_OVERRIDE;
+    QSize minimumSizeHint() const Q_DECL_OVERRIDE;
+
+signals:
+    void colorChanged(QColor ledColor);
+    void onChanged(bool on);
+
+public slots:
+    void setOn(bool on);
+    void turnOn();
+    void turnOff();
+    void toggle();
+
+protected:
+    void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE;
+
+private:
+    QColor m_color;
+    bool m_on;
+};
+
+#endif // LEDWIDGET_H
diff --git a/src/linindexbuffer.cpp b/src/linindexbuffer.cpp
--- a/src/linindexbuffer.cpp
+++ b/src/linindexbuffer.cpp
@@ -1,5 +1,5 @@
  /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -18,12 +18,14 @@
 */
 
 #include 
+#include 
 
 #include "linindexbuffer.h"
 
 LinIndexBuffer::LinIndexBuffer(unsigned n, Range lim)
 {
-    Q_ASSERT(n > 0);
+    // Note that calculation of _step would cause divide by 0
+    Q_ASSERT(n > 1);
 
     _size = n;
     setLimits(lim);
@@ -50,6 +52,18 @@ void LinIndexBuffer::resize(unsigned n)
     setLimits(_limits);         // called to update `_step`
 }
 
+int LinIndexBuffer::findIndex(double value) const
+{
+    if (value < _limits.start || value > _limits.end)
+    {
+        return OUT_OF_RANGE;
+    }
+
+    int r = (value - _limits.start) / _step;
+    // Note: we are limiting return value because of floating point in-accuracies
+    return std::min(std::max(r, 0), (_size-1));
+}
+
 void LinIndexBuffer::setLimits(Range lim)
 {
     _limits = lim;
diff --git a/src/linindexbuffer.h b/src/linindexbuffer.h
--- a/src/linindexbuffer.h
+++ b/src/linindexbuffer.h
@@ -26,17 +26,19 @@
 /// intermediate values are calculated linearly.
 ///
 /// @note This buffer isn't for storing data.
-class LinIndexBuffer : public ResizableBuffer
+class LinIndexBuffer : public XFrameBuffer
 {
 public:
     LinIndexBuffer(unsigned n, Range lim);
     LinIndexBuffer(unsigned n, double min, double max) :
         LinIndexBuffer(n, {min, max}) {};
 
-    unsigned size() const;
-    double sample(unsigned i) const;
-    Range limits() const;
-    void resize(unsigned n);
+    unsigned size() const override;
+    double sample(unsigned i) const override;
+    Range limits() const override;
+    void resize(unsigned n) override;
+    int findIndex(double value) const override;
+
     /// Sets minimum and maximum sample values of the buffer.
     void setLimits(Range lim);
 
diff --git a/src/main.cpp b/src/main.cpp
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -19,28 +19,64 @@
 
 #include 
 #include 
+#include 
 
 #include "mainwindow.h"
 #include "tooltipfilter.h"
 #include "version.h"
 
-MainWindow* pMainWindow;
+MainWindow* pMainWindow = nullptr;
 
 void messageHandler(QtMsgType type, const QMessageLogContext &context,
                     const QString &msg)
 {
-    // TODO: don't call MainWindow::messageHandler if window is destroyed
-    pMainWindow->messageHandler(type, context, msg);
+    QString logString;
+
+    switch (type)
+    {
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
+        case QtInfoMsg:
+            logString = "[Info] " + msg;
+            break;
+#endif
+        case QtDebugMsg:
+            logString = "[Debug] " + msg;
+            break;
+        case QtWarningMsg:
+            logString = "[Warning] " + msg;
+            break;
+        case QtCriticalMsg:
+            logString = "[Error] " + msg;
+            break;
+        case QtFatalMsg:
+            logString = "[Fatal] " + msg;
+            break;
+    }
+
+    std::cerr << logString.toStdString() << std::endl;
+
+    if (pMainWindow != nullptr)
+    {
+        // TODO: don't call MainWindow::messageHandler if window is destroyed
+        pMainWindow->messageHandler(type, logString, msg);
+    }
+
+    if (type == QtFatalMsg)
+    {
+        __builtin_trap();
+    }
 }
 
 int main(int argc, char *argv[])
 {
     QApplication a(argc, argv);
+    QApplication::setApplicationName(PROGRAM_NAME);
+    QApplication::setApplicationVersion(VERSION_STRING);
+
+    qInstallMessageHandler(messageHandler);
     MainWindow w;
     pMainWindow = &w;
 
-    qInstallMessageHandler(messageHandler);
-
     ToolTipFilter ttf;
     a.installEventFilter(&ttf);
 
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -29,10 +29,13 @@
 #include 
 #include 
 #include 
+#include 
+#include 
 #include 
 #include 
 #include 
 #include 
+#include 
 
 #include 
 #include 
@@ -55,7 +58,8 @@ const QMap panelSettingMap
         {2, "Plot"},
         {3, "Commands"},
         {4, "Record"},
-        {5, "Log"}
+        {5, "TextView"},
+        {6, "Log"}
     });
 
 MainWindow::MainWindow(QWidget *parent) :
@@ -68,7 +72,9 @@ MainWindow::MainWindow(QWidget *parent) 
     commandPanel(&serialPort),
     dataFormatPanel(&serialPort),
     recordPanel(&stream),
-    updateCheckDialog(this)
+    textView(&stream),
+    updateCheckDialog(this),
+    bpsLabel(&portControl, &dataFormatPanel, this)
 {
     ui->setupUi(this);
 
@@ -79,6 +85,7 @@ MainWindow::MainWindow(QWidget *parent) 
     ui->tabWidget->insertTab(2, &plotControlPanel, "Plot");
     ui->tabWidget->insertTab(3, &commandPanel, "Commands");
     ui->tabWidget->insertTab(4, &recordPanel, "Record");
+    ui->tabWidget->insertTab(5, &textView, "Text View");
     ui->tabWidget->setCurrentIndex(0);
     auto tbPortControl = portControl.toolBar();
     addToolBar(tbPortControl);
@@ -169,6 +176,9 @@ MainWindow::MainWindow(QWidget *parent) 
             plotMan, &PlotManager::setYAxis);
 
     connect(&plotControlPanel, &PlotControlPanel::xScaleChanged,
+            &stream, &Stream::setXAxis);
+
+    connect(&plotControlPanel, &PlotControlPanel::xScaleChanged,
             plotMan, &PlotManager::setXAxis);
 
     connect(&plotControlPanel, &PlotControlPanel::plotWidthChanged,
@@ -215,6 +225,9 @@ MainWindow::MainWindow(QWidget *parent) 
     plotControlPanel.setChannelInfoModel(stream.infoModel());
 
     // init scales
+    stream.setXAxis(plotControlPanel.xAxisAsIndex(),
+                    plotControlPanel.xMin(), plotControlPanel.xMax());
+
     plotMan->setYAxis(plotControlPanel.autoScale(),
                       plotControlPanel.yMin(), plotControlPanel.yMax());
     plotMan->setXAxis(plotControlPanel.xAxisAsIndex(),
@@ -222,13 +235,21 @@ MainWindow::MainWindow(QWidget *parent) 
     plotMan->setNumOfSamples(numOfSamples);
     plotMan->setPlotWidth(plotControlPanel.plotWidth());
 
+    // init bps (bits per second) counter
+    ui->statusBar->addPermanentWidget(&bpsLabel);
+
     // Init sps (sample per second) counter
     spsLabel.setText("0sps");
-    spsLabel.setToolTip("samples per second (per channel)");
+    spsLabel.setToolTip(tr("samples per second (per channel)"));
     ui->statusBar->addPermanentWidget(&spsLabel);
     connect(&sampleCounter, &SampleCounter::spsChanged,
             this, &MainWindow::onSpsChanged);
 
+    bpsLabel.setMinimumWidth(70);
+    bpsLabel.setAlignment(Qt::AlignRight);
+    spsLabel.setMinimumWidth(70);
+    spsLabel.setAlignment(Qt::AlignRight);
+
     // init demo
     QObject::connect(ui->actionDemoMode, &QAction::toggled,
                      this, &MainWindow::enableDemo);
@@ -242,9 +263,11 @@ MainWindow::MainWindow(QWidget *parent) 
     onSourceChanged(dataFormatPanel.activeSource());
 
     // load default settings
-    QSettings settings("serialplot", "serialplot");
+    QSettings settings(PROGRAM_NAME, PROGRAM_NAME);
     loadAllSettings(&settings);
 
+    handleCommandLineOptions(*QApplication::instance());
+
     // ensure command panel has 1 command if none loaded
     if (!commandPanel.numOfCommands())
     {
@@ -291,7 +314,7 @@ void MainWindow::closeEvent(QCloseEvent 
     }
 
     // save settings
-    QSettings settings("serialplot", "serialplot");
+    QSettings settings(PROGRAM_NAME, PROGRAM_NAME);
     saveAllSettings(&settings);
     settings.sync();
 
@@ -346,6 +369,11 @@ void MainWindow::onPortToggled(bool open
     // make sure demo mode is disabled
     if (open && isDemoRunning()) enableDemo(false);
     ui->actionDemoMode->setEnabled(!open);
+
+    if (!open)
+    {
+        spsLabel.setText("0sps");
+    }
 }
 
 void MainWindow::onSourceChanged(Source* source)
@@ -465,44 +493,16 @@ PlotViewSettings MainWindow::viewSetting
 }
 
 void MainWindow::messageHandler(QtMsgType type,
-                                const QMessageLogContext &context,
+                                const QString &logString,
                                 const QString &msg)
 {
-    QString logString;
-
-    switch (type)
-    {
-#if (QT_VERSION >= QT_VERSION_CHECK(5, 5, 0))
-        case QtInfoMsg:
-            logString = "[Info] " + msg;
-            break;
-#endif
-        case QtDebugMsg:
-            logString = "[Debug] " + msg;
-            break;
-        case QtWarningMsg:
-            logString = "[Warning] " + msg;
-            break;
-        case QtCriticalMsg:
-            logString = "[Error] " + msg;
-            break;
-        case QtFatalMsg:
-            logString = "[Fatal] " + msg;
-            break;
-    }
-
-    if (ui != NULL) ui->ptLog->appendPlainText(logString);
-    std::cerr << logString.toStdString() << std::endl;
+    if (ui != NULL)
+        ui->ptLog->appendPlainText(logString);
 
     if (type != QtDebugMsg && ui != NULL)
     {
         ui->statusBar->showMessage(msg, 5000);
     }
-
-    if (type == QtFatalMsg)
-    {
-        __builtin_trap();
-    }
 }
 
 void MainWindow::saveAllSettings(QSettings* settings)
@@ -515,6 +515,7 @@ void MainWindow::saveAllSettings(QSettin
     plotMenu.saveSettings(settings);
     commandPanel.saveSettings(settings);
     recordPanel.saveSettings(settings);
+    textView.saveSettings(settings);
     updateCheckDialog.saveSettings(settings);
 }
 
@@ -528,6 +529,7 @@ void MainWindow::loadAllSettings(QSettin
     plotMenu.loadSettings(settings);
     commandPanel.loadSettings(settings);
     recordPanel.loadSettings(settings);
+    textView.loadSettings(settings);
     updateCheckDialog.loadSettings(settings);
 }
 
@@ -605,3 +607,56 @@ void MainWindow::onLoadSettings()
         loadAllSettings(&settings);
     }
 }
+
+void MainWindow::handleCommandLineOptions(const QCoreApplication &app)
+{
+    QCommandLineParser parser;
+    parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsCompactedShortOptions);
+    parser.setApplicationDescription("Small and simple software for plotting data from serial port in realtime.");
+    parser.addHelpOption();
+    parser.addVersionOption();
+
+    QCommandLineOption configOpt({"c", "config"}, "Load configuration from file.", "filename");
+    QCommandLineOption portOpt({"p", "port"}, "Set port name.", "port name");
+    QCommandLineOption baudrateOpt({"b" ,"baudrate"}, "Set port baud rate.", "baud rate");
+    QCommandLineOption openPortOpt({"o", "open"}, "Open serial port.");
+
+    parser.addOption(configOpt);
+    parser.addOption(portOpt);
+    parser.addOption(baudrateOpt);
+    parser.addOption(openPortOpt);
+
+    parser.process(app);
+
+    if (parser.isSet(configOpt))
+    {
+        QString fileName = parser.value(configOpt);
+        QFileInfo fileInfo(fileName);
+
+        if (fileInfo.exists() && fileInfo.isFile())
+        {
+            QSettings settings(fileName, QSettings::IniFormat);
+            loadAllSettings(&settings);
+        }
+        else
+        {
+            qCritical() << "Configuration file not exist. Closing application.";
+            std::exit(1);
+        }
+    }
+
+    if (parser.isSet(portOpt))
+    {
+        portControl.selectPort(parser.value(portOpt));
+    }
+
+    if (parser.isSet(baudrateOpt))
+    {
+        portControl.selectBaudrate(parser.value(baudrateOpt));
+    }
+
+    if (parser.isSet(openPortOpt))
+    {
+        portControl.openPort();
+    }
+}
diff --git a/src/mainwindow.h b/src/mainwindow.h
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -46,6 +46,8 @@
 #include "plotmenu.h"
 #include "updatecheckdialog.h"
 #include "samplecounter.h"
+#include "datatextview.h"
+#include "bpslabel.h"
 
 namespace Ui {
 class MainWindow;
@@ -61,8 +63,7 @@ public:
 
     PlotViewSettings viewSettings() const;
 
-    void messageHandler(QtMsgType type, const QMessageLogContext &context,
-                        const QString &msg);
+    void messageHandler(QtMsgType type, const QString &logString, const QString &msg);
 
 private:
     Ui::MainWindow *ui;
@@ -89,7 +90,11 @@ private:
     RecordPanel recordPanel;
     PlotControlPanel plotControlPanel;
     PlotMenu plotMenu;
+    DataTextView textView;
     UpdateCheckDialog updateCheckDialog;
+    BPSLabel bpsLabel;
+
+    void handleCommandLineOptions(const QCoreApplication &app);
 
     /// Returns true if demo is running
     bool isDemoRunning();
diff --git a/src/plot.cpp b/src/plot.cpp
--- a/src/plot.cpp
+++ b/src/plot.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -91,6 +91,11 @@ Plot::~Plot()
     if (snapshotOverlay != NULL) delete snapshotOverlay;
 }
 
+void Plot::setDispChannels(QVector channels)
+{
+    zoomer.setDispChannels(channels);
+}
+
 void Plot::setYAxis(bool autoScaled, double yAxisMin, double yAxisMax)
 {
     this->isAutoScaled = autoScaled;
diff --git a/src/plot.h b/src/plot.h
--- a/src/plot.h
+++ b/src/plot.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -50,6 +50,9 @@ public:
     Plot(QWidget* parent = 0);
     ~Plot();
 
+    /// Set displayed channels for value tracking (can be null)
+    void setDispChannels(QVector channels);
+
 public slots:
     void showGrid(bool show = true);
     void showMinorGrid(bool show = true);
diff --git a/src/plotcontrolpanel.cpp b/src/plotcontrolpanel.cpp
--- a/src/plotcontrolpanel.cpp
+++ b/src/plotcontrolpanel.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -21,10 +21,10 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 
-#include "color_selector.hpp"
 #include "plotcontrolpanel.h"
 #include "ui_plotcontrolpanel.h"
 #include "setting_defines.h"
@@ -162,9 +162,9 @@ PlotControlPanel::PlotControlPanel(QWidg
                      this, SLOT(onRangeSelected()));
 
     // color selector starts disabled until a channel is selected
-    ui->colorSelector->setColor(QColor(0,0,0,0));
-    ui->colorSelector->setDisplayMode(color_widgets::ColorPreview::AllAlpha);
-    ui->colorSelector->setDisabled(true);
+    ui->pbColorSel->setDisabled(true);
+    setSelectorColor(QColor(0,0,0,0));
+    connect(ui->pbColorSel, &QPushButton::clicked, this, &PlotControlPanel::onColorSelect);
 
     // reset buttons
     resetAct.setToolTip(tr("Reset channel names and colors"));
@@ -241,6 +241,30 @@ bool PlotControlPanel::askNSConfirmation
     return mb.exec() == QMessageBox::Apply;
 }
 
+void PlotControlPanel::setSelectorColor(QColor color)
+{
+    ui->pbColorSel->setStyleSheet(QString("background-color: %1;").arg(color.name()));
+}
+
+void PlotControlPanel::onColorSelect()
+{
+    auto selection = ui->tvChannelInfo->selectionModel()->currentIndex();
+    // no selection
+    if (!selection.isValid()) return;
+
+    // current color
+    auto model = ui->tvChannelInfo->model();
+    QColor color = model->data(selection, Qt::ForegroundRole).value();
+
+    // show dialog
+    color = QColorDialog::getColor(color, this);
+
+    if (color.isValid())        // color is set to invalid if user cancels
+    {
+        ui->tvChannelInfo->model()->setData(selection, color, Qt::ForegroundRole);
+    }
+}
+
 void PlotControlPanel::onAutoScaleChecked(bool checked)
 {
     if (checked)
@@ -373,19 +397,16 @@ void PlotControlPanel::setChannelInfoMod
 
                 if (current.isValid())
                 {
-                    ui->colorSelector->setEnabled(true);
+                    ui->pbColorSel->setEnabled(true);
                     auto model = ui->tvChannelInfo->model();
                     color = model->data(current, Qt::ForegroundRole).value();
                 }
                 else
                 {
-                    ui->colorSelector->setDisabled(true);
+                    ui->pbColorSel->setDisabled(true);
                 }
 
-                // temporarily block signals because `setColor` emits `colorChanged`
-                bool wasBlocked = ui->colorSelector->blockSignals(true);
-                ui->colorSelector->setColor(color);
-                ui->colorSelector->blockSignals(wasBlocked);
+                setSelectorColor(color);
             });
 
     connect(ui->tvChannelInfo->selectionModel(), &QItemSelectionModel::selectionChanged,
@@ -393,22 +414,11 @@ void PlotControlPanel::setChannelInfoMod
             {
                 if (!selected.length())
                 {
-                    ui->colorSelector->setDisabled(true);
-
-                    // temporarily block signals because `setColor` emits `colorChanged`
-                    bool wasBlocked = ui->colorSelector->blockSignals(true);
-                    ui->colorSelector->setColor(QColor(0,0,0,0));
-                    ui->colorSelector->blockSignals(wasBlocked);
+                    ui->pbColorSel->setDisabled(true);
+                    setSelectorColor(QColor(0,0,0,0));
                 }
             });
 
-    connect(ui->colorSelector, &color_widgets::ColorSelector::colorChanged,
-            [this](QColor color)
-            {
-                auto index = ui->tvChannelInfo->selectionModel()->currentIndex();
-                ui->tvChannelInfo->model()->setData(index, color, Qt::ForegroundRole);
-            });
-
     connect(model, &QAbstractItemModel::dataChanged,
             [this](const QModelIndex & topLeft, const QModelIndex & bottomRight, const QVector & roles = QVector ())
             {
@@ -419,11 +429,7 @@ void PlotControlPanel::setChannelInfoMod
 
                 auto mod = ui->tvChannelInfo->model();
                 QColor color = mod->data(current, Qt::ForegroundRole).value();
-
-                // temporarily block signals because `setColor` emits `colorChanged`
-                bool wasBlocked = ui->colorSelector->blockSignals(true);
-                ui->colorSelector->setColor(color);
-                ui->colorSelector->blockSignals(wasBlocked);
+                setSelectorColor(color);
             });
 
     // reset actions
diff --git a/src/plotcontrolpanel.h b/src/plotcontrolpanel.h
--- a/src/plotcontrolpanel.h
+++ b/src/plotcontrolpanel.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -79,6 +79,9 @@ private:
     /// Show a confirmation dialog before setting #samples to a big value
     bool askNSConfirmation(int value);
 
+    /// Set the color displayed by color selector button
+    void setSelectorColor(QColor color);
+
 private slots:
     void onNumOfSamples(int value);
     void onAutoScaleChecked(bool checked);
@@ -87,6 +90,7 @@ private slots:
     void onIndexChecked(bool checked);
     void onXScaleChanged();
     void onPlotWidthChanged();
+    void onColorSelect();
 };
 
 #endif // PLOTCONTROLPANEL_H
diff --git a/src/plotcontrolpanel.ui b/src/plotcontrolpanel.ui
--- a/src/plotcontrolpanel.ui
+++ b/src/plotcontrolpanel.ui
@@ -7,7 +7,7 @@
     0
     0
     704
-    195
+    220
    
   
   
@@ -69,9 +69,9 @@
          QLayout::SetMaximumSize
         
         - 
-         
+         
           
-           
+           
             0
             0
            
@@ -88,6 +88,15 @@
             20
            
           
+          
+           background-color: rgb(70, 141, 255);
+          
+          
+           
+          
+          
+           false
+          
          
         
 
         - 
@@ -411,14 +420,6 @@
    
 
   
  
- 
-  
-   color_widgets::ColorSelector
-   QWidget
-   
-   1
-  
- 
  
  
 
diff --git a/src/plotmanager.cpp b/src/plotmanager.cpp
--- a/src/plotmanager.cpp
+++ b/src/plotmanager.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -19,7 +19,6 @@
 
 #include 
 #include 
-#include 
 #include "qwt_symbol.h"
 
 #include "plot.h"
@@ -31,9 +30,9 @@ PlotManager::PlotManager(QWidget* plotAr
                          const Stream* stream, QObject* parent) :
     QObject(parent)
 {
+    _stream = stream;
     construct(plotArea, menu);
-    _stream = stream;
-    if (_stream == NULL) return;
+    if (_stream == nullptr) return;
 
     // connect to ChannelInfoModel
     infoModel = _stream->infoModel();
@@ -53,15 +52,15 @@ PlotManager::PlotManager(QWidget* plotAr
     // add initial curves if any?
     for (unsigned int i = 0; i < stream->numChannels(); i++)
     {
-        addCurve(stream->channel(i)->name(), stream->channel(i)->yData());
+        addCurve(stream->channel(i)->name(), stream->channel(i)->xData(), stream->channel(i)->yData());
     }
-
 }
 
 PlotManager::PlotManager(QWidget* plotArea, PlotMenu* menu,
                          Snapshot* snapshot, QObject *parent) :
     QObject(parent)
 {
+    _stream = nullptr;
     construct(plotArea, menu);
 
     setNumOfSamples(snapshot->numSamples());
@@ -70,11 +69,14 @@ PlotManager::PlotManager(QWidget* plotAr
 
     for (unsigned ci = 0; ci < snapshot->numChannels(); ci++)
     {
-        addCurve(snapshot->channelName(ci), snapshot->yData[ci]);
+        addCurve(snapshot->channelName(ci), snapshot->xData[ci], snapshot->yData[ci]);
     }
 
     connect(infoModel, &QAbstractItemModel::dataChanged,
             this, &PlotManager::onChannelInfoChanged);
+
+    // TODO: remove when snapshot view supports multi display
+    checkNoVisChannels();
 }
 
 void PlotManager::construct(QWidget* plotArea, PlotMenu* menu)
@@ -94,8 +96,6 @@ void PlotManager::construct(QWidget* plo
     // initalize layout and single widget
     isMulti = false;
     scrollArea = NULL;
-    setupLayout(isMulti);
-    addPlotWidget();
 
     // connect to  menu
     connect(menu, &PlotMenu::symbolShowChanged, this, &PlotManager:: setSymbols);
@@ -148,7 +148,7 @@ void PlotManager::onNumChannelsChanged(u
         // add new channels
         for (unsigned int i = oldNum; i < numOfChannels; i++)
         {
-            addCurve(_stream->channel(i)->name(), _stream->channel(i)->yData());
+            addCurve(_stream->channel(i)->name(), _stream->channel(i)->xData(), _stream->channel(i)->yData());
         }
     }
     else if(numOfChannels < oldNum)
@@ -217,8 +217,6 @@ void PlotManager::checkNoVisChannels()
 
 void PlotManager::setMulti(bool enabled)
 {
-    if (enabled == isMulti) return;
-
     isMulti = enabled;
 
     // detach all curves
@@ -239,11 +237,14 @@ void PlotManager::setMulti(bool enabled)
     if (isMulti)
     {
         // add new widgets and attach
+        int i = 0;
         for (auto curve : curves)
         {
             auto plot = addPlotWidget();
             plot->setVisible(curve->isVisible());
+            plot->setDispChannels(QVector(1, _stream->channel(i)));
             curve->attach(plot);
+            i++;
         }
     }
     else
@@ -251,6 +252,11 @@ void PlotManager::setMulti(bool enabled)
         // add a single widget
         auto plot = addPlotWidget();
 
+        if (_stream != nullptr)
+        {
+            plot->setDispChannels(_stream->allChannels());
+        }
+
         // attach all curves
         for (auto curve : curves)
         {
@@ -332,11 +338,10 @@ Plot* PlotManager::addPlotWidget()
     return plot;
 }
 
-void PlotManager::addCurve(QString title, const FrameBuffer* buffer)
+void PlotManager::addCurve(QString title, const XFrameBuffer* xBuf, const FrameBuffer* yBuf)
 {
     auto curve = new QwtPlotCurve(title);
-    auto series = new FrameBufferSeries(buffer);
-    series->setXAxis(_xAxisAsIndex, _xMin, _xMax);
+    auto series = new FrameBufferSeries(xBuf, yBuf);
     curve->setSamples(series);
     _addCurve(curve);
 }
@@ -362,6 +367,20 @@ void PlotManager::_addCurve(QwtPlotCurve
         plot = plotWidgets[0];
     }
 
+    if (_stream != nullptr)     // not displaying snapshot
+    {
+        QVector dispChannels;
+        if (isMulti)
+        {
+            dispChannels = QVector(1, _stream->channel(index));
+        }
+        else
+        {
+            dispChannels = _stream->allChannels();
+        }
+        plot->setDispChannels(dispChannels);
+    }
+
     // show the curve
     curve->attach(plot);
     plot->replot();
@@ -369,6 +388,16 @@ void PlotManager::_addCurve(QwtPlotCurve
 
 void PlotManager::removeCurves(unsigned number)
 {
+    if (_stream != nullptr)     // not displaying snapshot
+    {
+        if (! isMulti)
+        {
+            QVector dispChannels;
+            dispChannels = _stream->allChannels();
+            plotWidgets[0]->setDispChannels(dispChannels);
+        }
+    }
+
     for (unsigned i = 0; i < number; i++)
     {
         if (!curves.isEmpty())
@@ -481,10 +510,13 @@ void PlotManager::setXAxis(bool asIndex,
     _xAxisAsIndex = asIndex;
     _xMin = xMin;
     _xMax = xMax;
+
+    int ci = 0;
     for (auto curve : curves)
     {
         FrameBufferSeries* series = static_cast(curve->data());
-        series->setXAxis(asIndex, xMin, xMax);
+        series->setX(_stream->channel(ci)->xData());
+        ci++;
     }
     for (auto plot : plotWidgets)
     {
diff --git a/src/plotmanager.h b/src/plotmanager.h
--- a/src/plotmanager.h
+++ b/src/plotmanager.h
@@ -41,7 +41,7 @@ class PlotManager : public QObject
 
 public:
     explicit PlotManager(QWidget* plotArea, PlotMenu* menu,
-                         const Stream* stream = NULL,
+                         const Stream* stream = nullptr,
                          QObject *parent = 0);
     explicit PlotManager(QWidget* plotArea, PlotMenu* menu,
                          Snapshot* snapshot,
@@ -49,7 +49,7 @@ public:
     ~PlotManager();
     /// Add a new curve with title and buffer. A color is
     /// automatically chosen for curve.
-    void addCurve(QString title, const FrameBuffer* buffer);
+    void addCurve(QString title, const XFrameBuffer* xBuf, const FrameBuffer* yBuf);
     /// Removes curves from the end
     void removeCurves(unsigned number);
     /// Returns current number of curves known by plot manager
@@ -82,7 +82,7 @@ private:
     QList curves;
     QList plotWidgets;
     Plot* emptyPlot;  ///< for displaying when all channels are hidden
-    const Stream* _stream;       ///< attached stream, can be `NULL`
+    const Stream* _stream;       ///< attached stream, can be `nullptr`
     const ChannelInfoModel* infoModel;
     bool isDemoShown;
     bool _autoScaled;
diff --git a/src/plotmenu.cpp b/src/plotmenu.cpp
--- a/src/plotmenu.cpp
+++ b/src/plotmenu.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -66,7 +66,7 @@ PlotMenu::PlotMenu(QWidget* parent) :
     setSymbolsMenu.addAction(&setSymbolsAutoAct);
     setSymbolsAutoAct.setCheckable(true);
     setSymbolsAutoAct.setChecked(true);
-    connect(&setSymbolsAutoAct, SELECT::OVERLOAD_OF(&QAction::triggered),
+    connect(&setSymbolsAutoAct, SELECT::OVERLOAD_OF(&QAction::toggled),
             [this](bool checked)
             {
                 if (checked) emit symbolShowChanged(Plot::ShowSymbolsAuto);
@@ -74,18 +74,18 @@ PlotMenu::PlotMenu(QWidget* parent) :
 
     setSymbolsMenu.addAction(&setSymbolsShowAct);
     setSymbolsShowAct.setCheckable(true);
-    connect(&setSymbolsShowAct, SELECT::OVERLOAD_OF(&QAction::triggered),
+    connect(&setSymbolsShowAct, SELECT::OVERLOAD_OF(&QAction::toggled),
             [this](bool checked)
             {
-                if (checked) symbolShowChanged(Plot::ShowSymbolsShow);
+                if (checked) emit symbolShowChanged(Plot::ShowSymbolsShow);
             });
 
     setSymbolsMenu.addAction(&setSymbolsHideAct);
     setSymbolsHideAct.setCheckable(true);
-    connect(&setSymbolsHideAct, SELECT::OVERLOAD_OF(&QAction::triggered),
+    connect(&setSymbolsHideAct, SELECT::OVERLOAD_OF(&QAction::toggled),
             [this](bool checked)
             {
-                if (checked) symbolShowChanged(Plot::ShowSymbolsHide);
+                if (checked) emit symbolShowChanged(Plot::ShowSymbolsHide);
             });
 
     // add symbol actions to same group so that they appear as radio buttons
@@ -202,20 +202,17 @@ void PlotMenu::loadSettings(QSettings* s
     QString showSymbolsStr = settings->value(SG_Plot_Symbols, QString()).toString();
     if (showSymbolsStr == "auto")
     {
-        // setSymbols(Plot::ShowSymbolsAuto);
         setSymbolsAutoAct.setChecked(true);
     }
     else if (showSymbolsStr == "show")
     {
-        // setSymbols(Plot::ShowSymbolsShow);
         setSymbolsShowAct.setChecked(true);
     }
     else if (showSymbolsStr == "hide")
     {
-        // setSymbols(Plot::ShowSymbolsHide);
         setSymbolsHideAct.setChecked(true);
     }
-    else
+    else if (!showSymbolsStr.isEmpty())
     {
         qCritical() << "Invalid symbol setting:" << showSymbolsStr;
     }
diff --git a/src/portcontrol.cpp b/src/portcontrol.cpp
--- a/src/portcontrol.cpp
+++ b/src/portcontrol.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -79,10 +79,10 @@ PortControl::PortControl(QSerialPort* po
                      this, &PortControl::onTbPortListActivated);
     QObject::connect(ui->cbPortList,
                      SELECT::OVERLOAD_OF(&QComboBox::activated),
-                     this, &PortControl::selectPort);
+                     this, &PortControl::selectListedPort);
     QObject::connect(&tbPortList,
                      SELECT::OVERLOAD_OF(&QComboBox::activated),
-                     this, &PortControl::selectPort);
+                     this, &PortControl::selectListedPort);
 
     // setup buttons
     ui->pbOpenPort->setDefaultAction(&openAction);
@@ -91,7 +91,7 @@ PortControl::PortControl(QSerialPort* po
     // setup baud rate selection widget
     QObject::connect(ui->cbBaudRate,
                      SELECT::OVERLOAD_OF(&QComboBox::activated),
-                     this, &PortControl::selectBaudRate);
+                     this, &PortControl::_selectBaudRate);
 
     // setup parity selection buttons
     parityButtons.addButton(ui->rbNoParity, (int) QSerialPort::NoParity);
@@ -198,7 +198,7 @@ void PortControl::loadBaudRateList()
     }
 }
 
-void PortControl::selectBaudRate(QString baudRate)
+void PortControl::_selectBaudRate(QString baudRate)
 {
     if (serialPort->isOpen())
     {
@@ -289,7 +289,7 @@ void PortControl::togglePort()
         if (serialPort->open(QIODevice::ReadWrite))
         {
             // set port settings
-            selectBaudRate(ui->cbBaudRate->currentText());
+            _selectBaudRate(ui->cbBaudRate->currentText());
             selectParity((QSerialPort::Parity) parityButtons.checkedId());
             selectDataBits((QSerialPort::DataBits) dataBitsButtons.checkedId());
             selectStopBits((QSerialPort::StopBits) stopBitsButtons.checkedId());
@@ -310,10 +310,17 @@ void PortControl::togglePort()
     openAction.setChecked(serialPort->isOpen());
 }
 
-void PortControl::selectPort(QString portName)
+void PortControl::selectListedPort(QString portName)
 {
     // portName may be coming from combobox
     portName = portName.split(" ")[0];
+
+    QSerialPortInfo portInfo(portName);
+    if (portInfo.isNull())
+    {
+        qWarning() << "Device doesn't exists:" << portName;
+    }
+
     // has selection actually changed
     if (portName != serialPort->portName())
     {
@@ -366,6 +373,13 @@ void PortControl::onTbPortListActivated(
 
 void PortControl::onPortError(QSerialPort::SerialPortError error)
 {
+#ifdef Q_OS_UNIX
+    // For suppressing "Invalid argument" errors that happens with pseudo terminals
+    auto isPtsInvalidArgErr = [this] () -> bool {
+        return serialPort->portName().contains("pts/") && serialPort->errorString().contains("Invalid argument");
+    };
+#endif
+
     switch(error)
     {
         case QSerialPort::NoError :
@@ -408,12 +422,22 @@ required privileges or device is already
             qCritical() << "An error occurred while reading data.";
             break;
         case QSerialPort::UnsupportedOperationError:
+#ifdef Q_OS_UNIX
+            // Qt 5.5 gives "Invalid argument" with 'UnsupportedOperationError'
+            if (isPtsInvalidArgErr())
+                break;
+#endif
             qCritical() << "Operation is not supported.";
             break;
         case QSerialPort::TimeoutError:
             qCritical() << "A timeout error occurred.";
             break;
         case QSerialPort::UnknownError:
+#ifdef Q_OS_UNIX
+            // Qt 5.2 gives "Invalid argument" with 'UnknownError'
+            if (isPtsInvalidArgErr())
+                break;
+#endif
             qCritical() << "Unknown error! Error: " << serialPort->errorString();
             break;
         default:
@@ -453,6 +477,64 @@ QString PortControl::currentFlowControlT
     }
 }
 
+void PortControl::selectPort(QString portName)
+{
+    int portIndex = portList.indexOfName(portName);
+    if (portIndex < 0) // not in list, add to model and update the selections
+    {
+        portList.appendRow(new PortListItem(portName));
+        portIndex = portList.rowCount()-1;
+    }
+
+    ui->cbPortList->setCurrentIndex(portIndex);
+    tbPortList.setCurrentIndex(portIndex);
+
+    selectListedPort(portName);
+}
+
+void PortControl::selectBaudrate(QString baudRate)
+{
+    int baudRateIndex = ui->cbBaudRate->findText(baudRate);
+    if (baudRateIndex < 0)
+    {
+        ui->cbBaudRate->setCurrentText(baudRate);
+    }
+    else
+    {
+        ui->cbBaudRate->setCurrentIndex(baudRateIndex);
+    }
+    _selectBaudRate(baudRate);
+}
+
+void PortControl::openPort()
+{
+    if (!serialPort->isOpen())
+    {
+        openAction.trigger();
+    }
+}
+
+unsigned PortControl::maxBitRate() const
+{
+    float baud = serialPort->baudRate();
+    float dataBits = serialPort->dataBits();
+    float parityBits = serialPort->parity() == QSerialPort::NoParity ? 0 : 1;
+
+    float stopBits;
+    if (serialPort->stopBits() == QSerialPort::OneAndHalfStop)
+    {
+        stopBits = 1.5;
+    }
+    else
+    {
+        stopBits = serialPort->stopBits();
+    }
+
+    float frame_size = 1 /* start bit */ + dataBits + parityBits + stopBits;
+
+    return float(baud) / frame_size;
+}
+
 void PortControl::saveSettings(QSettings* settings)
 {
     settings->beginGroup(SettingGroup_Port);
@@ -494,7 +576,7 @@ void PortControl::loadSettings(QSettings
     parityButtons.button(paritySetting)->setChecked(true);
 
     // load number of bits
-    int dataBits = settings->value(SG_Port_Parity, dataBitsButtons.checkedId()).toInt();
+    int dataBits = settings->value(SG_Port_DataBits, dataBitsButtons.checkedId()).toInt();
     if (dataBits >=5 && dataBits <= 8)
     {
         dataBitsButtons.button((QSerialPort::DataBits) dataBits)->setChecked(true);
diff --git a/src/portcontrol.h b/src/portcontrol.h
--- a/src/portcontrol.h
+++ b/src/portcontrol.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -47,6 +47,12 @@ public:
     QSerialPort* serialPort;
     QToolBar* toolBar();
 
+    void selectPort(QString portName);
+    void selectBaudrate(QString baudRate);
+    void openPort();
+    /// Returns maximum bit rate for current baud rate
+    unsigned maxBitRate() const;
+
     /// Stores port settings into a `QSettings`
     void saveSettings(QSettings* settings);
     /// Loads port settings from a `QSettings`. If open serial port is closed.
@@ -76,19 +82,18 @@ private:
     /// Returns currently selected flow control as text to be saved in settings
     QString currentFlowControlText();
 
-public slots:
+private slots:
     void loadPortList();
     void loadBaudRateList();
     void togglePort();
-    void selectPort(QString portName);
+    void selectListedPort(QString portName);
 
-    void selectBaudRate(QString baudRate);
+    void _selectBaudRate(QString baudRate);
     void selectParity(int parity); // parity must be one of QSerialPort::Parity
     void selectDataBits(int dataBits); // bits must be one of QSerialPort::DataBits
     void selectStopBits(int stopBits); // stopBits must be one of QSerialPort::StopBits
     void selectFlowControl(int flowControl); // flowControl must be one of QSerialPort::FlowControl
 
-private slots:
     void openActionTriggered(bool checked);
     void onCbPortListActivated(int index);
     void onTbPortListActivated(int index);
diff --git a/src/setting_defines.h b/src/setting_defines.h
--- a/src/setting_defines.h
+++ b/src/setting_defines.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -30,6 +30,7 @@ const char SettingGroup_Channels[] = "Ch
 const char SettingGroup_Plot[] = "Plot";
 const char SettingGroup_Commands[] = "Commands";
 const char SettingGroup_Record[] = "Record";
+const char SettingGroup_TextView[] = "TextView";
 const char SettingGroup_UpdateCheck[] = "UpdateCheck";
 
 // mainwindow setting keys
@@ -112,6 +113,10 @@ const char SG_Record_Separator[]        
 const char SG_Record_DisableBuffering[] = "disableBuffering";
 const char SG_Record_Timestamp[]        = "timestamp";
 
+// text view settings keys
+const char SG_TextView_NumLines[] = "numLines";
+const char SG_TextView_Decimals[] = "decimals";
+
 // update check settings keys
 const char SG_UpdateCheck_Periodic[]  = "periodicCheck";
 const char SG_UpdateCheck_LastCheck[] = "lastCheck";
diff --git a/src/snapshot.h b/src/snapshot.h
--- a/src/snapshot.h
+++ b/src/snapshot.h
@@ -28,6 +28,7 @@
 
 #include "channelinfomodel.h"
 #include "readonlybuffer.h"
+#include "indexbuffer.h"
 
 class SnapshotView;
 class MainWindow;
@@ -40,7 +41,8 @@ public:
     Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel, bool saved = false);
     ~Snapshot();
 
-    // TODO: yData of snapshot shouldn't be public, preferable should be handled in constructor
+    // TODO: yData and xData of snapshot shouldn't be public, preferable should be handled in constructor
+    QVector xData;
     QVector yData;
     QAction* showAction();
     QAction* deleteAction();
diff --git a/src/snapshotmanager.cpp b/src/snapshotmanager.cpp
--- a/src/snapshotmanager.cpp
+++ b/src/snapshotmanager.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2018 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -71,6 +71,7 @@ Snapshot* SnapshotManager::makeSnapshot(
 
     for (unsigned ci = 0; ci < _stream->numChannels(); ci++)
     {
+        snapshot->xData.append(new IndexBuffer(_stream->numSamples()));
         snapshot->yData.append(new ReadOnlyBuffer(_stream->channel(ci)->yData()));
     }
 
@@ -156,8 +157,16 @@ void SnapshotManager::loadSnapshotFromFi
     QTextStream ts(&file);
     QString line;
     unsigned lineNum = 1;
+
+#if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0)
     while (ts.readLineInto(&line))
     {
+#else
+    while (true)
+    {
+        line = ts.readLine();
+        if (line.isNull()) break;
+#endif
         // parse line
         auto split = line.split(',');
 
@@ -194,6 +203,7 @@ void SnapshotManager::loadSnapshotFromFi
 
     for (unsigned ci = 0; ci < numOfChannels; ci++)
     {
+        snapshot->xData.append(new IndexBuffer(data[ci].size()));
         snapshot->yData.append(new ReadOnlyBuffer(data[ci].data(), data[ci].size()));
     }
 
diff --git a/src/stream.cpp b/src/stream.cpp
--- a/src/stream.cpp
+++ b/src/stream.cpp
@@ -20,6 +20,7 @@
 #include "stream.h"
 #include "ringbuffer.h"
 #include "indexbuffer.h"
+#include "linindexbuffer.h"
 
 Stream::Stream(unsigned nc, bool x, unsigned ns) :
     _infoModel(nc)
@@ -27,15 +28,20 @@ Stream::Stream(unsigned nc, bool x, unsi
     _numSamples = ns;
     _paused = false;
 
+    xAsIndex = true;
+    xMin = 0;
+    xMax = 1;
+
     // create xdata buffer
     _hasx = x;
     if (x)
     {
-        xData = new RingBuffer(ns);
+        // TODO: implement XRingBuffer (binary search)
+        Q_ASSERT(false);
     }
     else
     {
-        xData = new IndexBuffer(ns);
+        xData = makeXBuffer();
     }
 
     // create channels
@@ -81,6 +87,16 @@ StreamChannel* Stream::channel(unsigned 
     return const_cast(static_cast(*this).channel(index));
 }
 
+QVector Stream::allChannels() const
+{
+    QVector result(numChannels());
+    for (unsigned ci = 0; ci < numChannels(); ci++)
+    {
+        result[ci] = channel(ci);
+    }
+    return result;
+}
+
 const ChannelInfoModel* Stream::infoModel() const
 {
     return &_infoModel;
@@ -118,11 +134,12 @@ void Stream::setNumChannels(unsigned nc,
     {
         if (x)
         {
-            xData = new RingBuffer(_numSamples);
+            // TODO: implement XRingBuffer (binary search)
+            Q_ASSERT(false);
         }
         else
         {
-            xData = new IndexBuffer(_numSamples);
+            xData = makeXBuffer();
         }
 
         for (auto c : channels)
@@ -142,6 +159,18 @@ void Stream::setNumChannels(unsigned nc,
     Sink::setNumChannels(nc, x);
 }
 
+XFrameBuffer* Stream::makeXBuffer() const
+{
+    if (xAsIndex)
+    {
+        return new IndexBuffer(_numSamples);
+    }
+    else
+    {
+        return new LinIndexBuffer(_numSamples, xMin, xMax);
+    }
+}
+
 const SamplePack* Stream::applyGainOffset(const SamplePack& pack) const
 {
     Q_ASSERT(infoModel()->gainOrOffsetEn());
@@ -191,7 +220,9 @@ void Stream::feedIn(const SamplePack& pa
     unsigned ns = pack.numSamples();
     if (_hasx)
     {
-        static_cast(xData)->addSamples(pack.xData(), ns);
+        // TODO: implement XRingBuffer (binary search)
+        Q_ASSERT(false);
+        // static_cast(xData)->addSamples(pack.xData(), ns);
     }
 
     // modified pack that gain and offset is applied to
@@ -237,6 +268,24 @@ void Stream::setNumSamples(unsigned valu
     }
 }
 
+void Stream::setXAxis(bool asIndex, double min, double max)
+{
+    xAsIndex = asIndex;
+    xMin = min;
+    xMax = max;
+
+    // Note that x axis scaling is ignored when X is provided from source as data
+    // TODO: assert (UI options for x axis should be disabled)
+    if (!hasX())
+    {
+        xData = makeXBuffer();
+        for (auto c : channels)
+        {
+            c->setX(xData);
+        }
+    }
+}
+
 void Stream::saveSettings(QSettings* settings) const
 {
     _infoModel.saveSettings(settings);
diff --git a/src/stream.h b/src/stream.h
--- a/src/stream.h
+++ b/src/stream.h
@@ -48,16 +48,16 @@ public:
      * @param x has X data input
      * @param ns number of samples
      */
-    Stream(unsigned nc = 0, bool x = false, unsigned ns = 0);
+    Stream(unsigned nc = 1, bool x = false, unsigned ns = 2);
     ~Stream();
 
-    // implementations for `Source`
-    virtual bool hasX() const;
-    virtual unsigned numChannels() const;
+    bool hasX() const;
+    unsigned numChannels() const;
 
     unsigned numSamples() const;
     const StreamChannel* channel(unsigned index) const;
     StreamChannel* channel(unsigned index);
+    QVector allChannels() const;
     const ChannelInfoModel* infoModel() const;
     ChannelInfoModel* infoModel();
 
@@ -79,10 +79,13 @@ signals:
     void dataAdded(); ///< emitted when data added to channel man.
 
 public slots:
-    // TODO: these won't be public
-    // void setNumChannels(unsigned number);
+    /// Change number of samples (buffer size)
     void setNumSamples(unsigned value);
 
+    /// Change X axis style
+    /// @note Ignored when X is provided by source (hasX == true)
+    void setXAxis(bool asIndex, double min, double max);
+
     /// When paused data feed is ignored
     void pause(bool paused);
 
@@ -94,11 +97,14 @@ private:
     bool _paused;
 
     bool _hasx;
-    ResizableBuffer* xData;
+    XFrameBuffer* xData;
     QList channels;
 
     ChannelInfoModel _infoModel;
 
+    bool xAsIndex;
+    double xMin, xMax;
+
     /**
      * Applies gain and offset to given pack.
      *
@@ -111,6 +117,9 @@ private:
      * @return modified data
      */
     const SamplePack* applyGainOffset(const SamplePack& pack) const;
+
+    /// Returns a new virtual X buffer for settings
+    XFrameBuffer* makeXBuffer() const;
 };
 
 
diff --git a/src/streamchannel.cpp b/src/streamchannel.cpp
--- a/src/streamchannel.cpp
+++ b/src/streamchannel.cpp
@@ -17,9 +17,10 @@
   along with serialplot.  If not, see .
 */
 
+#include 
 #include "streamchannel.h"
 
-StreamChannel::StreamChannel(unsigned i, const FrameBuffer* x,
+StreamChannel::StreamChannel(unsigned i, const XFrameBuffer* x,
               FrameBuffer* y, ChannelInfoModel* info)
 {
     _index = i;
@@ -37,8 +38,37 @@ unsigned StreamChannel::index() const {r
 QString StreamChannel::name() const {return _info->name(_index);};
 QColor StreamChannel::color() const {return _info->color(_index);};
 bool StreamChannel::visible() const {return _info->isVisible(_index);};
-const FrameBuffer* StreamChannel::xData() const {return _x;}
+const XFrameBuffer* StreamChannel::xData() const {return _x;}
 const FrameBuffer* StreamChannel::yData() const {return _y;}
 FrameBuffer* StreamChannel::yData() {return _y;}
 const ChannelInfoModel* StreamChannel::info() const {return _info;}
-void StreamChannel::setX(const FrameBuffer* x) {_x = x;};
+void StreamChannel::setX(const XFrameBuffer* x) {_x = x;};
+
+double StreamChannel::findValue(double x) const
+{
+    int index = _x->findIndex(x);
+    Q_ASSERT(index < (int) _x->size());
+
+    if (index >= 0)
+    {
+        // can't do estimation for last sample
+        if (index == (int) _x->size() - 1)
+        {
+            return _y->sample(index);
+        }
+        else
+        {
+            // calculate middle of the line
+            double prev_x = _x->sample(index);
+            double next_x = _x->sample(index+1);
+            double ratio = (x - prev_x) / (next_x - prev_x);
+            double prev_y = _y->sample(index);
+            double next_y = _y->sample(index+1);
+            return ratio * (next_y - prev_y) + prev_y;
+        }
+    }
+    else
+    {
+        return std::numeric_limits::quiet_NaN();
+    }
+}
diff --git a/src/streamchannel.h b/src/streamchannel.h
--- a/src/streamchannel.h
+++ b/src/streamchannel.h
@@ -35,7 +35,7 @@ public:
      * @param info channel info model
      */
     StreamChannel(unsigned i,
-                  const FrameBuffer* x,
+                  const XFrameBuffer* x,
                   FrameBuffer* y,
                   ChannelInfoModel* info);
     ~StreamChannel();
@@ -44,15 +44,23 @@ public:
     QString name() const;
     QColor color() const;
     bool visible() const;
-    const FrameBuffer* xData() const;
+    const XFrameBuffer* xData() const;
     FrameBuffer* yData();
     const FrameBuffer* yData() const;
     const ChannelInfoModel* info() const;
-    void setX(const FrameBuffer* x);
+    void setX(const XFrameBuffer* x);
+
+    /**
+     * Returns sample value for `x`.
+     *
+     * If `x` is out of range `NaN` is returned. A calculated (linear)
+     * value is returned when `x` is in between two data points.
+     */
+    double findValue(double x) const;
 
 private:
     unsigned _index;
-    const FrameBuffer* _x;
+    const XFrameBuffer* _x;
     FrameBuffer* _y;
     ChannelInfoModel* _info;
 };
diff --git a/src/updatechecker.cpp b/src/updatechecker.cpp
--- a/src/updatechecker.cpp
+++ b/src/updatechecker.cpp
@@ -118,7 +118,7 @@ bool UpdateChecker::parseData(const QJso
 
     if (!data.isObject()) return false;
 
-    auto values = data.object()["values"];
+    auto values = data.object().value("values");
     if (values == QJsonValue::Undefined || !values.isArray()) return false;
 
     for (auto value : values.toArray())
diff --git a/src/zoomer.cpp b/src/zoomer.cpp
--- a/src/zoomer.cpp
+++ b/src/zoomer.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2020 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -19,9 +19,13 @@
 
 #include "zoomer.h"
 #include 
-#include 
+#include 
+#include 
+#include 
+#include 
 
-#include 
+static const int VALUE_POINT_DIAM = 4;
+static const int VALUE_TEXT_MARGIN = VALUE_POINT_DIAM + 2;
 
 Zoomer::Zoomer(QWidget* widget, bool doReplot) :
     ScrollZoomer(widget)
@@ -59,6 +63,11 @@ void Zoomer::zoom( const QRectF & rect)
     ScrollZoomer::zoom(rect);
 }
 
+void Zoomer::setDispChannels(QVector channels)
+{
+    dispChannels = channels;
+}
+
 QwtText Zoomer::trackerTextF(const QPointF& pos) const
 {
     QwtText b = ScrollZoomer::trackerTextF(pos);
@@ -102,6 +111,225 @@ QRegion Zoomer::rubberBandMask() const
     return QRegion(r);
 }
 
+void Zoomer::drawTracker(QPainter* painter) const
+{
+    if (isActive())
+    {
+        QwtPlotZoomer::drawTracker(painter);
+    }
+    else if (dispChannels.length())
+    {
+        drawValues(painter);
+    }
+}
+
+QList Zoomer::visChannels() const
+{
+    QList result;
+
+    for (unsigned ci = 0; ci < (unsigned) dispChannels.length(); ci++)
+    {
+        if (dispChannels[ci]->visible())
+            result.append(dispChannels[ci]);
+    }
+
+    return result;
+}
+
+const double ValueLabelHeight = 12; // TODO: calculate
+
+struct ChannelValue
+{
+    const StreamChannel* ch;
+    double value;
+    double y;
+    double top() const {return y;};
+    double bottom() const {return y + ValueLabelHeight;};
+};
+
+static void layoutValues(QList& values)
+{
+    typedef ChannelValue LayItem;
+    typedef QList LayItemList;
+
+    struct LayGroup
+    {
+        struct VRange {double top, bottom;};
+        LayItemList items;
+        LayGroup(LayItem* initialItem) {items.append(initialItem);}
+        unsigned numItems() const {return items.size();}
+        double top() const {return items.first()->top();}
+        double bottom() const {return items.last()->bottom();}
+        VRange vRange() const {return {top(), bottom()};}
+        double overlap(const LayGroup* otrGroup) const
+            {
+                auto myr = vRange();
+                auto otr = otrGroup->vRange();
+
+                double a = myr.bottom - otr.top;
+                double b = otr.bottom - myr.top;
+                if (a > 0 and b > 0)
+                {
+                    return std::min(a, b);
+                }
+                return 0;
+            }
+        void moveBy(double y) {for (auto it : items) it->y += y;}
+        void join(LayGroup* other)
+            {
+                // assumes other group is below this one and they overlap
+                double ovr_h = overlap(other);
+
+                // groups are moved less if they have more items and vice versa
+                double ratio = double(numItems()) / double(numItems() + other->numItems());
+                double self_off = ovr_h * (1. - ratio);
+
+                // make sure we don't go out of screen (above) after the shift
+                double final_top = top() - self_off;
+                if (final_top < 0)
+                    self_off += final_top;
+
+                // move groups
+                moveBy(-self_off);                // up
+                other->moveBy(ovr_h - self_off);  // down
+
+                // finalize the merge by gettin items from other
+                do
+                {
+                    items.append(other->items.takeFirst());
+                } while (!other->items.isEmpty());
+            }
+    };
+
+    // create initial groups (1 group per item)
+    QList groups;
+    for (auto& val : values)
+        groups.append(new LayGroup(&val));
+
+    // sort groups according to their items position
+    struct {
+        bool operator()(LayGroup* a, LayGroup* b) const
+            {
+                return a->top() < b->top();
+            }
+    } compTops;
+
+    std::sort(groups.begin(), groups.end(), compTops);
+
+    // do spacing
+    bool somethingOverlaps = true;
+    while (somethingOverlaps and groups.size() > 1)
+    {
+        somethingOverlaps = false;
+        for (int i = 0; i < groups.size() - 1; i++)
+        {
+            auto a = groups[i];
+            auto b = groups[i + 1];
+
+            // make sure nothing is over the top
+            if (a->top() < 0)
+                a->moveBy(-a->top());
+
+            // join if groups overlap
+            if (a->overlap(b))
+            {
+                somethingOverlaps = true;
+                a->join(b);
+                delete groups.takeAt(i + 1);
+                break;
+            }
+        }
+    }
+
+    // cleanup
+    do
+    {
+        delete groups.takeFirst();
+    } while (!groups.isEmpty());
+};
+
+void Zoomer::drawValues(QPainter* painter) const
+{
+    auto tpos = trackerPosition();
+    if (tpos.x() < 0) return;   // cursor not on window
+
+    // find Y values for current cursor X position
+    double x = invTransform(tpos).x();
+    auto channels = visChannels();
+    QList values;
+    for (auto ch : channels)
+    {
+        double value = ch->findValue(x);
+        if (!std::isnan(value))
+        {
+            auto point = transform(QPointF(x, value));
+            values.append({ch, value, double(point.y())});
+        }
+    }
+
+    // TODO should keep?
+    if (values.isEmpty())
+    {
+        return;
+    }
+
+    layoutValues(values);
+
+    painter->save();
+
+    // draw vertical line
+    auto linePen = rubberBandPen();
+    linePen.setStyle(Qt::DotLine);
+    painter->setPen(linePen);
+    const QRect pRect = pickArea().boundingRect().toRect();
+    int px = tpos.x();
+    painter->drawLine(px, pRect.top(), px, pRect.bottom());
+
+    // draw sample values
+    for (auto value : values)
+    {
+        double val = value.value;
+        auto ch = value.ch;
+
+        auto point = transform(QPointF(x, val));
+
+        painter->setBrush(ch->color());
+        painter->setPen(Qt::NoPen);
+        painter->drawEllipse(point, VALUE_POINT_DIAM, VALUE_POINT_DIAM);
+
+        painter->setPen(rubberBandPen());
+        // We give a very small (1x1) rectangle but disable clipping
+        painter->drawText(QRectF(point.x() + VALUE_TEXT_MARGIN, value.y, 1, 1),
+                          Qt::AlignVCenter | Qt::TextDontClip,
+                          QString("%1").arg(val));
+    }
+
+    painter->restore();
+}
+
+QRect Zoomer::trackerRect(const QFont& font) const
+{
+    if (isActive())
+    {
+        return QwtPlotZoomer::trackerRect(font);
+    }
+    else
+    {
+        return valueTrackerRect(font);
+    }
+}
+
+QRect Zoomer::valueTrackerRect(const QFont& font) const
+{
+    // TODO: consider using actual tracker values for width calculation
+    const int textWidth = qCeil(QwtText("-8.8888888").textSize(font).width());
+    const int width = textWidth + VALUE_POINT_DIAM + VALUE_TEXT_MARGIN;
+    const int x = trackerPosition().x() - VALUE_POINT_DIAM;
+    const auto pickRect = pickArea().boundingRect();
+
+    return QRect(x, pickRect.y(), width, pickRect.height());
+}
+
 void Zoomer::widgetMousePressEvent(QMouseEvent* mouseEvent)
 {
     if (mouseEvent->modifiers() & Qt::ControlModifier)
diff --git a/src/zoomer.h b/src/zoomer.h
--- a/src/zoomer.h
+++ b/src/zoomer.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2019 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -20,25 +20,36 @@
 #ifndef ZOOMER_H
 #define ZOOMER_H
 
-#include 
+#include 
+#include 
+#include 
+
+#include "scrollzoomer.h"
+#include "streamchannel.h"
 
 class Zoomer : public ScrollZoomer
 {
     Q_OBJECT
 
 public:
-    Zoomer(QWidget *, bool doReplot=true);
+    Zoomer(QWidget*, bool doReplot=true);
     void zoom(int up);
-    void zoom( const QRectF & );
+    void zoom(const QRectF&);
+    /// Set displayed channels for value tracking (can be null)
+    void setDispChannels(QVector channels);
 
 signals:
     void unzoomed();
 
 protected:
     /// Re-implemented to display selection size in the tracker text.
-    QwtText trackerTextF(const QPointF &pos) const;
+    QwtText trackerTextF(const QPointF &pos) const override;
+    /// Re-implemented for sample value tracker
+    QRect trackerRect(const QFont&) const override;
     /// Re-implemented for alpha background
-    void drawRubberBand(QPainter* painter) const;
+    void drawRubberBand(QPainter* painter) const override;
+    /// Re-implemented to draw sample values
+    void drawTracker(QPainter* painter) const override;
     /// Re-implemented for alpha background (masking is basically disabled)
     QRegion rubberBandMask() const;
     /// Overloaded for panning
@@ -51,6 +62,15 @@ protected:
 private:
     bool is_panning;
     QPointF pan_point;
+    /// displayed channels for value tracking
+    QVector dispChannels;
+
+    /// Get a list of visible channels
+    QList visChannels() const;
+    /// Draw sample values
+    void drawValues(QPainter* painter) const;
+    /// Returns trackerRect for value tracker
+    QRect valueTrackerRect(const QFont& font) const;
 };
 
 #endif // ZOOMER_H
diff --git a/tests/test.cpp b/tests/test.cpp
--- a/tests/test.cpp
+++ b/tests/test.cpp
@@ -224,6 +224,14 @@ TEST_CASE("LinIndexBuffer", "[memory, bu
 
     REQUIRE(buf.sample(0) == -5.0);
     REQUIRE(buf.sample(19) == 5.0);
+
+    buf.resize(10);
+    buf.setLimits({0., 3.});
+    REQUIRE(buf.findIndex(0.01) == 0);
+    REQUIRE(buf.findIndex(1.51) == 4);
+    REQUIRE(buf.findIndex(2.99) == 8);
+    REQUIRE(buf.findIndex(3.01) == XFrameBuffer::OUT_OF_RANGE);
+    REQUIRE(buf.findIndex(-0.01) == XFrameBuffer::OUT_OF_RANGE);
 }
 
 TEST_CASE("RingBuffer sizing", "[memory, buffer]")
diff --git a/tests/test_stream.cpp b/tests/test_stream.cpp
--- a/tests/test_stream.cpp
+++ b/tests/test_stream.cpp
@@ -27,17 +27,17 @@ TEST_CASE("construction of stream with d
     // default values are an empty stream with no channels
     Stream s;
 
-    REQUIRE(s.numChannels() == 0);
+    REQUIRE(s.numChannels() == 1);
     REQUIRE(!s.hasX());
-    REQUIRE(s.numSamples() == 0);
+    REQUIRE(s.numSamples() == 2);
 }
 
 TEST_CASE("construction of stream with parameters", "[memory, stream]")
 {
-    Stream s(4, true, 100);
+    Stream s(4, false, 100);
 
     REQUIRE(s.numChannels() == 4);
-    REQUIRE(s.hasX());
+    REQUIRE(!s.hasX());
     REQUIRE(s.numSamples() == 100);
 
     for (unsigned i = 0; i < 4; i++)
@@ -64,6 +64,8 @@ TEST_CASE("changing stream number of cha
         REQUIRE(c->index() == i);
     }
 
+// TODO: enable test when `Stream` supports X channel
+#if 0
     // increase nc value, add X
     so._setNumChannels(5, true);
 
@@ -76,6 +78,7 @@ TEST_CASE("changing stream number of cha
         REQUIRE(c != NULL);
         REQUIRE(c->index() == i);
     }
+#endif
 
     // reduce nc value, remove X
     so._setNumChannels(1, false);
@@ -127,9 +130,11 @@ TEST_CASE("adding data to a stream with 
     }
 }
 
+// TODO: enable test when `Stream` supports X channel
+#if 0
 TEST_CASE("adding data to a stream with X", "[memory, stream, data, sink]")
 {
-    Stream s(3, true, 10);
+    Stream s(3, false, 10);
 
     // prepare data
     SamplePack pack(5, 3, true);
@@ -147,7 +152,7 @@ TEST_CASE("adding data to a stream with 
     }
 
     TestSource so(3, true);
-    so.connectSink(&s);
+    REQUIRE_THROWS(so.connectSink(&s));
 
     // test
     so._feed(pack);
@@ -178,6 +183,7 @@ TEST_CASE("adding data to a stream with 
         REQUIRE(x->sample(i) == (i-5)+10);
     }
 }
+#endif
 
 TEST_CASE("paused stream shouldn't store data", "[memory, stream, pause]")
 {