diff --git a/.hgtags b/.hgtags
--- a/.hgtags
+++ b/.hgtags
@@ -11,3 +11,4 @@ fd5f1eb480ec372b49df58b497458de05c30057c
 9c9a11cd15fd094e2b2b65dc51805fd8fd1d2460 v0.8.1
 4cf9a1ee1f107a38e03dbe17c4f2882c43d827c9 v0.9.0
 ef003f7af8f37f760c22dae776f5ff8e1b526deb v0.9.1
+7ce85527f3f2f6d19afc0401d2ac723f6c072481 v0.10.0
diff --git a/CMakeLists.txt b/CMakeLists.txt
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,5 @@
 #
-# Copyright © 2017 Hasan Yavuz Özderya
+# Copyright © 2018 Hasan Yavuz Özderya
 #
 # This file is part of serialplot.
 #
@@ -52,10 +52,18 @@ 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
@@ -77,6 +85,8 @@ qt5_wrap_ui(UI_FILES
   src/binarystreamreadersettings.ui
   src/asciireadersettings.ui
   src/framedreadersettings.ui
+  src/demoreadersettings.ui
+  src/updatecheckdialog.ui
   )
 
 if (WIN32)
@@ -94,7 +104,6 @@ add_executable(${PROGRAM_NAME} WIN32
   src/scrollzoomer.cpp
   src/scrollbar.cpp
   src/hidabletabwidget.cpp
-  src/framebuffer.cpp
   src/scalepicker.cpp
   src/scalezoomer.cpp
   src/portlist.cpp
@@ -111,8 +120,13 @@ add_executable(${PROGRAM_NAME} WIN32
   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/ringbuffer.cpp
+  src/indexbuffer.cpp
+  src/readonlybuffer.cpp
   src/framebufferseries.cpp
   src/numberformatbox.cpp
   src/endiannessbox.cpp
@@ -122,10 +136,22 @@ add_executable(${PROGRAM_NAME} WIN32
   src/asciireader.cpp
   src/asciireadersettings.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/updatechecker.cpp
+  src/versionnumber.cpp
+  src/updatecheckdialog.cpp
+  src/samplepack.cpp
+  src/source.cpp
+  src/sink.cpp
+  src/samplecounter.cpp
   misc/windows_icon.rc
   ${UI_FILES}
   ${RES_FILES}
@@ -135,8 +161,9 @@ add_executable(${PROGRAM_NAME} WIN32
 target_link_libraries(${PROGRAM_NAME}
   ${QWT_LIBRARY}
   ${QTCOLORWIDGETS_LIBRARIES}
+  ${LEDWIDGET_LIBRARY}
   )
-qt5_use_modules(${PROGRAM_NAME} Widgets SerialPort)
+qt5_use_modules(${PROGRAM_NAME} Widgets SerialPort Network)
 
 if (BUILD_QWT)
   add_dependencies(${PROGRAM_NAME} QWT)
@@ -146,6 +173,11 @@ 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")
 
@@ -162,13 +194,16 @@ else()
 endif()
 
 # default version
-set(VERSION_STRING "0.9.1")
+set(VERSION_STRING "0.10.0")
 set(VERSION_REVISION "0")
 
 # get revision number from mercurial and parse version string
 include(GetVersion)
 
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_STRING=\\\"${VERSION_STRING}\\\" ")
+set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_MAJOR=${VERSION_MAJOR} ")
+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}\\\" ")
 
 # add make run target
diff --git a/cmake/modules/BuildLedWidget.cmake b/cmake/modules/BuildLedWidget.cmake
new file mode 100644
--- /dev/null
+++ b/cmake/modules/BuildLedWidget.cmake
@@ -0,0 +1,30 @@
+#
+# 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/BuildLinuxAppImage.cmake b/cmake/modules/BuildLinuxAppImage.cmake
--- a/cmake/modules/BuildLinuxAppImage.cmake
+++ b/cmake/modules/BuildLinuxAppImage.cmake
@@ -1,7 +1,11 @@
 # Based on: https://github.com/mhoeher/opentodolist
+#
+# Note: we extract linuxdeployqt appimage so that it can run in docker, that's
+# because fuse doesn't work in docker.
 
-set(LINUXDEPLOYQT_URL "https://github.com/probonopd/linuxdeployqt/releases/download/4/linuxdeployqt-4-x86_64.AppImage")
-set(LINUXDEPLOYQT_TOOL ${CMAKE_CURRENT_BINARY_DIR}/linuxdeployqt-4-x86_64.AppImage)
+set(LINUXDEPLOYQT_URL "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage")
+set(LINUXDEPLOYQT_APPIMAGE ${CMAKE_CURRENT_BINARY_DIR}/linuxdeployqt-continuous-x86_64.AppImage)
+set(LINUXDEPLOYQT_TOOL ${CMAKE_CURRENT_BINARY_DIR}/squashfs-root/AppRun)
 
 set(APPIMAGE_DIR ${CMAKE_CURRENT_BINARY_DIR}/${PROGRAM_NAME}-${VERSION_STRING}-${CMAKE_HOST_SYSTEM_PROCESSOR})
 
@@ -11,7 +15,9 @@ add_custom_command(
     COMMAND
         wget ${LINUXDEPLOYQT_URL}
     COMMAND
-        chmod a+x ${LINUXDEPLOYQT_TOOL})
+        chmod a+x ${LINUXDEPLOYQT_APPIMAGE}
+    COMMAND
+        ${LINUXDEPLOYQT_APPIMAGE} --appimage-extract)
 
 add_custom_target(
     appimage
diff --git a/cmake/modules/BuildQColorWidgets.cmake b/cmake/modules/BuildQColorWidgets.cmake
--- a/cmake/modules/BuildQColorWidgets.cmake
+++ b/cmake/modules/BuildQColorWidgets.cmake
@@ -1,5 +1,5 @@
 #
-# Copyright © 2017 Hasan Yavuz Özderya
+# Copyright © 2018 Hasan Yavuz Özderya
 #
 # This file is part of serialplot.
 #
@@ -21,8 +21,8 @@ include(ExternalProject)
 
 ExternalProject_Add(QCW
   PREFIX qcw
-  GIT_REPOSITORY https://github.com/mbasaglia/Qt-Color-Widgets
-  PATCH_COMMAND patch -t -p1 -i ${CMAKE_CURRENT_LIST_DIR}/qt_5_2_moc_creation_namespace_fix.diff
+  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 "")
diff --git a/cmake/modules/FindLedWidget.cmake b/cmake/modules/FindLedWidget.cmake
new file mode 100644
--- /dev/null
+++ b/cmake/modules/FindLedWidget.cmake
@@ -0,0 +1,25 @@
+#
+# 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 .
+#
+
+find_library(LEDWIDGET_LIBRARY "ledwidget")
+find_path(LEDWIDGET_INCLUDE_DIR "ledwidget.h" PATH_SUFFIXES "ledwidget")
+
+mark_as_advanced(LEDWIDGET_LIBRARY LEDWIDGET_INCLUDE_DIR)
+
+find_package_handle_standard_args(LedWidget DEFAULT_MSG LEDWIDGET_LIBRARY LEDWIDGET_INCLUDE_DIR)
diff --git a/cmake/modules/qt_5_2_moc_creation_namespace_fix.diff b/cmake/modules/qt_5_2_moc_creation_namespace_fix.diff
deleted file mode 100644
--- a/cmake/modules/qt_5_2_moc_creation_namespace_fix.diff
+++ /dev/null
@@ -1,39 +0,0 @@
-diff --git a/include/color_dialog.hpp b/include/color_dialog.hpp
-index 5c7653d..895215c 100644
---- a/include/color_dialog.hpp
-+++ b/include/color_dialog.hpp
-@@ -30,6 +30,8 @@
- 
- class QAbstractButton;
- 
-+using namespace color_widgets;
-+
- namespace color_widgets {
- 
- class QCP_EXPORT ColorDialog : public QDialog
-diff --git a/include/color_list_widget.hpp b/include/color_list_widget.hpp
-index 282bea5..7d8e0c5 100644
---- a/include/color_list_widget.hpp
-+++ b/include/color_list_widget.hpp
-@@ -25,6 +25,8 @@
- #include "abstract_widget_list.hpp"
- #include "color_wheel.hpp"
- 
-+using namespace color_widgets;
-+
- namespace color_widgets {
- 
- class QCP_EXPORT ColorListWidget : public AbstractWidgetList
-diff --git a/include/color_selector.hpp b/include/color_selector.hpp
-index db817d5..48b374d 100644
---- a/include/color_selector.hpp
-+++ b/include/color_selector.hpp
-@@ -25,6 +25,8 @@
- #include "color_preview.hpp"
- #include "color_wheel.hpp"
- 
-+using namespace color_widgets;
-+
- namespace color_widgets {
- 
- /**
diff --git a/misc/pseudo_device.py b/misc/pseudo_device.py
--- a/misc/pseudo_device.py
+++ b/misc/pseudo_device.py
@@ -6,7 +6,7 @@
 #
 # Currently this script only outputs ASCII(comma separated) data.
 #
-# Copyright © 2015 Hasan Yavuz Özderya
+# Copyright © 2018 Hasan Yavuz Özderya
 #
 # This file is part of serialplot.
 #
@@ -26,6 +26,22 @@
 
 import os, pty, time, struct, math
 
+def ascii_test_str(port):
+    text = """
+1,2,3
+2,4,6
+3,8,11
+1,2,3
+-1,-1,-1
+nana
+0,0,0
+1,na,na
+    """
+    while True:
+        for line in text.splitlines():
+            os.write(port, bytes(line+"\r\n", 'utf8'))
+            time.sleep(1)
+
 def ascii_test(port):
     """Put ASCII test data through pseudo terminal."""
     print("\n")
@@ -109,8 +125,9 @@ def run():
 
     try:
         # float_sine(master)
-        frame_test(master)
+        # frame_test(master)
         # ascii_test(master)
+        ascii_test_str(master)
     finally:
         # close the pseudo terminal files
         os.close(master)
diff --git a/serialplot.pro b/serialplot.pro
--- a/serialplot.pro
+++ b/serialplot.pro
@@ -68,7 +68,10 @@ SOURCES += \
     src/framedreader.cpp \
     src/plotmanager.cpp \
     src/numberformat.cpp \
-    src/recordpanel.cpp
+    src/recordpanel.cpp \
+    src/updatechecker.cpp \
+    src/updatecheckdialog.cpp \
+    src/demoreadersettings.cpp
 
 HEADERS += \
     src/mainwindow.h \
@@ -108,7 +111,10 @@ HEADERS += \
     src/plotmanager.h \
     src/setting_defines.h \
     src/numberformat.h \
-    src/recordpanel.h
+    src/recordpanel.h \
+    src/updatechecker.h \
+    src/updatecheckdialog.h \
+    src/demoreadersettings.h
 
 FORMS += \
     src/mainwindow.ui \
@@ -124,7 +130,9 @@ FORMS += \
     src/framedreadersettings.ui \
     src/binarystreamreadersettings.ui \
     src/asciireadersettings.ui \
-    src/recordpanel.ui
+    src/recordpanel.ui \
+    src/updatecheckdialog.ui \
+    src/demoreadersettings.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 © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -19,41 +19,27 @@
 
 #include "abstractreader.h"
 
-AbstractReader::AbstractReader(QIODevice* device, ChannelManager* channelMan,
-                               DataRecorder* recorder, QObject* parent) :
+AbstractReader::AbstractReader(QIODevice* device, QObject* parent) :
     QObject(parent)
 {
     _device = device;
-    _channelMan = channelMan;
-    _recorder = recorder;
-    recording = false;
+}
 
-    // initialize sps counter
-    sampleCount = 0;
-    samplesPerSecond = 0;
-    QObject::connect(&spsTimer, &QTimer::timeout,
-                     this, &AbstractReader::spsTimerTimeout);
-    // TODO: start sps timer when reader is enabled
-    spsTimer.start(SPS_UPDATE_TIMEOUT * 1000);
+void AbstractReader::pause(bool enabled)
+{
+    paused = enabled;
 }
 
-void AbstractReader::spsTimerTimeout()
+void AbstractReader::enable(bool enabled)
 {
-    unsigned currentSps = samplesPerSecond;
-    samplesPerSecond = (sampleCount/numOfChannels())/SPS_UPDATE_TIMEOUT;
-    if (currentSps != samplesPerSecond)
+    if (enabled)
     {
-        emit samplesPerSecondChanged(samplesPerSecond);
+        QObject::connect(_device, &QIODevice::readyRead,
+                         this, &AbstractReader::onDataReady);
     }
-    sampleCount = 0;
+    else
+    {
+        QObject::disconnect(_device, 0, this, 0);
+        disconnectSinks();
+    }
 }
-
-void AbstractReader::addData(double* samples, unsigned length)
-{
-    _channelMan->addData(samples, length);
-    if (recording)
-    {
-        _recorder->addData(samples, length, numOfChannels());
-    }
-    sampleCount += length;
-}
diff --git a/src/abstractreader.h b/src/abstractreader.h
--- a/src/abstractreader.h
+++ b/src/abstractreader.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -25,20 +25,16 @@
 #include 
 #include 
 
-#include "channelmanager.h"
-#include "datarecorder.h"
+#include "source.h"
 
 /**
  * All reader classes must inherit this class.
  */
-class AbstractReader : public QObject
+class AbstractReader : public QObject, public Source
 {
     Q_OBJECT
 public:
-    explicit AbstractReader(QIODevice* device, ChannelManager* channelMan,
-                            DataRecorder* recorder, QObject* parent = 0);
-
-    bool recording;                 /// is recording started
+    explicit AbstractReader(QIODevice* device, QObject* parent = 0);
 
     /**
      * Returns a widget to be shown in data format panel when reader
@@ -46,31 +42,16 @@ public:
      */
     virtual QWidget* settingsWidget() = 0;
 
-    /**
-     * Number of channels being read.
-     *
-     * This number may be user selected or automatically determined
-     * from incoming stream.
-     */
-    virtual unsigned numOfChannels() = 0;
-
     /// Reader should only read when enabled. Default state should be
     /// 'disabled'.
-    virtual void enable(bool enabled = true) = 0;
+    virtual void enable(bool enabled = true);
 
-    /**
-     * @brief Starts sending data to recorder.
-     *
-     * @note recorder must have been started!
-     */
-    void startRecording();
-
-    /// Stops recording.
-    void stopRecording();
+    /// None of the current readers support X channel at the moment
+    bool hasX() const final { return false; };
 
 signals:
+    // TODO: should we keep this?
     void numOfChannelsChanged(unsigned);
-    void samplesPerSecondChanged(unsigned);
 
 public slots:
     /**
@@ -79,26 +60,15 @@ public slots:
      * Reader should actually continue reading to keep the
      * synchronization but shouldn't commit data.
      */
-    virtual void pause(bool) = 0;
+    void pause(bool enabled);
 
 protected:
     QIODevice* _device;
-
-    /// Should be called with read data
-    void addData(double* samples, unsigned length);
-
-private:
-    const int SPS_UPDATE_TIMEOUT = 1;  // second
+    bool paused;
 
-    unsigned sampleCount;
-    unsigned samplesPerSecond;
-
-    ChannelManager* _channelMan;
-    DataRecorder* _recorder;
-    QTimer spsTimer;
-
-private slots:
-    void spsTimerTimeout();
+protected slots:
+    /// all derived readers has to override this function
+    virtual void onDataReady() = 0;
 };
 
 #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 © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -24,28 +24,33 @@
 /// If set to this value number of channels is determined from input
 #define NUMOFCHANNELS_AUTO   (0)
 
-AsciiReader::AsciiReader(QIODevice* device, ChannelManager* channelMan,
-                         DataRecorder* recorder, QObject* parent) :
-    AbstractReader(device, channelMan, recorder, parent)
+AsciiReader::AsciiReader(QIODevice* device, QObject* parent) :
+    AbstractReader(device, parent)
 {
     paused = false;
-    discardFirstLine = true;
 
-    _numOfChannels = _settingsWidget.numOfChannels();
-    autoNumOfChannels = (_numOfChannels == NUMOFCHANNELS_AUTO);
+    _numChannels = _settingsWidget.numOfChannels();
+    autoNumOfChannels = (_numChannels == NUMOFCHANNELS_AUTO);
+    delimiter = _settingsWidget.delimiter();
 
     connect(&_settingsWidget, &AsciiReaderSettings::numOfChannelsChanged,
             [this](unsigned value)
             {
-                _numOfChannels = value;
-                autoNumOfChannels = (_numOfChannels == NUMOFCHANNELS_AUTO);
+                _numChannels = value;
+                updateNumChannels(); // TODO: setting numchannels = 0, should remove all buffers
+                                     // do we want this?
+                autoNumOfChannels = (_numChannels == NUMOFCHANNELS_AUTO);
                 if (!autoNumOfChannels)
                 {
                     emit numOfChannelsChanged(value);
                 }
             });
 
-    connect(device, &QIODevice::aboutToClose, [this](){discardFirstLine=true;});
+    connect(&_settingsWidget, &AsciiReaderSettings::delimiterChanged,
+            [this](QChar d)
+            {
+                delimiter = d;
+            });
 }
 
 QWidget* AsciiReader::settingsWidget()
@@ -53,49 +58,38 @@ QWidget* AsciiReader::settingsWidget()
     return &_settingsWidget;
 }
 
-unsigned AsciiReader::numOfChannels()
+unsigned AsciiReader::numChannels() const
 {
+    // TODO: an alternative is to never set _numChannels to '0'
     // do not allow '0'
-    if (_numOfChannels == 0)
-    {
-        return 1;
-    }
-    else
-    {
-        return _numOfChannels;
-    }
+    return _numChannels == 0 ? 1 : _numChannels;
 }
 
-// TODO: this could be a part of AbstractReader
 void AsciiReader::enable(bool enabled)
 {
     if (enabled)
     {
-        discardFirstLine = true;
+        firstReadAfterEnable = true;
         QObject::connect(_device, &QIODevice::readyRead,
                          this, &AsciiReader::onDataReady);
     }
     else
     {
         QObject::disconnect(_device, 0, this, 0);
+        disconnectSinks();
     }
 }
 
-void AsciiReader::pause(bool enabled)
-{
-    paused = enabled;
-}
-
 void AsciiReader::onDataReady()
 {
     while(_device->canReadLine())
     {
-        QByteArray line = _device->readLine();
+        QString line = QString(_device->readLine());
 
         // discard only once when we just started reading
-        if (discardFirstLine)
+        if (firstReadAfterEnable)
         {
-            discardFirstLine = false;
+            firstReadAfterEnable = false;
             continue;
         }
 
@@ -116,51 +110,57 @@ void AsciiReader::onDataReady()
             continue;
         }
 
-        auto separatedValues = line.split(',');
+        const SamplePack* samples = parseLine(line);
+        if (samples != nullptr) {
+            // update number of channels if in auto mode
+            if (autoNumOfChannels ) {
+                unsigned nc = samples->numChannels();
+                if (nc != _numChannels) {
+                    _numChannels = nc;
+                    updateNumChannels();
+                    // TODO: is `numOfChannelsChanged` signal still used?
+                    emit numOfChannelsChanged(nc);
+                }
+            }
 
-        unsigned numReadChannels; // effective number of channels to read
-        unsigned numComingChannels = separatedValues.length();
+            Q_ASSERT(samples->numChannels() == _numChannels);
+
+            // commit data
+            feedOut(*samples);
+        }
+    }
+}
 
-        if (autoNumOfChannels)
-        {
-            // did number of channels changed?
-            if (numComingChannels != _numOfChannels)
-            {
-                _numOfChannels = numComingChannels;
-                emit numOfChannelsChanged(numComingChannels);
-            }
-            numReadChannels = numComingChannels;
-        }
-        else if (numComingChannels >= _numOfChannels)
-        {
-            numReadChannels = _numOfChannels;
-        }
-        else // there is missing channel data
+SamplePack* AsciiReader::parseLine(const QString& line) const
+{
+    auto separatedValues = line.split(delimiter, QString::SkipEmptyParts);
+    unsigned numComingChannels = separatedValues.length();
+
+    // check number of channels (skipped if auto num channels is enabled)
+    if ((!numComingChannels) || (!autoNumOfChannels && numComingChannels != _numChannels))
+    {
+        qWarning() << "Line parsing error: invalid number of channels!";
+        qWarning() << "Read line: " << line;
+        return nullptr;
+    }
+
+    // parse data per channel
+    auto samples = new SamplePack(1, numComingChannels);
+    for (unsigned ci = 0; ci < numComingChannels; ci++)
+    {
+        bool ok;
+        samples->data(ci)[0] = separatedValues[ci].toDouble(&ok);
+        if (!ok)
         {
-            numReadChannels = separatedValues.length();
-            qWarning() << "Incoming data is missing data for some channels!";
+            qWarning() << "Data parsing error for channel: " << ci;
             qWarning() << "Read line: " << line;
-        }
 
-        // parse read line
-        double* channelSamples = new double[_numOfChannels]();
-        for (unsigned ci = 0; ci < numReadChannels; ci++)
-        {
-            bool ok;
-            channelSamples[ci] = separatedValues[ci].toDouble(&ok);
-            if (!ok)
-            {
-                qWarning() << "Data parsing error for channel: " << ci;
-                qWarning() << "Read line: " << line;
-                channelSamples[ci] = 0;
-            }
+            delete samples;
+            return nullptr;
         }
+    }
 
-        // commit data
-        addData(channelSamples, _numOfChannels);
-
-        delete[] channelSamples;
-    }
+    return samples;
 }
 
 void AsciiReader::saveSettings(QSettings* settings)
diff --git a/src/asciireader.h b/src/asciireader.h
--- a/src/asciireader.h
+++ b/src/asciireader.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -21,7 +21,9 @@
 #define ASCIIREADER_H
 
 #include 
+#include 
 
+#include "samplepack.h"
 #include "abstractreader.h"
 #include "asciireadersettings.h"
 
@@ -30,32 +32,32 @@ class AsciiReader : public AbstractReade
     Q_OBJECT
 
 public:
-    explicit AsciiReader(QIODevice* device, ChannelManager* channelMan,
-                         DataRecorder* recorder, QObject *parent = 0);
+    explicit AsciiReader(QIODevice* device, QObject *parent = 0);
     QWidget* settingsWidget();
-    unsigned numOfChannels();
-    void enable(bool enabled = true);
+    unsigned numChannels() const;
+    void enable(bool enabled) override;
     /// Stores settings into a `QSettings`
     void saveSettings(QSettings* settings);
     /// Loads settings from a `QSettings`.
     void loadSettings(QSettings* settings);
 
-public slots:
-    void pause(bool);
-
 private:
     AsciiReaderSettings _settingsWidget;
-    unsigned _numOfChannels;
+    unsigned _numChannels;
     /// number of channels will be determined from incoming data
     unsigned autoNumOfChannels;
-    bool paused;
+    QChar delimiter; ///< selected column delimiter
 
-    // We may have (usually true) started reading in the middle of a
-    // line, so its a better idea to just discard first line.
-    bool discardFirstLine;
+    bool firstReadAfterEnable = false;
 
 private slots:
-    void onDataReady();
+    void onDataReady() override;
+    /**
+     * Parses given line and returns sample pack.
+     *
+     * Returns `nullptr` in case of error.
+     */
+    SamplePack* parseLine(const QString& line) const;
 };
 
 #endif // ASCIIREADER_H
diff --git a/src/asciireadersettings.cpp b/src/asciireadersettings.cpp
--- a/src/asciireadersettings.cpp
+++ b/src/asciireadersettings.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2016 Hasan Yavuz Özderya
+  Copyright © 2017 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -17,20 +17,35 @@
   along with serialplot.  If not, see .
 */
 
+#include 
+#include 
+
 #include "utils.h"
 #include "setting_defines.h"
 
 #include "asciireadersettings.h"
 #include "ui_asciireadersettings.h"
 
-#include 
-
 AsciiReaderSettings::AsciiReaderSettings(QWidget *parent) :
     QWidget(parent),
     ui(new Ui::AsciiReaderSettings)
 {
     ui->setupUi(this);
 
+    auto validator = new QRegularExpressionValidator(QRegularExpression("[^\\d]?"), this);
+    ui->leDelimiter->setValidator(validator);
+
+    connect(ui->rbComma, &QAbstractButton::toggled,
+            this, &AsciiReaderSettings::delimiterToggled);
+    connect(ui->rbSpace, &QAbstractButton::toggled,
+            this, &AsciiReaderSettings::delimiterToggled);
+    connect(ui->rbTab, &QAbstractButton::toggled,
+            this, &AsciiReaderSettings::delimiterToggled);
+    connect(ui->rbOtherDelimiter, &QAbstractButton::toggled,
+            this, &AsciiReaderSettings::delimiterToggled);
+    connect(ui->leDelimiter, &QLineEdit::textChanged,
+            this, &AsciiReaderSettings::customDelimiterChanged);
+
     // Note: if directly connected we get a runtime warning on incompatible signal arguments
     connect(ui->spNumOfChannels, SELECT::OVERLOAD_OF(&QSpinBox::valueChanged),
             [this](int value)
@@ -44,11 +59,51 @@ AsciiReaderSettings::~AsciiReaderSetting
     delete ui;
 }
 
-unsigned AsciiReaderSettings::numOfChannels()
+unsigned AsciiReaderSettings::numOfChannels() const
 {
     return ui->spNumOfChannels->value();
 }
 
+QChar AsciiReaderSettings::delimiter() const
+{
+    if (ui->rbComma->isChecked())
+    {
+        return QChar(',');
+    }
+    else if (ui->rbSpace->isChecked())
+    {
+        return QChar(' ');
+    }
+    else if (ui->rbTab->isChecked())
+    {
+        return QChar('\t');
+    }
+    else                        // rbOther
+    {
+        auto t = ui->leDelimiter->text();
+        return t.isEmpty() ? QChar() : t.at(0);
+    }
+}
+
+void AsciiReaderSettings::delimiterToggled(bool checked)
+{
+    if (!checked) return;
+
+    auto d = delimiter();
+    if (!d.isNull())
+    {
+        emit delimiterChanged(d);
+    }
+}
+
+void AsciiReaderSettings::customDelimiterChanged(const QString text)
+{
+    if (ui->rbOtherDelimiter->isChecked())
+    {
+        if (!text.isEmpty()) emit delimiterChanged(text.at(0));
+    }
+}
+
 void AsciiReaderSettings::saveSettings(QSettings* settings)
 {
     settings->beginGroup(SettingGroup_ASCII);
@@ -58,6 +113,25 @@ void AsciiReaderSettings::saveSettings(Q
     if (numOfChannelsSetting == "0") numOfChannelsSetting = "auto";
     settings->setValue(SG_ASCII_NumOfChannels, numOfChannelsSetting);
 
+    // save delimiter
+    QString delimiterS;
+    if (ui->rbOtherDelimiter->isChecked())
+    {
+        delimiterS = "other";
+    }
+    else if (ui->rbTab->isChecked())
+    {
+        // Note: \t is not correctly loaded
+        delimiterS = "TAB";
+    }
+    else
+    {
+        delimiterS = delimiter();
+    }
+
+    settings->setValue(SG_ASCII_Delimiter, delimiterS);
+    settings->setValue(SG_ASCII_CustomDelimiter, ui->leDelimiter->text());
+
     settings->endGroup();
 }
 
@@ -83,5 +157,26 @@ void AsciiReaderSettings::loadSettings(Q
         }
     }
 
+    // load delimiter
+    auto delimiterS = settings->value(SG_ASCII_Delimiter, delimiter()).toString();
+    auto customDelimiter = settings->value(SG_ASCII_CustomDelimiter, delimiter()).toString();
+    if (!customDelimiter.isEmpty()) ui->leDelimiter->setText(customDelimiter);
+    if (delimiterS == ",")
+    {
+        ui->rbComma->setChecked(true);
+    }
+    else if (delimiterS == " ")
+    {
+        ui->rbSpace->setChecked(true);
+    }
+    else if (delimiterS == "TAB")
+    {
+        ui->rbTab->setChecked(true);
+    }
+    else
+    {
+        ui->rbOtherDelimiter->setChecked(true);
+    }
+
     settings->endGroup();
 }
diff --git a/src/asciireadersettings.h b/src/asciireadersettings.h
--- a/src/asciireadersettings.h
+++ b/src/asciireadersettings.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2016 Hasan Yavuz Özderya
+  Copyright © 2017 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -22,6 +22,7 @@
 
 #include 
 #include 
+#include 
 
 namespace Ui {
 class AsciiReaderSettings;
@@ -35,7 +36,8 @@ public:
     explicit AsciiReaderSettings(QWidget *parent = 0);
     ~AsciiReaderSettings();
 
-    unsigned numOfChannels();
+    unsigned numOfChannels() const;
+    QChar delimiter() const;
     /// Stores settings into a `QSettings`
     void saveSettings(QSettings* settings);
     /// Loads settings from a `QSettings`.
@@ -43,9 +45,15 @@ public:
 
 signals:
     void numOfChannelsChanged(unsigned);
+    /// Signaled only with a valid delimiter
+    void delimiterChanged(QChar);
 
 private:
     Ui::AsciiReaderSettings *ui;
+
+private slots:
+    void delimiterToggled(bool checked);
+    void customDelimiterChanged(const QString text);
 };
 
 #endif // ASCIIREADERSETTINGS_H
diff --git a/src/asciireadersettings.ui b/src/asciireadersettings.ui
--- a/src/asciireadersettings.ui
+++ b/src/asciireadersettings.ui
@@ -6,14 +6,17 @@
    
     0
     0
-    414
+    493
     171
    
   
   
    Form
   
-  
+  
+   
+    QFormLayout::ExpandingFieldsGrow
+   
    
     0
    
@@ -26,68 +29,105 @@
    
     0
    
-   - 
+   
- 
+    
+     
+      Number Of Channels:
+     
+    
+   
 
+   - 
+    
+     
+      
+       60
+       0
+      
+     
+     
+      Select number of channels or set to 0 for Auto (determined from incoming data)
+     
+     
+      Auto
+     
+     
+      false
+     
+     
+      0
+     
+     
+      32
+     
+    
+   
 
+   - 
+    
+     
+      Column Delimiter:
+     
+    
+   
 
+   - 
     
      
- 
-      
+      
        
-        Number Of Channels:
+        comma
+       
+       
+        true
        
       
      
 
      - 
-      
-       
-        
-         60
-         0
-        
-       
-       
-        Select number of channels or set to 0 for Auto (determined from incoming data)
+      
+       
+        space
        
-       
-        Auto
-       
-       
-        false
+      
+     
 
+     - 
+      
+       
+        tab
        
-       
-        0
-       
-       
-        32
+      
+     
 
+     - 
+      
+       
+        other:
        
       
      
 
      - 
-      
-       
-        Qt::Horizontal
+      
+       
+        
+         0
+         0
+        
        
-       
+       
         
-         1
-         20
+         30
+         16777215
         
        
-      
+       
+        Enter a custom delimiter character
+       
+       
+        
+       
+       
+        |
+       
+      
      
 
     
     
-   - 
-    
-     
-      Qt::Vertical
-     
-     
-      
-       20
-       1
-      
-     
-    
-   
 
    
  
  
diff --git a/src/barchart.cpp b/src/barchart.cpp
new file mode 100644
--- /dev/null
+++ b/src/barchart.cpp
@@ -0,0 +1,96 @@
+/*
+  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 
+#include 
+
+#include "barchart.h"
+
+BarChart::BarChart(const Stream* stream)
+{
+    _stream = stream;
+    setSpacing(0);
+}
+
+void BarChart::resample()
+{
+    setSamples(chartData());
+}
+
+QVector BarChart::chartData() const
+{
+    unsigned numChannels = _stream->numChannels();
+    unsigned numSamples = _stream->numSamples();
+    QVector data(numChannels);
+    for (unsigned i = 0; i < numChannels; i++)
+    {
+        data[i] = _stream->channel(i)->yData()->sample(numSamples-1);
+    }
+    return data;
+}
+
+QwtColumnSymbol* BarChart::specialSymbol(int sampleIndex, const QPointF& sample) const
+{
+    unsigned numChannels = _stream->numChannels();
+    if (sampleIndex < 0 || sampleIndex > (int) numChannels)
+    {
+        return NULL;
+    }
+
+    auto color = _stream->channel(sampleIndex)->color();
+
+    QwtColumnSymbol* symbol = new QwtColumnSymbol(QwtColumnSymbol::Box);
+    symbol->setLineWidth(1);
+    symbol->setFrameStyle(QwtColumnSymbol::Plain);
+    symbol->setPalette(QPalette(color));
+
+    return symbol;
+}
+
+void BarChart::drawSample(
+        QPainter *painter, const QwtScaleMap &xMap, const QwtScaleMap &yMap,
+        const QRectF &canvasRect, const QwtInterval &boundingInterval,
+        int index, const QPointF &sample ) const
+{
+    QwtPlotBarChart::drawSample(painter, xMap, yMap, canvasRect, boundingInterval, index, sample);
+
+    // calculate bar size
+    const double barWidth = sampleWidth(xMap, canvasRect.width(),
+                                        boundingInterval.width(), sample.y());
+    const double x = xMap.transform( sample.x() );
+    const double x1 = x - 0.5 * barWidth;
+    // const double x2 = x + 0.5 * barWidth;
+
+    const double y1 = yMap.transform(baseline());
+    const double y2 = yMap.transform(sample.y());
+    const double barHeight = fabs(y2 - y1);
+
+    // create and calculate text size
+    const auto valueStr = QString::number(sample.y());
+    auto valueText = QwtText(valueStr, QwtText::PlainText);
+    const auto textSize = valueText.textSize();
+    auto r = QRectF(x1, y2, barWidth, textSize.height());
+    if (y2 > y1) r.moveBottom(y2);
+
+    // display text if there is enough space
+    if (barHeight >= textSize.height() && barWidth >= textSize.width())
+    {
+        valueText.draw(painter, r);
+    }
+}
diff --git a/src/barchart.h b/src/barchart.h
new file mode 100644
--- /dev/null
+++ b/src/barchart.h
@@ -0,0 +1,47 @@
+/*
+  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 .
+*/
+
+#ifndef BARCHART_H
+#define BARCHART_H
+
+#include 
+#include 
+#include "stream.h"
+
+class BarChart : public QwtPlotBarChart
+{
+public:
+    explicit BarChart(const Stream* stream);
+
+    void resample();
+    QwtColumnSymbol* specialSymbol(int sampleIndex, const QPointF&) const;
+
+    void drawSample(
+        QPainter *painter, const QwtScaleMap &xMap, const QwtScaleMap &yMap,
+        const QRectF &canvasRect, const QwtInterval &boundingInterval,
+        int index, const QPointF &sample ) const;
+
+private:
+    const Stream* _stream;
+
+    QVector chartData() const;
+};
+
+
+#endif // BARCHART_H
diff --git a/src/barplot.cpp b/src/barplot.cpp
new file mode 100644
--- /dev/null
+++ b/src/barplot.cpp
@@ -0,0 +1,74 @@
+/*
+  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 "barplot.h"
+#include "barscaledraw.h"
+#include "utils.h"
+
+BarPlot::BarPlot(Stream* stream, PlotMenu* menu, QWidget* parent) :
+    QwtPlot(parent), _menu(menu), barChart(stream)
+{
+    _stream = stream;
+    barChart.attach(this);
+    setAxisMaxMinor(QwtPlot::xBottom, 0);
+    setAxisScaleDraw(QwtPlot::xBottom, new BarScaleDraw(stream));
+
+    update();
+    connect(_stream, &Stream::dataAdded, this, &BarPlot::update);
+    connect(_stream, &Stream::numChannelsChanged, this, &BarPlot::update);
+
+    // connect to menu
+    connect(&menu->darkBackgroundAction, SELECT::OVERLOAD_OF(&QAction::toggled),
+            this, &BarPlot::darkBackground);
+    darkBackground(menu->darkBackgroundAction.isChecked());
+}
+
+void BarPlot::update()
+{
+    // Note: -0.99 is used instead of -1 to handle the case of `numOfChannels==1`
+    setAxisScale(QwtPlot::xBottom, 0, _stream->numChannels()-0.99, 1);
+    barChart.resample();
+    replot();
+}
+
+void BarPlot::setYAxis(bool autoScaled, double yMin, double yMax)
+{
+    if (autoScaled)
+    {
+        setAxisAutoScale(QwtPlot::yLeft);
+    }
+    else
+    {
+        setAxisScale(QwtPlot::yLeft, yMin, yMax);
+    }
+}
+
+
+void BarPlot::darkBackground(bool enabled)
+{
+    if (enabled)
+    {
+        setCanvasBackground(QBrush(Qt::black));
+    }
+    else
+    {
+        setCanvasBackground(QBrush(Qt::white));
+    }
+    replot();
+}
diff --git a/src/barplot.h b/src/barplot.h
new file mode 100644
--- /dev/null
+++ b/src/barplot.h
@@ -0,0 +1,55 @@
+/*
+  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 .
+*/
+
+#ifndef BARPLOT_H
+#define BARPLOT_H
+
+#include 
+
+#include "stream.h"
+#include "plotmenu.h"
+#include "barchart.h"
+
+class BarPlot : public QwtPlot
+{
+    Q_OBJECT
+
+public:
+    explicit BarPlot(Stream* stream,
+                     PlotMenu* menu,
+                     QWidget* parent = 0);
+
+public slots:
+    /// Set the Y axis
+    void setYAxis(bool autoScaled, double yMin = 0, double yMax = 1);
+    /// Enable/disable dark background
+    void darkBackground(bool enabled);
+
+private:
+    Stream* _stream;
+    PlotMenu* _menu;
+    BarChart barChart;
+
+    QVector chartData() const;
+
+private slots:
+    void update();
+};
+
+#endif // BARPLOT_H
diff --git a/src/barscaledraw.cpp b/src/barscaledraw.cpp
new file mode 100644
--- /dev/null
+++ b/src/barscaledraw.cpp
@@ -0,0 +1,51 @@
+/*
+  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 "barscaledraw.h"
+
+#include 
+
+BarScaleDraw::BarScaleDraw(const Stream* stream)
+{
+    _stream = stream;
+    enableComponent(Backbone, false);
+    setLabelRotation(-90);
+    setLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+
+    QObject::connect(_stream, &Stream::channelNameChanged,
+            [this]()
+            {
+                invalidateCache();
+            });
+}
+
+QwtText BarScaleDraw::label(double value) const
+{
+    int index = value;
+    unsigned numChannels = _stream->numChannels();
+
+    if (index >=0 && index < (int) numChannels)
+    {
+        return _stream->channel(index)->name();
+    }
+    else
+    {
+        return QString("");
+    }
+}
diff --git a/src/barscaledraw.h b/src/barscaledraw.h
new file mode 100644
--- /dev/null
+++ b/src/barscaledraw.h
@@ -0,0 +1,39 @@
+/*
+  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 .
+*/
+
+#ifndef BARSCALEDRAW_H
+#define BARSCALEDRAW_H
+
+#include 
+#include 
+#include 
+
+#include "stream.h"
+
+class BarScaleDraw : public QwtScaleDraw
+{
+public:
+    explicit BarScaleDraw(const Stream* stream);
+    QwtText label(double value) const;
+
+private:
+    const Stream* _stream;
+};
+
+#endif // BARSCALEDRAW_H
diff --git a/src/binarystreamreader.cpp b/src/binarystreamreader.cpp
--- a/src/binarystreamreader.cpp
+++ b/src/binarystreamreader.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -23,19 +23,16 @@
 #include "binarystreamreader.h"
 #include "floatswap.h"
 
-BinaryStreamReader::BinaryStreamReader(QIODevice* device, ChannelManager* channelMan,
-                                       DataRecorder* recorder, QObject* parent) :
-    AbstractReader(device, channelMan, recorder, parent)
+BinaryStreamReader::BinaryStreamReader(QIODevice* device, QObject* parent) :
+    AbstractReader(device, parent)
 {
     paused = false;
     skipByteRequested = false;
     skipSampleRequested = false;
 
-    _numOfChannels = _settingsWidget.numOfChannels();
+    _numChannels = _settingsWidget.numOfChannels();
     connect(&_settingsWidget, &BinaryStreamReaderSettings::numOfChannelsChanged,
-            this, &BinaryStreamReader::numOfChannelsChanged);
-    connect(&_settingsWidget, &BinaryStreamReaderSettings::numOfChannelsChanged,
-            this, &BinaryStreamReader::onNumOfChannelsChanged);
+                     this, &BinaryStreamReader::onNumOfChannelsChanged);
 
     // initial number format selection
     onNumberFormatChanged(_settingsWidget.numberFormat());
@@ -60,27 +57,9 @@ QWidget* BinaryStreamReader::settingsWid
     return &_settingsWidget;
 }
 
-unsigned BinaryStreamReader::numOfChannels()
-{
-    return _numOfChannels;
-}
-
-void BinaryStreamReader::enable(bool enabled)
+unsigned BinaryStreamReader::numChannels() const
 {
-    if (enabled)
-    {
-        QObject::connect(_device, &QIODevice::readyRead,
-                         this, &BinaryStreamReader::onDataReady);
-    }
-    else
-    {
-        QObject::disconnect(_device, 0, this, 0);
-    }
-}
-
-void BinaryStreamReader::pause(bool enabled)
-{
-    paused = enabled;
+    return _numChannels;
 }
 
 void BinaryStreamReader::onNumberFormatChanged(NumberFormat numberFormat)
@@ -123,17 +102,19 @@ void BinaryStreamReader::onNumberFormatC
 
 void BinaryStreamReader::onNumOfChannelsChanged(unsigned value)
 {
-    _numOfChannels = value;
+    _numChannels = value;
+    updateNumChannels();
+    emit numOfChannelsChanged(value);
 }
 
 void BinaryStreamReader::onDataReady()
 {
     // a package is a set of channel data like {CHAN0_SAMPLE, CHAN1_SAMPLE...}
-    int packageSize = sampleSize * _numOfChannels;
+    int packageSize = sampleSize * _numChannels;
     int bytesAvailable = _device->bytesAvailable();
 
     // skip 1 byte if requested
-    if (bytesAvailable > 0 && skipByteRequested)
+    if (skipByteRequested && bytesAvailable > 0)
     {
         _device->read(1);
         skipByteRequested = false;
@@ -141,11 +122,11 @@ void BinaryStreamReader::onDataReady()
     }
 
     // skip 1 sample (channel) if requested
-    if (bytesAvailable >= (int) sampleSize && skipSampleRequested)
+    if (skipSampleRequested && bytesAvailable >= (int) sampleSize)
     {
         _device->read(sampleSize);
         skipSampleRequested = false;
-        bytesAvailable--;
+        bytesAvailable -= sampleSize;
     }
 
     if (bytesAvailable < packageSize) return;
@@ -160,20 +141,16 @@ void BinaryStreamReader::onDataReady()
         return;
     }
 
-    double* channelSamples = new double[numOfPackagesToRead*_numOfChannels];
-
+    // actual reading
+    SamplePack samples(numOfPackagesToRead, _numChannels);
     for (int i = 0; i < numOfPackagesToRead; i++)
     {
-        for (unsigned int ci = 0; ci < _numOfChannels; ci++)
+        for (unsigned int ci = 0; ci < _numChannels; ci++)
         {
-            // channelSamples[ci].replace(i, (this->*readSample)());
-            channelSamples[ci*numOfPackagesToRead+i] = (this->*readSample)();
+            samples.data(ci)[i] = (this->*readSample)();
         }
     }
-
-    addData(channelSamples, numOfPackagesToRead*_numOfChannels);
-
-    delete[] channelSamples;
+    feedOut(samples);
 }
 
 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 © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -34,24 +34,18 @@ class BinaryStreamReader : public Abstra
 {
     Q_OBJECT
 public:
-    explicit BinaryStreamReader(QIODevice* device, ChannelManager* channelMan,
-                                DataRecorder* recorder, QObject *parent = 0);
+    explicit BinaryStreamReader(QIODevice* device, QObject *parent = 0);
     QWidget* settingsWidget();
-    unsigned numOfChannels();
-    void enable(bool enabled = true);
+    unsigned numChannels() const;
     /// Stores settings into a `QSettings`
     void saveSettings(QSettings* settings);
     /// Loads settings from a `QSettings`.
     void loadSettings(QSettings* settings);
 
-public slots:
-    void pause(bool);
-
 private:
     BinaryStreamReaderSettings _settingsWidget;
-    unsigned _numOfChannels;
+    unsigned _numChannels;
     unsigned sampleSize;
-    bool paused;
     bool skipByteRequested;
     bool skipSampleRequested;
 
@@ -69,7 +63,7 @@ private:
 private slots:
     void onNumberFormatChanged(NumberFormat numberFormat);
     void onNumOfChannelsChanged(unsigned value);
-    void onDataReady();
+    void onDataReady() override;
 };
 
 #endif // BINARYSTREAMREADER_H
diff --git a/src/channelinfomodel.cpp b/src/channelinfomodel.cpp
--- a/src/channelinfomodel.cpp
+++ b/src/channelinfomodel.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -68,6 +68,7 @@ ChannelInfoModel::ChannelInfoModel(unsig
 ChannelInfoModel::ChannelInfoModel(const ChannelInfoModel& other) :
     ChannelInfoModel(other.rowCount(), other.parent())
 {
+    // TODO: why not set (copy) info list directly instead?
     for (int i = 0; i < other.rowCount(); i++)
     {
         setData(index(i, COLUMN_NAME),
@@ -76,9 +77,24 @@ ChannelInfoModel::ChannelInfoModel(const
         setData(index(i, COLUMN_NAME),
                 other.data(other.index(i, COLUMN_NAME), Qt::ForegroundRole),
                 Qt::ForegroundRole);
+
         setData(index(i, COLUMN_VISIBILITY),
                 other.data(other.index(i, COLUMN_VISIBILITY), Qt::CheckStateRole),
                 Qt::CheckStateRole);
+
+        setData(index(i, COLUMN_GAIN),
+                other.data(other.index(i, COLUMN_GAIN), Qt::CheckStateRole),
+                Qt::CheckStateRole);
+        setData(index(i, COLUMN_GAIN),
+                other.data(other.index(i, COLUMN_GAIN), Qt::EditRole),
+                Qt::EditRole);
+
+        setData(index(i, COLUMN_OFFSET),
+                other.data(other.index(i, COLUMN_OFFSET), Qt::CheckStateRole),
+                Qt::CheckStateRole);
+        setData(index(i, COLUMN_OFFSET),
+                other.data(other.index(i, COLUMN_OFFSET), Qt::EditRole),
+                Qt::EditRole);
     }
 }
 
@@ -96,6 +112,10 @@ ChannelInfoModel::ChannelInfo::ChannelIn
     name = tr("Channel %1").arg(index + 1);
     visibility = true;
     color = colors[index % NUMOF_COLORS];
+    gain = 1.0;
+    offset = 0.0;
+    gainEn = false;
+    offsetEn = false;
 }
 
 QString ChannelInfoModel::name(unsigned i) const
@@ -113,6 +133,26 @@ bool ChannelInfoModel::isVisible(unsigne
     return infos[i].visibility;
 }
 
+bool ChannelInfoModel::gainEn (unsigned i) const
+{
+    return infos[i].gainEn;
+}
+
+double ChannelInfoModel::gain (unsigned i) const
+{
+    return infos[i].gain;
+}
+
+bool ChannelInfoModel::offsetEn (unsigned i) const
+{
+    return infos[i].offsetEn;
+}
+
+double ChannelInfoModel::offset (unsigned i) const
+{
+    return infos[i].offset;
+}
+
 QStringList ChannelInfoModel::channelNames() const
 {
     QStringList r;
@@ -143,6 +183,10 @@ Qt::ItemFlags ChannelInfoModel::flags(co
     {
         return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren | Qt::ItemIsSelectable;
     }
+    else if (index.column() == COLUMN_GAIN || index.column() == COLUMN_OFFSET)
+    {
+        return Qt::ItemIsEditable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren | Qt::ItemIsSelectable;
+    }
 
     return Qt::NoItemFlags;
 }
@@ -155,27 +199,51 @@ QVariant ChannelInfoModel::data(const QM
         return QVariant();
     }
 
+    auto &info = infos[index.row()];
+
     // get color
     if (role == Qt::ForegroundRole)
     {
-        return infos[index.row()].color;
+        return info.color;
     }
 
-    // get name
+    // name
     if (index.column() == COLUMN_NAME)
     {
         if (role == Qt::DisplayRole || role == Qt::EditRole)
         {
-            return QVariant(infos[index.row()].name);
+            return QVariant(info.name);
         }
-    } // get visibility
+    } // visibility
     else if (index.column() == COLUMN_VISIBILITY)
     {
         if (role == Qt::CheckStateRole)
         {
-            bool visible = infos[index.row()].visibility;
+            bool visible = info.visibility;
             return visible ? Qt::Checked : Qt::Unchecked;
         }
+    } // gain
+    else if (index.column() == COLUMN_GAIN)
+    {
+        if (role == Qt::CheckStateRole)
+        {
+            return info.gainEn ? Qt::Checked : Qt::Unchecked;
+        }
+        else if (role == Qt::DisplayRole || role == Qt::EditRole)
+        {
+            return QVariant(info.gain);
+        }
+    } // offset
+    else if (index.column() == COLUMN_OFFSET)
+    {
+        if (role == Qt::CheckStateRole)
+        {
+            return info.offsetEn ? Qt::Checked : Qt::Unchecked;
+        }
+        else if (role == Qt::DisplayRole || role == Qt::EditRole)
+        {
+            return QVariant(info.offset);
+        }
     }
 
     return QVariant();
@@ -195,6 +263,14 @@ QVariant ChannelInfoModel::headerData(in
             {
                 return tr("Visible");
             }
+            else if (section == COLUMN_GAIN)
+            {
+                return tr("Gain");
+            }
+            else if (section == COLUMN_OFFSET)
+            {
+                return tr("Offset");
+            }
         }
     }
     else                        // vertical
@@ -216,22 +292,24 @@ bool ChannelInfoModel::setData(const QMo
         return false;
     }
 
+    auto &info = infos[index.row()];
+
     // set color
     if (role == Qt::ForegroundRole)
     {
-        infos[index.row()].color = value.value();
+        info.color = value.value();
         emit dataChanged(index, index, QVector({Qt::ForegroundRole}));
         return true;
     }
 
     // set name
+    bool r = false;
     if (index.column() == COLUMN_NAME)
     {
         if (role == Qt::DisplayRole || role == Qt::EditRole)
         {
-            infos[index.row()].name = value.toString();
-            emit dataChanged(index, index, QVector({role}));
-            return true;
+            info.name = value.toString();
+            r = true;
         }
     } // set visibility
     else if (index.column() == COLUMN_VISIBILITY)
@@ -239,14 +317,47 @@ bool ChannelInfoModel::setData(const QMo
         if (role == Qt::CheckStateRole)
         {
             bool checked = value.toInt() == Qt::Checked;
-            infos[index.row()].visibility = checked;
-            emit dataChanged(index, index, QVector({role}));
-            return true;
+            info.visibility = checked;
+            r = true;
+        }
+    }
+    else if (index.column() == COLUMN_GAIN)
+    {
+        if (role == Qt::DisplayRole || role == Qt::EditRole)
+        {
+            info.gain = value.toDouble();
+            r = true;
+        }
+        else if (role == Qt::CheckStateRole)
+        {
+            bool checked = value.toInt() == Qt::Checked;
+            info.gainEn = checked;
+            if (_gainOrOffsetEn != checked) updateGainOrOffsetEn();
+            r = true;
+        }
+    }
+    else if (index.column() == COLUMN_OFFSET)
+    {
+        if (role == Qt::DisplayRole || role == Qt::EditRole)
+        {
+            info.offset = value.toDouble();
+            r = true;
+        }
+        else if (role == Qt::CheckStateRole)
+        {
+            bool checked = value.toInt() == Qt::Checked;
+            info.offsetEn = checked;
+            if (_gainOrOffsetEn != checked) updateGainOrOffsetEn();
+            r = true;
         }
     }
 
-    // invalid index/role
-    return false;
+    if (r)
+    {
+        emit dataChanged(index, index, QVector({role}));
+    }
+
+    return r;
 }
 
 void ChannelInfoModel::setNumOfChannels(unsigned number)
@@ -285,6 +396,7 @@ void ChannelInfoModel::setNumOfChannels(
     }
 
     _numOfChannels = number;
+    updateGainOrOffsetEn();
 
     if (isInserting)
     {
@@ -306,11 +418,13 @@ void ChannelInfoModel::resetInfos()
     endResetModel();
 }
 
+// TODO: fix repetitive code, ChannelInfoModel::reset* functions
 void ChannelInfoModel::resetNames()
 {
     beginResetModel();
     for (unsigned ci = 0; (int) ci < infos.length(); ci++)
     {
+        // TODO: do not create a full object every time (applies to other reset methods as well)
         infos[ci].name = ChannelInfo(ci).name;
     }
     endResetModel();
@@ -326,17 +440,56 @@ void ChannelInfoModel::resetColors()
     endResetModel();
 }
 
-void ChannelInfoModel::resetVisibility()
+void ChannelInfoModel::resetVisibility(bool visible)
+{
+    beginResetModel();
+    for (unsigned ci = 0; (int) ci < infos.length(); ci++)
+    {
+        infos[ci].visibility = visible;
+    }
+    endResetModel();
+}
+
+void ChannelInfoModel::resetGains()
 {
     beginResetModel();
     for (unsigned ci = 0; (int) ci < infos.length(); ci++)
     {
-        infos[ci].visibility = true;
+        infos[ci].gain = ChannelInfo(ci).gain;
+        infos[ci].gainEn = ChannelInfo(ci).gainEn;
     }
+    updateGainOrOffsetEn();
     endResetModel();
 }
 
-void ChannelInfoModel::saveSettings(QSettings* settings)
+void ChannelInfoModel::resetOffsets()
+{
+    beginResetModel();
+    for (unsigned ci = 0; (int) ci < infos.length(); ci++)
+    {
+        infos[ci].offset = ChannelInfo(ci).offset;
+        infos[ci].offsetEn = ChannelInfo(ci).offsetEn;
+    }
+    updateGainOrOffsetEn();
+    endResetModel();
+}
+
+bool ChannelInfoModel::gainOrOffsetEn() const
+{
+    return _gainOrOffsetEn;
+}
+
+void ChannelInfoModel::updateGainOrOffsetEn()
+{
+    _gainOrOffsetEn = false;
+    for (unsigned ci = 0; ci < _numOfChannels; ci++)
+    {
+        auto& info = infos[ci];
+        _gainOrOffsetEn |= (info.gainEn || info.offsetEn);
+    }
+}
+
+void ChannelInfoModel::saveSettings(QSettings* settings) const
 {
     settings->beginGroup(SettingGroup_Channels);
     settings->beginWriteArray(SG_Channels_Channel);
@@ -345,9 +498,14 @@ void ChannelInfoModel::saveSettings(QSet
     for (unsigned ci = 0; (int) ci < infos.length(); ci++)
     {
         settings->setArrayIndex(ci);
-        settings->setValue(SG_Channels_Name, infos[ci].name);
-        settings->setValue(SG_Channels_Color, infos[ci].color);
-        settings->setValue(SG_Channels_Visible, infos[ci].visibility);
+        auto& info = infos[ci];
+        settings->setValue(SG_Channels_Name, info.name);
+        settings->setValue(SG_Channels_Color, info.color);
+        settings->setValue(SG_Channels_Visible, info.visibility);
+        settings->setValue(SG_Channels_Gain, info.gain);
+        settings->setValue(SG_Channels_GainEn, info.gainEn);
+        settings->setValue(SG_Channels_Offset, info.offset);
+        settings->setValue(SG_Channels_OffsetEn, info.offsetEn);
     }
 
     settings->endArray();
@@ -364,9 +522,13 @@ void ChannelInfoModel::loadSettings(QSet
         settings->setArrayIndex(ci);
 
         ChannelInfo chanInfo(ci);
-        chanInfo.name       = settings->value(SG_Channels_Name, chanInfo.name).toString();
-        chanInfo.color      = settings->value(SG_Channels_Color, chanInfo.color).value();
-        chanInfo.visibility = settings->value(SG_Channels_Visible, true).toBool();
+        chanInfo.name       = settings->value(SG_Channels_Name     , chanInfo.name).toString();
+        chanInfo.color      = settings->value(SG_Channels_Color    , chanInfo.color).value();
+        chanInfo.visibility = settings->value(SG_Channels_Visible  , chanInfo.visibility).toBool();
+        chanInfo.gain       = settings->value(SG_Channels_Gain     , chanInfo.gain).toDouble();
+        chanInfo.gainEn     = settings->value(SG_Channels_GainEn   , chanInfo.gainEn).toBool();
+        chanInfo.offset     = settings->value(SG_Channels_Offset   , chanInfo.offset).toDouble();
+        chanInfo.offsetEn   = settings->value(SG_Channels_OffsetEn , chanInfo.offsetEn).toBool();
 
         if ((int) ci < infos.size())
         {
@@ -385,6 +547,8 @@ void ChannelInfoModel::loadSettings(QSet
         }
     }
 
+    updateGainOrOffsetEn();
+
     settings->endArray();
     settings->endGroup();
 }
diff --git a/src/channelinfomodel.h b/src/channelinfomodel.h
--- a/src/channelinfomodel.h
+++ b/src/channelinfomodel.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -34,7 +34,9 @@ public:
     {
         COLUMN_NAME = 0,
         COLUMN_VISIBILITY,
-        COLUMN_COUNT
+        COLUMN_GAIN,
+        COLUMN_OFFSET,
+        COLUMN_COUNT            // MUST be last
     };
 
     explicit ChannelInfoModel(unsigned numberOfChannels, QObject *parent = 0);
@@ -44,6 +46,12 @@ public:
     QString name     (unsigned i) const;
     QColor  color    (unsigned i) const;
     bool    isVisible(unsigned i) const;
+    bool    gainEn   (unsigned i) const;
+    double  gain     (unsigned i) const;
+    bool    offsetEn (unsigned i) const;
+    double  offset   (unsigned i) const;
+    /// Returns true if any of the channels have gain or offset enabled
+    bool gainOrOffsetEn() const;
     /// Returns a list of channel names
     QStringList channelNames() const;
 
@@ -57,7 +65,7 @@ public:
 
     void setNumOfChannels(unsigned number);
     /// Stores all channel info into a `QSettings`
-    void saveSettings(QSettings* settings);
+    void saveSettings(QSettings* settings) const;
     /// Loads all channel info from a `QSettings`.
     void loadSettings(QSettings* settings);
 
@@ -68,8 +76,12 @@ public slots:
     void resetNames();
     /// reset all channel colors
     void resetColors();
+    /// reset all channel gain values and disables gains
+    void resetGains();
+    /// reset all channel offset values and disables offsets
+    void resetOffsets();
     /// reset visibility
-    void resetVisibility();
+    void resetVisibility(bool visible);
 
 private:
     struct ChannelInfo
@@ -79,6 +91,8 @@ private:
         QString name;
         bool visibility;
         QColor color;
+        double gain, offset;
+        bool gainEn, offsetEn;
     };
 
     unsigned _numOfChannels;     ///< @note this is not necessarily the length of `infos`
@@ -88,6 +102,16 @@ private:
      * remember user entered info (names, colors etc.).
      */
     QList infos;
+
+    /**
+     * Cache for gain and offset enabled variables of channels. If gain and/or
+     * offset is not enabled for *any* of the channels this is false otherwise
+     * true.
+     */
+    bool _gainOrOffsetEn;
+
+    /// Updates `_gainOrOffsetEn` by scanning all channel infos.
+    void updateGainOrOffsetEn();
 };
 
 #endif // CHANNELINFOMODEL_H
diff --git a/src/channelmanager.cpp b/src/channelmanager.cpp
deleted file mode 100644
--- a/src/channelmanager.cpp
+++ /dev/null
@@ -1,193 +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 
-
-#include 
-
-#include "channelmanager.h"
-#include "setting_defines.h"
-
-ChannelManager::ChannelManager(unsigned numberOfChannels, unsigned numberOfSamples, QObject *parent) :
-    QObject(parent),
-    _infoModel(numberOfChannels)
-{
-    _numOfChannels = numberOfChannels;
-    _numOfSamples = numberOfSamples;
-    _paused = false;
-
-    for (unsigned int i = 0; i < numberOfChannels; i++)
-    {
-        channelBuffers.append(new FrameBuffer(numberOfSamples));
-    }
-
-    connect(&_infoModel, &ChannelInfoModel::dataChanged,
-            this, &ChannelManager::onChannelInfoChanged);
-}
-
-ChannelManager::~ChannelManager()
-{
-    for (auto buffer : channelBuffers)
-    {
-        delete buffer;
-    }
-}
-
-unsigned ChannelManager::numOfChannels()
-{
-    return channelBuffers.size();
-}
-
-unsigned ChannelManager::numOfSamples()
-{
-    return _numOfSamples;
-}
-
-void ChannelManager::setNumOfChannels(unsigned number)
-{
-    unsigned int oldNum = channelBuffers.size();
-
-    if (number > oldNum)
-    {
-        // add new channels
-        for (unsigned int i = 0; i < number - oldNum; i++)
-        {
-            channelBuffers.append(new FrameBuffer(_numOfSamples));
-        }
-    }
-    else if(number < oldNum)
-    {
-        // remove channels
-        for (unsigned int i = oldNum-1; i > number-1; i--)
-        {
-            delete channelBuffers.takeLast();
-        }
-    }
-
-    _numOfChannels = number;
-    _infoModel.setNumOfChannels(number);
-
-    emit numOfChannelsChanged(number);
-}
-
-void ChannelManager::setNumOfSamples(unsigned number)
-{
-    _numOfSamples = number;
-
-    for (int ci = 0; ci < channelBuffers.size(); ci++)
-    {
-        channelBuffers[ci]->resize(_numOfSamples);
-    }
-
-    emit numOfSamplesChanged(number);
-}
-
-void ChannelManager::pause(bool paused)
-{
-    _paused = paused;
-}
-
-FrameBuffer* ChannelManager::channelBuffer(unsigned channel)
-{
-    return channelBuffers[channel];
-}
-
-ChannelInfoModel* ChannelManager::infoModel()
-{
-    return &_infoModel;
-}
-
-QString ChannelManager::channelName(unsigned channel)
-{
-    return _infoModel.data(_infoModel.index(channel, ChannelInfoModel::COLUMN_NAME),
-                           Qt::DisplayRole).toString();
-}
-
-QStringList ChannelManager::channelNames()
-{
-    QStringList list;
-    for (unsigned ci = 0; ci < _numOfChannels; ci++)
-    {
-        list << channelName(ci);
-    }
-    return list;
-}
-
-void ChannelManager::onChannelInfoChanged(const QModelIndex & topLeft,
-                                          const QModelIndex & bottomRight,
-                                          const QVector & roles)
-{
-    int start = topLeft.row();
-    int end = bottomRight.row();
-    int col = topLeft.column();
-
-    for (int ci = start; ci <= end; ci++)
-    {
-        for (auto role : roles)
-        {
-            switch (role)
-            {
-                case Qt::EditRole:
-                    if (col == ChannelInfoModel::COLUMN_NAME)
-                    {
-                        emit channelNameChanged(ci, channelName(ci));
-                    }
-                    break;
-                case Qt::ForegroundRole:
-                    if (col == ChannelInfoModel::COLUMN_NAME)
-                    {
-                        // TODO: emit channel color changed
-                    }
-                    break;
-                case Qt::CheckStateRole:
-                    if (col == ChannelInfoModel::COLUMN_VISIBILITY)
-                    {
-                        // TODO: emit visibility
-                    }
-                    break;
-            }
-        }
-        // emit channelNameChanged(i, channelName(i));
-    }
-}
-
-void ChannelManager::addData(double* data, unsigned size)
-{
-    Q_ASSERT(size % _numOfChannels == 0);
-
-    if (_paused) return;
-
-    int n = size / _numOfChannels;
-    for (unsigned ci = 0; ci < _numOfChannels; ci++)
-    {
-        channelBuffers[ci]->addSamples(&data[ci*n], n);
-    }
-
-    emit dataAdded();
-}
-
-void ChannelManager::saveSettings(QSettings* settings)
-{
-    _infoModel.saveSettings(settings);
-}
-
-void ChannelManager::loadSettings(QSettings* settings)
-{
-    _infoModel.loadSettings(settings);
-}
diff --git a/src/channelmanager.h b/src/channelmanager.h
deleted file mode 100644
--- a/src/channelmanager.h
+++ /dev/null
@@ -1,93 +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 .
-*/
-
-#ifndef CHANNELMANAGER_H
-#define CHANNELMANAGER_H
-
-#include 
-#include 
-#include 
-#include 
-#include 
-
-#include "framebuffer.h"
-#include "channelinfomodel.h"
-
-class ChannelManager : public QObject
-{
-    Q_OBJECT
-public:
-    explicit ChannelManager(unsigned numberOfChannels, unsigned numberOfSamples, QObject *parent = 0);
-    ~ChannelManager();
-
-    unsigned numOfChannels();
-    unsigned numOfSamples();
-    FrameBuffer* channelBuffer(unsigned channel);
-    QString channelName(unsigned channel);
-    /// Stores channel names into a `QSettings`
-    void saveSettings(QSettings* settings);
-    /// Loads channel names from a `QSettings`.
-    void loadSettings(QSettings* settings);
-    /// Returns a model that manages channel information (name, color etc)
-    ChannelInfoModel* infoModel();
-    /// Returns a list of channel names
-    QStringList channelNames();
-
-signals:
-    void numOfChannelsChanged(unsigned value);
-    void numOfSamplesChanged(unsigned value);
-    void channelNameChanged(unsigned channel, QString name);
-    void dataAdded(); ///< emitted when data added to channel man.
-
-public slots:
-    void setNumOfChannels(unsigned number);
-    void setNumOfSamples(unsigned number);
-    /**
-     * Add data for all channels.
-     *
-     * All channels data is provided in a single array which contains equal
-     * number of samples for all channels. Structure is as shown below:
-     *
-     * [CH0_SMP0, CH0_SMP1 ... CH0_SMPN, CH1_SMP0, CH1_SMP1, ... , CHN_SMPN]
-     *
-     * @param data samples for all channels
-     * @param size size of `data`, must be multiple of `numOfChannels`
-     */
-    void addData(double* data, unsigned size);
-
-    /// When paused `addData` does nothing.
-    void pause(bool paused);
-
-private:
-    unsigned _numOfChannels;
-    unsigned _numOfSamples;
-    bool _paused;
-    QList channelBuffers;
-    // QStringListModel _channelNames;
-    ChannelInfoModel _infoModel;
-
-    void addChannelName(QString name); ///< appends a new channel name at the end of list
-
-private slots:
-    void onChannelInfoChanged(const QModelIndex & topLeft,
-                              const QModelIndex & bottomRight,
-                              const QVector & roles = QVector ());
-};
-
-#endif // CHANNELMANAGER_H
diff --git a/src/commandedit.cpp b/src/commandedit.cpp
--- a/src/commandedit.cpp
+++ b/src/commandedit.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2015 Hasan Yavuz Özderya
+  Copyright © 2017 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -62,7 +62,7 @@ CommandEdit::CommandEdit(QWidget *parent
     QLineEdit(parent)
 {
     hexValidator = new HexCommandValidator(this);
-    asciiValidator = new QRegExpValidator(QRegExp("[\\x0000-\\x007F]+"));
+    asciiValidator = new QRegExpValidator(QRegExp("[\\x0000-\\x007F]+"), this);
     ascii_mode = true;
     setValidator(asciiValidator);
 }
diff --git a/src/commandwidget.cpp b/src/commandwidget.cpp
--- a/src/commandwidget.cpp
+++ b/src/commandwidget.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2016 Hasan Yavuz Özderya
+  Copyright © 2017 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -103,7 +103,14 @@ bool CommandWidget::isASCIIMode()
 
 void CommandWidget::setASCIIMode(bool enabled)
 {
-    ui->pbASCII->setChecked(enabled);
+    if (enabled)
+    {
+        ui->pbASCII->setChecked(true);
+    }
+    else
+    {
+        ui->pbHEX->setChecked(true);
+    }
 }
 
 void CommandWidget::setName(QString name)
diff --git a/src/dataformatpanel.cpp b/src/dataformatpanel.cpp
--- a/src/dataformatpanel.cpp
+++ b/src/dataformatpanel.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -21,41 +21,30 @@
 #include "ui_dataformatpanel.h"
 
 #include 
-#include 
-#include 
 #include 
 
 #include "utils.h"
 #include "setting_defines.h"
-#include "floatswap.h"
 
-DataFormatPanel::DataFormatPanel(QSerialPort* port,
-                                 ChannelManager* channelMan,
-                                 DataRecorder* recorder,
-                                 QWidget *parent) :
+DataFormatPanel::DataFormatPanel(QSerialPort* port, QWidget *parent) :
     QWidget(parent),
     ui(new Ui::DataFormatPanel),
-    bsReader(port, channelMan, recorder, this),
-    asciiReader(port, channelMan, recorder, this),
-    framedReader(port, channelMan, recorder, this),
-    demoReader(port, channelMan, recorder, this)
+    bsReader(port, this),
+    asciiReader(port, this),
+    framedReader(port, this),
+    demoReader(port, this)
 {
     ui->setupUi(this);
 
     serialPort = port;
-    _channelMan = channelMan;
     paused = false;
-    demoEnabled = false;
+    readerBeforeDemo = nullptr;
 
     // initalize default reader
     currentReader = &bsReader;
     bsReader.enable();
     ui->rbBinary->setChecked(true);
     ui->horizontalLayout->addWidget(bsReader.settingsWidget(), 1);
-    connect(&bsReader, SIGNAL(numOfChannelsChanged(unsigned)),
-            this, SIGNAL(numOfChannelsChanged(unsigned)));
-    connect(&bsReader, SIGNAL(samplesPerSecondChanged(unsigned)),
-            this, SIGNAL(samplesPerSecondChanged(unsigned)));
 
     // initalize reader selection buttons
     connect(ui->rbBinary, &QRadioButton::toggled, [this](bool checked)
@@ -72,10 +61,6 @@ DataFormatPanel::DataFormatPanel(QSerial
             {
                 if (checked) selectReader(&framedReader);
             });
-
-    // re-purpose numofchannels settings from actual reader settings to demo reader
-    connect(this, &DataFormatPanel::numOfChannelsChanged,
-            &demoReader, &DemoReader::setNumOfChannels);
 }
 
 DataFormatPanel::~DataFormatPanel()
@@ -83,9 +68,14 @@ DataFormatPanel::~DataFormatPanel()
     delete ui;
 }
 
-unsigned DataFormatPanel::numOfChannels()
+unsigned DataFormatPanel::numChannels() const
 {
-    return currentReader->numOfChannels();
+    return currentReader->numChannels();
+}
+
+Source* DataFormatPanel::activeSource()
+{
+    return currentReader;
 }
 
 void DataFormatPanel::pause(bool enabled)
@@ -95,33 +85,29 @@ void DataFormatPanel::pause(bool enabled
     demoReader.pause(enabled);
 }
 
-void DataFormatPanel::enableDemo(bool enabled)
+void DataFormatPanel::enableDemo(bool demoEnabled)
 {
-    if (enabled)
+    if (demoEnabled)
     {
-        demoReader.enable();
-        demoReader.recording = currentReader->recording;
-        connect(&demoReader, &DemoReader::samplesPerSecondChanged,
-                this, &DataFormatPanel::samplesPerSecondChanged);
+        readerBeforeDemo = currentReader;
+        demoReader.setNumChannels(readerBeforeDemo->numChannels());
+        selectReader(&demoReader);
     }
     else
     {
-        demoReader.enable(false);
-        disconnect(&demoReader, 0, this, 0);
+        Q_ASSERT(readerBeforeDemo != nullptr);
+        selectReader(readerBeforeDemo);
     }
-    demoEnabled = enabled;
+
+    // disable/enable reader selection buttons during/after demo
+    ui->rbAscii->setDisabled(demoEnabled);
+    ui->rbBinary->setDisabled(demoEnabled);
+    ui->rbFramed->setDisabled(demoEnabled);
 }
 
-void DataFormatPanel::startRecording()
+bool DataFormatPanel::isDemoEnabled() const
 {
-    currentReader->recording = true;
-    if (demoEnabled) demoReader.recording = true;
-}
-
-void DataFormatPanel::stopRecording()
-{
-    currentReader->recording = false;
-    if (demoEnabled) demoReader.recording = false;
+    return currentReader == &demoReader;
 }
 
 void DataFormatPanel::selectReader(AbstractReader* reader)
@@ -131,10 +117,6 @@ void DataFormatPanel::selectReader(Abstr
 
     // re-connect signals
     disconnect(currentReader, 0, this, 0);
-    connect(reader, SIGNAL(numOfChannelsChanged(unsigned)),
-            this, SIGNAL(numOfChannelsChanged(unsigned)));
-    connect(reader, SIGNAL(samplesPerSecondChanged(unsigned)),
-            this, SIGNAL(samplesPerSecondChanged(unsigned)));
 
     // switch the settings widget
     ui->horizontalLayout->removeWidget(currentReader->settingsWidget());
@@ -142,29 +124,24 @@ void DataFormatPanel::selectReader(Abstr
     ui->horizontalLayout->addWidget(reader->settingsWidget(), 1);
     reader->settingsWidget()->show();
 
-    // notify if number of channels is different
-    if (currentReader->numOfChannels() != reader->numOfChannels())
-    {
-        emit numOfChannelsChanged(reader->numOfChannels());
-    }
-
     reader->pause(paused);
-    reader->recording = currentReader->recording;
 
     currentReader = reader;
+    emit sourceChanged(currentReader);
 }
 
 void DataFormatPanel::saveSettings(QSettings* settings)
 {
     settings->beginGroup(SettingGroup_DataFormat);
 
-    // save selected format
+    // save selected data format (current reader)
     QString format;
-    if (currentReader == &bsReader)
+    AbstractReader* selectedReader = isDemoEnabled() ? readerBeforeDemo : currentReader;
+    if (selectedReader == &bsReader)
     {
         format = "binary";
     }
-    else if (currentReader == &asciiReader)
+    else if (selectedReader == &asciiReader)
     {
         format = "ascii";
     }
diff --git a/src/dataformatpanel.h b/src/dataformatpanel.h
--- a/src/dataformatpanel.h
+++ b/src/dataformatpanel.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -28,8 +28,6 @@
 #include 
 #include 
 
-#include "framebuffer.h"
-#include "channelmanager.h"
 #include "binarystreamreader.h"
 #include "asciireader.h"
 #include "demoreader.h"
@@ -45,14 +43,13 @@ class DataFormatPanel : public QWidget
     Q_OBJECT
 
 public:
-    explicit DataFormatPanel(QSerialPort* port,
-                             ChannelManager* channelMan,
-                             DataRecorder* recorder,
-                             QWidget* parent = 0);
+    explicit DataFormatPanel(QSerialPort* port, QWidget* parent = 0);
     ~DataFormatPanel();
 
     /// Returns currently selected number of channels
-    unsigned numOfChannels();
+    unsigned numChannels() const;
+    /// Returns active source (reader)
+    Source* activeSource();
     /// Stores data format panel settings into a `QSettings`
     void saveSettings(QSettings* settings);
     /// Loads data format panel settings from a `QSettings`.
@@ -62,25 +59,14 @@ public slots:
     void pause(bool);
     void enableDemo(bool); // demo shouldn't be enabled when port is open
 
-    /**
-     * @brief Starts sending data to recorder.
-     *
-     * @note recorder must have been started!
-     */
-    void startRecording();
-
-    /// Stops recording.
-    void stopRecording();
-
 signals:
-    void numOfChannelsChanged(unsigned);
-    void samplesPerSecondChanged(unsigned);
+    /// Active (selected) reader has changed.
+    void sourceChanged(Source* source);
 
 private:
     Ui::DataFormatPanel *ui;
 
     QSerialPort* serialPort;
-    ChannelManager* _channelMan;
 
     BinaryStreamReader bsReader;
     AsciiReader asciiReader;
@@ -92,8 +78,10 @@ private:
 
     bool paused;
 
-    bool demoEnabled;
     DemoReader demoReader;
+    AbstractReader* readerBeforeDemo;
+
+    bool isDemoEnabled() const;
 };
 
 #endif // DATAFORMATPANEL_H
diff --git a/src/datarecorder.cpp b/src/datarecorder.cpp
--- a/src/datarecorder.cpp
+++ b/src/datarecorder.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -19,6 +19,9 @@
 
 #include "datarecorder.h"
 
+#include 
+#include 
+#include 
 #include 
 
 DataRecorder::DataRecorder(QObject *parent) :
@@ -28,12 +31,25 @@ DataRecorder::DataRecorder(QObject *pare
     lastNumChannels = 0;
     disableBuffering = false;
     windowsLE = false;
+    timestampEn = false;
 }
 
-bool DataRecorder::startRecording(QString fileName, QString separator, QStringList channelNames)
+bool DataRecorder::startRecording(QString fileName, QString separator,
+                                  QStringList channelNames, bool insertTime)
 {
     Q_ASSERT(!file.isOpen());
     _sep =  separator;
+    timestampEn = insertTime;
+
+    // create directory if it doesn't exist
+    {
+        QFileInfo fi(fileName);
+        if (!fi.dir().mkpath("."))
+        {
+            qCritical() << "Failed to create directory for: " << fileName;
+            return false;
+        }
+    }
 
     // open file
     file.setFileName(fileName);
@@ -47,6 +63,10 @@ bool DataRecorder::startRecording(QStrin
     // write header line
     if (!channelNames.isEmpty())
     {
+        if (timestampEn)
+        {
+            fileStream << tr("timestamp") << _sep;
+        }
         fileStream << channelNames.join(_sep);
         fileStream << le();
         lastNumChannels = channelNames.length();
@@ -54,26 +74,35 @@ bool DataRecorder::startRecording(QStrin
     return true;
 }
 
-void DataRecorder::addData(double* data, unsigned length, unsigned numOfChannels)
+void DataRecorder::feedIn(const SamplePack& data)
 {
-    Q_ASSERT(length > 0);
-    Q_ASSERT(length % numOfChannels == 0);
+    Q_ASSERT(file.isOpen());    // recorder should be disconnected before stopping recording
+    Q_ASSERT(!data.hasX());     // NYI
 
-    if (lastNumChannels != 0 && numOfChannels != lastNumChannels)
+    // check if number of channels has changed during recording and warn
+    unsigned numChannels = data.numChannels();
+    if (lastNumChannels != 0 && numChannels != lastNumChannels)
     {
         qWarning() << "Number of channels changed from " << lastNumChannels
-                   << " to " << numOfChannels <<
+                   << " to " << numChannels <<
             " during recording, CSV file is corrupted but no data will be lost.";
     }
-    lastNumChannels = numOfChannels;
+    lastNumChannels = numChannels;
 
-    unsigned numOfSamples = length / numOfChannels; // per channel
-    for (unsigned int i = 0; i < numOfSamples; i++)
+    // write data
+    qint64 timestamp;
+    if (timestampEn) timestamp = QDateTime::currentMSecsSinceEpoch();
+    unsigned numSamples = data.numSamples();
+    for (unsigned int i = 0; i < numSamples; i++)
     {
-        for (unsigned ci = 0; ci < numOfChannels; ci++)
+        if (timestampEn)
         {
-            fileStream << data[ci * numOfSamples + i];
-            if (ci != numOfChannels-1) fileStream << _sep;
+            fileStream << timestamp << _sep;
+        }
+        for (unsigned ci = 0; ci < numChannels; ci++)
+        {
+            fileStream << data.data(ci)[i];
+            if (ci != numChannels-1) fileStream << _sep;
         }
         fileStream << le();
     }
diff --git a/src/datarecorder.h b/src/datarecorder.h
--- a/src/datarecorder.h
+++ b/src/datarecorder.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -24,7 +24,15 @@
 #include 
 #include 
 
-class DataRecorder : public QObject
+#include "sink.h"
+
+/**
+ * Implemented as a `Sink` that writes incoming data to a file. Before
+ * connecting a `Source` recording must be started with the `startRecording`
+ * method. Also before calling `stopRecording`, recorder should be disconnected
+ * from source.
+ */
+class DataRecorder : public QObject, public Sink
 {
     Q_OBJECT
 public:
@@ -44,14 +52,17 @@ public:
     /**
      * @brief Starts recording data to a file in CSV format.
      *
-     * File is opened and header line (names of channels) is written.
+     * File is opened and header line (names of channels) is written. After
+     * calling this function recorder should be connected to a `Source`.
      *
      * @param fileName name of the recording file
      * @param separator column separator
      * @param channelNames names of the channels for header line, if empty no header line is written
+     * @param insertTime enable inserting timestamp
      * @return false if file operation fails (read only etc.)
      */
-    bool startRecording(QString fileName, QString separator, QStringList channelNames);
+    bool startRecording(QString fileName, QString separator,
+                        QStringList channelNames, bool insertTime);
 
     /**
      * @brief Adds data to a channel.
@@ -75,11 +86,15 @@ public:
     /// Stops recording, closes file.
     void stopRecording();
 
+protected:
+    virtual void feedIn(const SamplePack& data);
+
 private:
     unsigned lastNumChannels;   ///< used for error message only
     QFile file;
     QTextStream fileStream;
     QString _sep;
+    bool timestampEn;
 
     /// Returns the selected line ending.
     const char* le() const;
diff --git a/src/demoreader.cpp b/src/demoreader.cpp
--- a/src/demoreader.cpp
+++ b/src/demoreader.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -25,21 +25,23 @@
 #define M_PI 3.14159265358979323846
 #endif
 
-DemoReader::DemoReader(QIODevice* device, ChannelManager* channelMan,
-                       DataRecorder* recorder, QObject* parent) :
-    AbstractReader(device, channelMan, recorder, parent)
+DemoReader::DemoReader(QIODevice* device, QObject* parent) :
+    AbstractReader(device, parent)
 {
     paused = false;
-    _numOfChannels = 1;
+    _numChannels = _settingsWidget.numChannels();
+    connect(&_settingsWidget, &DemoReaderSettings::numChannelsChanged,
+            this, &DemoReader::onNumChannelsChanged);
+
     count = 0;
     timer.setInterval(100);
-    QObject::connect(&timer, &QTimer::timeout,
-                     this, &DemoReader::demoTimerTimeout);
+    connect(&timer, &QTimer::timeout,
+            this, &DemoReader::demoTimerTimeout);
 }
 
 QWidget* DemoReader::settingsWidget()
 {
-    return NULL;
+    return &_settingsWidget;
 }
 
 void DemoReader::enable(bool enabled)
@@ -51,22 +53,18 @@ void DemoReader::enable(bool enabled)
     else
     {
         timer.stop();
+        disconnectSinks();
     }
 }
 
-unsigned DemoReader::numOfChannels()
+unsigned DemoReader::numChannels() const
 {
-    return _numOfChannels;
+    return _numChannels;
 }
 
-void DemoReader::setNumOfChannels(unsigned value)
+void DemoReader::setNumChannels(unsigned value)
 {
-    _numOfChannels = value;
-}
-
-void DemoReader::pause(bool enabled)
-{
-    paused = enabled;
+    _settingsWidget.setNumChannels(value);
 }
 
 void DemoReader::demoTimerTimeout()
@@ -77,13 +75,23 @@ void DemoReader::demoTimerTimeout()
 
     if (!paused)
     {
-        double* samples = new double[_numOfChannels];
-        for (unsigned ci = 0; ci < _numOfChannels; ci++)
+        SamplePack samples(1, _numChannels);
+        for (unsigned ci = 0; ci < _numChannels; ci++)
         {
             // we are calculating the fourier components of square wave
-            samples[ci] = 4*sin(2*M_PI*double((ci+1)*count)/period)/((2*(ci+1))*M_PI);
+            samples.data(ci)[0] = 4*sin(2*M_PI*double((ci+1)*count)/period)/((2*(ci+1))*M_PI);
         }
-        addData(samples, _numOfChannels);
-        delete[] samples;
+        feedOut(samples);
     }
 }
+
+void DemoReader::onNumChannelsChanged(unsigned value)
+{
+    _numChannels = value;
+    updateNumChannels();
+}
+
+void DemoReader::onDataReady()
+{
+    // intentionally empty, required by AbstractReader
+}
diff --git a/src/demoreader.h b/src/demoreader.h
--- a/src/demoreader.h
+++ b/src/demoreader.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -23,6 +23,7 @@
 #include 
 
 #include "abstractreader.h"
+#include "demoreadersettings.h"
 
 /**
  * This is a special case of reader implementation and should be used
@@ -38,30 +39,26 @@ class DemoReader : public AbstractReader
     Q_OBJECT
 
 public:
-    explicit DemoReader(QIODevice* device, ChannelManager* channelMan,
-                        DataRecorder* recorder, QObject* parent = 0);
+    explicit DemoReader(QIODevice* device, QObject* parent = 0);
 
-    /// Demo reader is an exception so this function returns NULL
     QWidget* settingsWidget();
-
-    unsigned numOfChannels();
-
-    void enable(bool enabled = true);
+    unsigned numChannels() const;
+    void enable(bool enabled = true) override;
 
 public slots:
-    void pause(bool);
-
-    /// Sets the number of channels, this doesn't trigger a `numOfChannelsChanged` signal.
-    void setNumOfChannels(unsigned value);
+    void setNumChannels(unsigned value);
 
 private:
-    bool paused;
-    unsigned _numOfChannels;
+    DemoReaderSettings _settingsWidget;
+
+    unsigned _numChannels;
     QTimer timer;
     int count;
 
 private slots:
     void demoTimerTimeout();
+    void onNumChannelsChanged(unsigned value);
+    void onDataReady() override;
 };
 
 #endif // DEMOREADER_H
diff --git a/src/demoreadersettings.cpp b/src/demoreadersettings.cpp
new file mode 100644
--- /dev/null
+++ b/src/demoreadersettings.cpp
@@ -0,0 +1,51 @@
+/*
+  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 "demoreadersettings.h"
+#include "ui_demoreadersettings.h"
+
+#include "utils.h"
+
+DemoReaderSettings::DemoReaderSettings(QWidget *parent) :
+    QWidget(parent),
+    ui(new Ui::DemoReaderSettings)
+{
+    ui->setupUi(this);
+
+    connect(ui->spNumChannels, SELECT::OVERLOAD_OF(&QSpinBox::valueChanged),
+            [this](int value)
+            {
+                emit numChannelsChanged(value);
+            });
+}
+
+DemoReaderSettings::~DemoReaderSettings()
+{
+    delete ui;
+}
+
+unsigned DemoReaderSettings::numChannels() const
+{
+    return ui->spNumChannels->value();
+}
+
+void DemoReaderSettings::setNumChannels(unsigned value)
+{
+    ui->spNumChannels->setValue(value);
+}
diff --git a/src/demoreadersettings.h b/src/demoreadersettings.h
new file mode 100644
--- /dev/null
+++ b/src/demoreadersettings.h
@@ -0,0 +1,48 @@
+/*
+  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 .
+*/
+
+#ifndef DEMOREADERSETTINGS_H
+#define DEMOREADERSETTINGS_H
+
+#include 
+
+namespace Ui {
+class DemoReaderSettings;
+}
+
+class DemoReaderSettings : public QWidget
+{
+    Q_OBJECT
+
+public:
+    explicit DemoReaderSettings(QWidget *parent = 0);
+    ~DemoReaderSettings();
+
+    unsigned numChannels() const;
+    /// Doesn't signal `numChannelsChanged`.
+    void setNumChannels(unsigned value);
+
+private:
+    Ui::DemoReaderSettings *ui;
+
+signals:
+    void numChannelsChanged(unsigned);
+};
+
+#endif // DEMOREADERSETTINGS_H
diff --git a/src/demoreadersettings.ui b/src/demoreadersettings.ui
new file mode 100644
--- /dev/null
+++ b/src/demoreadersettings.ui
@@ -0,0 +1,83 @@
+
+
+ DemoReaderSettings
+ 
+  
+   
+    0
+    0
+    444
+    141
+   
+  
+  
+   Form
+  
+  
+   - 
+    
+     
+      QFormLayout::ExpandingFieldsGrow
+     
+     
- 
+      
+       
+        Number Of Channels:
+       
+      
+     
 
+     - 
+      
+       
+        
+         60
+         0
+        
+       
+       
+        Select number of channels or set to 0 for Auto (determined from incoming data)
+       
+       
+        
+       
+       
+        false
+       
+       
+        1
+       
+       
+        32
+       
+       
+        1
+       
+      
+     
 
+    
+    
+   - 
+    
+     
+      Qt::Vertical
+     
+     
+      
+       20
+       40
+      
+     
+    
+   
 
+   - 
+    
+     
+      Demo is enabled, exit demo for reader settings.
+     
+    
+   
 
+  
+ 
+ 
+ 
+
diff --git a/src/framebuffer.cpp b/src/framebuffer.cpp
deleted file mode 100644
--- a/src/framebuffer.cpp
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
-  Copyright © 2015 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 "framebuffer.h"
-
-FrameBuffer::FrameBuffer(size_t size)
-{
-    _size = size;
-    data = new double[_size]();
-    headIndex = 0;
-
-    _boundingRect.setCoords(0, 0, size, 0);
-}
-
-FrameBuffer::~FrameBuffer()
-{
-    delete[] data;
-}
-
-void FrameBuffer::resize(size_t size)
-{
-    int offset = size - _size;
-    if (offset == 0) return;
-
-    double* newData = new double[size];
-
-    // move data to new array
-    int fill_start = offset > 0 ? offset : 0;
-
-    for (int i = fill_start; i < int(size); i++)
-    {
-        newData[i] = sample(i - offset);
-    }
-
-    // fill the beginning of the new data
-    if (fill_start > 0)
-    {
-        for (int i = 0; i < fill_start; i++)
-        {
-            newData[i] = 0;
-        }
-    }
-
-    // data is ready, clean and re-point
-    delete data;
-    data = newData;
-    headIndex = 0;
-    _size = size;
-
-    // update the bounding rectangle
-    _boundingRect.setRight(_size);
-}
-
-void FrameBuffer::addSamples(double* samples, size_t size)
-{
-    unsigned shift = size;
-    if (shift < _size)
-    {
-        unsigned x = _size - headIndex; // distance of `head` to end
-
-        if (shift <= x) // there is enough room at the end of array
-        {
-            for (size_t i = 0; i < shift; i++)
-            {
-                data[i+headIndex] = samples[i];
-            }
-
-            if (shift == x) // we used all the room at the end
-            {
-                headIndex = 0;
-            }
-            else
-            {
-                headIndex += shift;
-            }
-        }
-        else // there isn't enough room
-        {
-            for (size_t i = 0; i < x; i++) // fill the end part
-            {
-                data[i+headIndex] = samples[i];
-            }
-            for (size_t i = 0; i < (shift-x); i++) // continue from the beginning
-            {
-                data[i] = samples[i+x];
-            }
-            headIndex = shift-x;
-        }
-    }
-    else // number of new samples equal or bigger than current size
-    {
-        int x = shift - _size;
-        for (size_t i = 0; i < _size; i++)
-        {
-            data[i] = samples[i+x];
-        }
-        headIndex = 0;
-    }
-
-    // update bounding rectangle
-    double minValue = data[0];
-    double maxValue = data[0];
-    for (size_t i = 0; i < _size; i++)
-    {
-        if (data[i] > maxValue)
-        {
-            maxValue = data[i];
-        }
-        else if (data[i] < minValue)
-        {
-            minValue = data[i];
-        }
-    }
-    _boundingRect.setTop(minValue);
-    _boundingRect.setBottom(maxValue);
-}
-
-void FrameBuffer::clear()
-{
-    for (size_t i=0; i < _size; i++) data[i] = 0.;
-}
-
-size_t FrameBuffer::size() const
-{
-    return _size;
-}
-
-QRectF FrameBuffer::boundingRect() const
-{
-    return _boundingRect;
-}
-
-double FrameBuffer::sample(size_t i) const
-{
-    size_t index = headIndex + i;
-    if (index >= _size) index -= _size;
-    return data[index];
-}
diff --git a/src/framebuffer.h b/src/framebuffer.h
--- a/src/framebuffer.h
+++ b/src/framebuffer.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2015 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -17,33 +17,48 @@
   along with serialplot.  If not, see .
 */
 
+// IMPORTANT NOTE: this file will be renamed to "framebuffer.h" when
+// stream work is complete
+
 #ifndef FRAMEBUFFER_H
 #define FRAMEBUFFER_H
 
-#include 
-#include 
+struct Range
+{
+    double start, end;
+};
 
+/// Abstract base class for all frame buffers.
 class FrameBuffer
 {
 public:
-    FrameBuffer(size_t size);
-    ~FrameBuffer();
-
-    void resize(size_t size);
-    void addSamples(double* samples, size_t size);
-    void clear(); // fill 0
+    /// Placeholder virtual destructor
+    virtual ~FrameBuffer() {};
+    /// Returns size of the buffer.
+    virtual unsigned size() const = 0;
+    /// Returns a sample from given index.
+    virtual double sample(unsigned i) const = 0;
+    /// Returns minimum and maximum of the buffer values.
+    virtual Range limits() const = 0;
+};
 
-    // QwtSeriesData related implementations
-    size_t size() const;
-    QRectF boundingRect() const;
-    double sample(size_t i) const;
+/// Common base class for index and writable frame buffers
+class ResizableBuffer : public FrameBuffer
+{
+public:
+    /// Resize the buffer.
+    ///
+    /// @important Resizing to same value is an error.
+    virtual void resize(unsigned n) = 0;
+};
 
-private:
-    size_t _size; // size of `data`
-    double* data;
-    size_t headIndex; // indicates the actual `0` index of the ring buffer
-
-    QRectF _boundingRect;
+/// Abstract base class for writable frame buffers
+class WFrameBuffer : public ResizableBuffer
+{
+    /// Add samples to the buffer
+    virtual void addSamples(double* samples, unsigned n) = 0;
+    /// Reset all data to 0
+    virtual void clear() = 0;
 };
 
 #endif // FRAMEBUFFER_H
diff --git a/src/framebufferseries.cpp b/src/framebufferseries.cpp
--- a/src/framebufferseries.cpp
+++ b/src/framebufferseries.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -17,14 +17,17 @@
   along with serialplot.  If not, see .
 */
 
+#include 
 #include "framebufferseries.h"
 
-FrameBufferSeries::FrameBufferSeries(FrameBuffer* buffer)
+FrameBufferSeries::FrameBufferSeries(const FrameBuffer* buffer)
 {
     xAsIndex = true;
     _xmin = 0;
     _xmax = 1;
     _buffer = buffer;
+    int_index_start = 0;
+    int_index_end = _buffer->size();
 }
 
 void FrameBufferSeries::setXAxis(bool asIndex, double xmin, double xmax)
@@ -36,32 +39,56 @@ void FrameBufferSeries::setXAxis(bool as
 
 size_t FrameBufferSeries::size() const
 {
-    return _buffer->size();
+    return int_index_end - int_index_start;
 }
 
 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) / size() + _xmin, _buffer->sample(i));
+        return QPointF(i * (_xmax - _xmin) / _buffer->size() + _xmin, _buffer->sample(i));
     }
 }
 
 QRectF FrameBufferSeries::boundingRect() const
 {
+    QRectF rect;
+    auto yLim = _buffer->limits();
+    rect.setBottom(yLim.start);
+    rect.setTop(yLim.end);
     if (xAsIndex)
     {
-        return _buffer->boundingRect();
+        rect.setLeft(0);
+        rect.setRight(size());
     }
     else
     {
-        auto rect = _buffer->boundingRect();
         rect.setLeft(_xmin);
         rect.setRight(_xmax);
-        return rect;
+    }
+    return rect.normalized();
+}
+
+void FrameBufferSeries::setRectOfInterest(const QRectF& rect)
+{
+    if (xAsIndex)
+    {
+        int_index_start = floor(rect.left())-1;
+        int_index_end = ceil(rect.right())+1;
     }
+    else
+    {
+        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 = std::max(int_index_start, 0);
+    int_index_end = std::min((int) _buffer->size(), int_index_end);
 }
diff --git a/src/framebufferseries.h b/src/framebufferseries.h
--- a/src/framebufferseries.h
+++ b/src/framebufferseries.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -35,7 +35,7 @@
 class FrameBufferSeries : public QwtSeriesData
 {
 public:
-    FrameBufferSeries(FrameBuffer* buffer);
+    FrameBufferSeries(const FrameBuffer* buffer);
 
     /// Behavior of X axis
     void setXAxis(bool asIndex, double xmin, double xmax);
@@ -44,12 +44,16 @@ public:
     size_t size() const;
     QPointF sample(size_t i) const;
     QRectF boundingRect() const;
+    void setRectOfInterest(const QRectF& rect);
 
 private:
-    FrameBuffer* _buffer;
+    const FrameBuffer* _buffer;
     bool xAsIndex;
     double _xmin;
     double _xmax;
+
+    int int_index_start; ///< starting index of "rectangle of interest"
+    int int_index_end;   ///< ending index of "rectangle of interest"
 };
 
 #endif // FRAMEBUFFERSERIES_H
diff --git a/src/framedreader.cpp b/src/framedreader.cpp
--- a/src/framedreader.cpp
+++ b/src/framedreader.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -23,15 +23,14 @@
 
 #include "framedreader.h"
 
-FramedReader::FramedReader(QIODevice* device, ChannelManager* channelMan,
-                           DataRecorder* recorder, QObject* parent) :
-    AbstractReader(device, channelMan, recorder, parent)
+FramedReader::FramedReader(QIODevice* device, QObject* parent) :
+    AbstractReader(device, parent)
 {
     paused = false;
 
     // initial settings
     settingsInvalid = 0;
-    _numOfChannels = _settingsWidget.numOfChannels();
+    _numChannels = _settingsWidget.numOfChannels();
     hasSizeByte = _settingsWidget.frameSize() == 0;
     frameSize = _settingsWidget.frameSize();
     syncWord = _settingsWidget.syncWord();
@@ -63,32 +62,14 @@ FramedReader::FramedReader(QIODevice* de
     reset();
 }
 
-void FramedReader::enable(bool enabled)
-{
-    if (enabled)
-    {
-        connect(_device, &QIODevice::readyRead,
-                this, &FramedReader::onDataReady);
-    }
-    else
-    {
-        QObject::disconnect(_device, 0, this, 0);
-    }
-}
-
 QWidget* FramedReader::settingsWidget()
 {
     return &_settingsWidget;
 }
 
-unsigned FramedReader::numOfChannels()
+unsigned FramedReader::numChannels() const
 {
-    return _numOfChannels;
-}
-
-void FramedReader::pause(bool enabled)
-{
-    paused = enabled;
+    return _numChannels;
 }
 
 void FramedReader::onNumberFormatChanged(NumberFormat numberFormat)
@@ -145,7 +126,7 @@ void FramedReader::checkSettings()
     }
 
     // check if fixed frame size is multiple of a sample set size
-    if (!hasSizeByte && frameSize % (_numOfChannels * sampleSize) != 0)
+    if (!hasSizeByte && frameSize % (_numChannels * sampleSize) != 0)
     {
         settingsInvalid |= FRAMESIZE_INVALID;
     }
@@ -163,7 +144,7 @@ void FramedReader::checkSettings()
     {
         QString errorMessage =
             QString("Frame size must be multiple of %1 (#channels * sample size)!")\
-            .arg(_numOfChannels * sampleSize);
+            .arg(_numChannels * sampleSize);
 
         _settingsWidget.showMessage(errorMessage, true);
     }
@@ -175,7 +156,7 @@ void FramedReader::checkSettings()
 
 void FramedReader::onNumOfChannelsChanged(unsigned value)
 {
-    _numOfChannels = value;
+    _numChannels = value;
     checkSettings();
     reset();
     emit numOfChannelsChanged(value);
@@ -238,11 +219,11 @@ void FramedReader::onDataReady()
                 qCritical() << "Frame size is 0!";
                 reset();
             }
-            else if (frameSize % (_numOfChannels * sampleSize) != 0)
+            else if (frameSize % (_numChannels * sampleSize) != 0)
             {
                 qCritical() <<
                     QString("Frame size is not multiple of %1 (#channels * sample size)!") \
-                    .arg(_numOfChannels * sampleSize);
+                    .arg(_numChannels * sampleSize);
                 reset();
             }
             else
@@ -287,14 +268,13 @@ void FramedReader::readFrameDataAndCheck
     }
 
     // a package is 1 set of samples for all channels
-    unsigned numOfPackagesToRead = frameSize / (_numOfChannels * sampleSize);
-    double* channelSamples = new double[numOfPackagesToRead * _numOfChannels];
-
+    unsigned numOfPackagesToRead = frameSize / (_numChannels * sampleSize);
+    SamplePack samples(numOfPackagesToRead, _numChannels);
     for (unsigned i = 0; i < numOfPackagesToRead; i++)
     {
-        for (unsigned int ci = 0; ci < _numOfChannels; ci++)
+        for (unsigned int ci = 0; ci < _numChannels; ci++)
         {
-            channelSamples[ci*numOfPackagesToRead+i] = (this->*readSample)();
+            samples.data(ci)[i] = (this->*readSample)();
         }
     }
 
@@ -311,14 +291,12 @@ void FramedReader::readFrameDataAndCheck
     if (!checksumEnabled || checksumPassed)
     {
         // commit data
-        addData(channelSamples, numOfPackagesToRead*_numOfChannels);
+        feedOut(samples);
     }
     else
     {
         qCritical() << "Checksum failed! Received:" << rChecksum << "Calculated:" << calcChecksum;
     }
-
-    delete[] channelSamples;
 }
 
 template double FramedReader::readSampleAs()
diff --git a/src/framedreader.h b/src/framedreader.h
--- a/src/framedreader.h
+++ b/src/framedreader.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -33,19 +33,14 @@ class FramedReader : public AbstractRead
     Q_OBJECT
 
 public:
-    explicit FramedReader(QIODevice* device, ChannelManager* channelMan,
-                          DataRecorder* recorder, QObject *parent = 0);
+    explicit FramedReader(QIODevice* device, QObject *parent = 0);
     QWidget* settingsWidget();
-    unsigned numOfChannels();
-    void enable(bool enabled = true);
+    unsigned numChannels() const;
     /// Stores settings into a `QSettings`
     void saveSettings(QSettings* settings);
     /// Loads settings from a `QSettings`.
     void loadSettings(QSettings* settings);
 
-public slots:
-    void pause(bool);
-
 private:
     /// bit wise fields for `settingsValid` member
     enum SettingInvalidFlag
@@ -56,9 +51,8 @@ private:
 
     // settings related members
     FramedReaderSettings _settingsWidget;
-    unsigned _numOfChannels;
+    unsigned _numChannels;
     unsigned sampleSize;
-    bool paused;
     unsigned settingsInvalid;   /// settings are all valid if this is 0, if not no reading is done
     QByteArray syncWord;
     bool checksumEnabled;
@@ -86,7 +80,7 @@ private:
     void readFrameDataAndCheck();
 
 private slots:
-    void onDataReady();
+    void onDataReady() override;
 
     void onNumberFormatChanged(NumberFormat numberFormat);
     void onNumOfChannelsChanged(unsigned value);
diff --git a/src/indexbuffer.cpp b/src/indexbuffer.cpp
new file mode 100644
--- /dev/null
+++ b/src/indexbuffer.cpp
@@ -0,0 +1,49 @@
+/*
+  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 
+
+#include "indexbuffer.h"
+
+IndexBuffer::IndexBuffer(unsigned n)
+{
+    _size = n;
+}
+
+unsigned IndexBuffer::size() const
+{
+    return _size;
+}
+
+void IndexBuffer::resize(unsigned n)
+{
+    _size = n;
+}
+
+double IndexBuffer::sample(unsigned i) const
+{
+    Q_ASSERT(i < _size);
+
+    return i;
+}
+
+Range IndexBuffer::limits() const
+{
+    return Range{0, _size-1.};
+}
diff --git a/src/indexbuffer.h b/src/indexbuffer.h
new file mode 100644
--- /dev/null
+++ b/src/indexbuffer.h
@@ -0,0 +1,43 @@
+/*
+  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 .
+*/
+
+#ifndef INDEXBUFFER_H
+#define INDEXBUFFER_H
+
+#include "framebuffer.h"
+
+/// A simple frame buffer that simply returns requested index as
+/// sample value.
+///
+/// @note This buffer isn't for storing data.
+class IndexBuffer : public ResizableBuffer
+{
+public:
+    IndexBuffer(unsigned n);
+
+    unsigned size() const;
+    double sample(unsigned i) const;
+    Range limits() const;
+    void resize(unsigned n);
+
+private:
+    unsigned _size;
+};
+
+#endif
diff --git a/src/linindexbuffer.cpp b/src/linindexbuffer.cpp
new file mode 100644
--- /dev/null
+++ b/src/linindexbuffer.cpp
@@ -0,0 +1,57 @@
+ /*
+  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 
+
+#include "linindexbuffer.h"
+
+LinIndexBuffer::LinIndexBuffer(unsigned n, Range lim)
+{
+    Q_ASSERT(n > 0);
+
+    _size = n;
+    setLimits(lim);
+}
+
+unsigned LinIndexBuffer::size() const
+{
+    return _size;
+}
+
+double LinIndexBuffer::sample(unsigned i) const
+{
+    return _limits.start + i * _step;
+}
+
+Range LinIndexBuffer::limits() const
+{
+    return _limits;
+}
+
+void LinIndexBuffer::resize(unsigned n)
+{
+    _size = n;
+    setLimits(_limits);         // called to update `_step`
+}
+
+void LinIndexBuffer::setLimits(Range lim)
+{
+    _limits = lim;
+    _step = (lim.end - lim.start) / (_size-1);
+}
diff --git a/src/linindexbuffer.h b/src/linindexbuffer.h
new file mode 100644
--- /dev/null
+++ b/src/linindexbuffer.h
@@ -0,0 +1,49 @@
+ /*
+  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 .
+*/
+
+#ifndef LININDEXBUFFER_H
+#define LININDEXBUFFER_H
+
+#include "framebuffer.h"
+
+/// A dynamic frame buffer that start and end values can be set and
+/// intermediate values are calculated linearly.
+///
+/// @note This buffer isn't for storing data.
+class LinIndexBuffer : public ResizableBuffer
+{
+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);
+    /// Sets minimum and maximum sample values of the buffer.
+    void setLimits(Range lim);
+
+private:
+    unsigned _size;
+    Range _limits;
+    double _step;
+};
+
+#endif
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -35,6 +35,7 @@
 #include 
 
 #include 
+#include 
 
 #include "framebufferseries.h"
 #include "utils.h"
@@ -47,11 +48,14 @@
 Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin)
 #endif
 
+// TODO: depends on tab insertion order, a better solution would be to use object names
 const QMap panelSettingMap({
         {0, "Port"},
         {1, "DataFormat"},
         {2, "Plot"},
-        {3, "Commands"}
+        {3, "Commands"},
+        {4, "Record"},
+        {5, "Log"}
     });
 
 MainWindow::MainWindow(QWidget *parent) :
@@ -59,15 +63,16 @@ MainWindow::MainWindow(QWidget *parent) 
     ui(new Ui::MainWindow),
     aboutDialog(this),
     portControl(&serialPort),
-    channelMan(1, 1, this),
-    snapshotMan(this, &channelMan),
+    secondaryPlot(NULL),
+    snapshotMan(this, &stream),
     commandPanel(&serialPort),
-    dataFormatPanel(&serialPort, &channelMan, &recorder),
-    recordPanel(&recorder, &channelMan)
+    dataFormatPanel(&serialPort),
+    recordPanel(&stream),
+    updateCheckDialog(this)
 {
     ui->setupUi(this);
 
-    plotMan = new PlotManager(ui->plotArea, channelMan.infoModel());
+    plotMan = new PlotManager(ui->plotArea, &plotMenu, &stream);
 
     ui->tabWidget->insertTab(0, &portControl, "Port");
     ui->tabWidget->insertTab(1, &dataFormatPanel, "Data Format");
@@ -80,8 +85,8 @@ MainWindow::MainWindow(QWidget *parent) 
     addToolBar(recordPanel.toolbar());
 
     ui->plotToolBar->addAction(snapshotMan.takeSnapshotAction());
-    ui->menuBar->insertMenu(ui->menuHelp->menuAction(), snapshotMan.menu());
-    ui->menuBar->insertMenu(ui->menuHelp->menuAction(), commandPanel.menu());
+    menuBar()->insertMenu(ui->menuHelp->menuAction(), snapshotMan.menu());
+    menuBar()->insertMenu(ui->menuHelp->menuAction(), commandPanel.menu());
 
     connect(&commandPanel, &CommandPanel::focusRequested, [this]()
             {
@@ -95,23 +100,42 @@ MainWindow::MainWindow(QWidget *parent) 
     setupAboutDialog();
 
     // init view menu
-    for (auto a : plotMan->menuActions())
-    {
-        ui->menuView->addAction(a);
-    }
-
-    ui->menuView->addSeparator();
-
-    QMenu* tbMenu = ui->menuView->addMenu("Toolbars");
+    ui->menuBar->insertMenu(ui->menuSecondary->menuAction(), &plotMenu);
+    plotMenu.addSeparator();
+    QMenu* tbMenu = plotMenu.addMenu("Toolbars");
     tbMenu->addAction(ui->plotToolBar->toggleViewAction());
     tbMenu->addAction(portControl.toolBar()->toggleViewAction());
 
+    // init secondary plot menu
+    auto group = new QActionGroup(this);
+    group->addAction(ui->actionVertical);
+    group->addAction(ui->actionHorizontal);
+
     // init UI signals
 
+    // Secondary plot menu signals
+    connect(ui->actionBarPlot, &QAction::triggered,
+            this, &MainWindow::showBarPlot);
+
+    connect(ui->actionVertical, &QAction::triggered,
+            [this](bool checked)
+            {
+                if (checked) ui->splitter->setOrientation(Qt::Vertical);
+            });
+
+    connect(ui->actionHorizontal, &QAction::triggered,
+            [this](bool checked)
+            {
+                if (checked) ui->splitter->setOrientation(Qt::Horizontal);
+            });
+
     // Help menu signals
     QObject::connect(ui->actionHelpAbout, &QAction::triggered,
               &aboutDialog, &QWidget::show);
 
+    QObject::connect(ui->actionCheckUpdate, &QAction::triggered,
+              &updateCheckDialog, &QWidget::show);
+
     QObject::connect(ui->actionReportBug, &QAction::triggered,
                      [](){QDesktopServices::openUrl(QUrl(BUG_REPORT_URL));});
 
@@ -147,28 +171,18 @@ MainWindow::MainWindow(QWidget *parent) 
     connect(&plotControlPanel, &PlotControlPanel::xScaleChanged,
             plotMan, &PlotManager::setXAxis);
 
+    connect(&plotControlPanel, &PlotControlPanel::plotWidthChanged,
+            plotMan, &PlotManager::setPlotWidth);
+
+    // plot toolbar signals
     QObject::connect(ui->actionClear, SIGNAL(triggered(bool)),
                      this, SLOT(clearPlot()));
 
     QObject::connect(snapshotMan.takeSnapshotAction(), &QAction::triggered,
                      plotMan, &PlotManager::flashSnapshotOverlay);
 
-    // init port signals
-    QObject::connect(&(this->serialPort), SIGNAL(error(QSerialPort::SerialPortError)),
-                     this, SLOT(onPortError(QSerialPort::SerialPortError)));
-
-    // init data format and reader
-    QObject::connect(&channelMan, &ChannelManager::dataAdded,
-                     plotMan, &PlotManager::replot);
-
     QObject::connect(ui->actionPause, &QAction::triggered,
-                     &channelMan, &ChannelManager::pause);
-
-    QObject::connect(&recordPanel, &RecordPanel::recordStarted,
-                     &dataFormatPanel, &DataFormatPanel::startRecording);
-
-    QObject::connect(&recordPanel, &RecordPanel::recordStopped,
-                     &dataFormatPanel, &DataFormatPanel::stopRecording);
+                     &stream, &Stream::pause);
 
     QObject::connect(ui->actionPause, &QAction::triggered,
                      [this](bool enabled)
@@ -195,26 +209,10 @@ MainWindow::MainWindow(QWidget *parent) 
     connect(&serialPort, &QIODevice::aboutToClose,
             &recordPanel, &RecordPanel::onPortClose);
 
-    // init data arrays and plot
+    // init plot
     numOfSamples = plotControlPanel.numOfSamples();
-    unsigned numOfChannels = dataFormatPanel.numOfChannels();
-
-    channelMan.setNumOfSamples(numOfSamples);
-    channelMan.setNumOfChannels(dataFormatPanel.numOfChannels());
-
-    connect(&dataFormatPanel, &DataFormatPanel::numOfChannelsChanged,
-            &channelMan, &ChannelManager::setNumOfChannels);
-
-    connect(&channelMan, &ChannelManager::numOfChannelsChanged,
-            this, &MainWindow::onNumOfChannelsChanged);
-
-    plotControlPanel.setChannelInfoModel(channelMan.infoModel());
-
-    // init curve list
-    for (unsigned int i = 0; i < numOfChannels; i++)
-    {
-        plotMan->addCurve(channelMan.channelName(i), channelMan.channelBuffer(i));
-    }
+    stream.setNumSamples(numOfSamples);
+    plotControlPanel.setChannelInfoModel(stream.infoModel());
 
     // init scales
     plotMan->setYAxis(plotControlPanel.autoScale(),
@@ -222,14 +220,14 @@ MainWindow::MainWindow(QWidget *parent) 
     plotMan->setXAxis(plotControlPanel.xAxisAsIndex(),
                       plotControlPanel.xMin(), plotControlPanel.xMax());
     plotMan->setNumOfSamples(numOfSamples);
+    plotMan->setPlotWidth(plotControlPanel.plotWidth());
 
     // Init sps (sample per second) counter
     spsLabel.setText("0sps");
     spsLabel.setToolTip("samples per second (per channel)");
     ui->statusBar->addPermanentWidget(&spsLabel);
-    QObject::connect(&dataFormatPanel,
-                     &DataFormatPanel::samplesPerSecondChanged,
-                     this, &MainWindow::onSpsChanged);
+    connect(&sampleCounter, &SampleCounter::spsChanged,
+            this, &MainWindow::onSpsChanged);
 
     // init demo
     QObject::connect(ui->actionDemoMode, &QAction::toggled,
@@ -238,6 +236,11 @@ MainWindow::MainWindow(QWidget *parent) 
     QObject::connect(ui->actionDemoMode, &QAction::toggled,
                      plotMan, &PlotManager::showDemoIndicator);
 
+    // init stream connections
+    connect(&dataFormatPanel, &DataFormatPanel::sourceChanged,
+            this, &MainWindow::onSourceChanged);
+    onSourceChanged(dataFormatPanel.activeSource());
+
     // load default settings
     QSettings settings("serialplot", "serialplot");
     loadAllSettings(&settings);
@@ -279,8 +282,7 @@ void MainWindow::closeEvent(QCloseEvent 
         auto clickedButton = QMessageBox::warning(
             this, "Closing SerialPlot",
             "There are un-saved snapshots. If you close you will loose the data.",
-            QMessageBox::Discard | QMessageBox::Discard,
-            QMessageBox::Cancel);
+            QMessageBox::Discard, QMessageBox::Cancel);
         if (clickedButton == QMessageBox::Cancel)
         {
             event->ignore();
@@ -346,104 +348,29 @@ void MainWindow::onPortToggled(bool open
     ui->actionDemoMode->setEnabled(!open);
 }
 
-void MainWindow::onPortError(QSerialPort::SerialPortError error)
+void MainWindow::onSourceChanged(Source* source)
 {
-    switch(error)
-    {
-        case QSerialPort::NoError :
-            break;
-        case QSerialPort::ResourceError :
-            qWarning() << "Port error: resource unavaliable; most likely device removed.";
-            if (serialPort.isOpen())
-            {
-                qWarning() << "Closing port on resource error: " << serialPort.portName();
-                portControl.togglePort();
-            }
-            portControl.loadPortList();
-            break;
-        case QSerialPort::DeviceNotFoundError:
-            qCritical() << "Device doesn't exists: " << serialPort.portName();
-            break;
-        case QSerialPort::PermissionError:
-            qCritical() << "Permission denied. Either you don't have \
-required privileges or device is already opened by another process.";
-            break;
-        case QSerialPort::OpenError:
-            qWarning() << "Device is already opened!";
-            break;
-        case QSerialPort::NotOpenError:
-            qCritical() << "Device is not open!";
-            break;
-        case QSerialPort::ParityError:
-            qCritical() << "Parity error detected.";
-            break;
-        case QSerialPort::FramingError:
-            qCritical() << "Framing error detected.";
-            break;
-        case QSerialPort::BreakConditionError:
-            qCritical() << "Break condition is detected.";
-            break;
-        case QSerialPort::WriteError:
-            qCritical() << "An error occurred while writing data.";
-            break;
-        case QSerialPort::ReadError:
-            qCritical() << "An error occurred while reading data.";
-            break;
-        case QSerialPort::UnsupportedOperationError:
-            qCritical() << "Operation is not supported.";
-            break;
-        case QSerialPort::TimeoutError:
-            qCritical() << "A timeout error occurred.";
-            break;
-        case QSerialPort::UnknownError:
-            qCritical() << "Unknown error! Error: " << serialPort.errorString();
-            break;
-        default:
-            qCritical() << "Unhandled port error: " << error;
-            break;
-    }
+    source->connectSink(&stream);
+    source->connectSink(&sampleCounter);
 }
 
 void MainWindow::clearPlot()
 {
-    for (unsigned ci = 0; ci < channelMan.numOfChannels(); ci++)
-    {
-        channelMan.channelBuffer(ci)->clear();
-    }
+    stream.clear();
     plotMan->replot();
 }
 
 void MainWindow::onNumOfSamplesChanged(int value)
 {
     numOfSamples = value;
-    channelMan.setNumOfSamples(value);
+    stream.setNumSamples(value);
     plotMan->replot();
 }
 
-void MainWindow::onNumOfChannelsChanged(unsigned value)
+void MainWindow::onSpsChanged(float sps)
 {
-    unsigned int oldNum = plotMan->numOfCurves();
-    unsigned numOfChannels = value;
-
-    if (numOfChannels > oldNum)
-    {
-        // add new channels
-        for (unsigned int i = oldNum; i < numOfChannels; i++)
-        {
-            plotMan->addCurve(channelMan.channelName(i), channelMan.channelBuffer(i));
-        }
-    }
-    else if(numOfChannels < oldNum)
-    {
-        plotMan->removeCurves(oldNum - numOfChannels);
-    }
-
-    plotMan->replot();
-}
-
-void MainWindow::onSpsChanged(unsigned sps)
-{
-    spsLabel.setText(QString::number(sps) + "sps");
+    int precision = sps < 1. ? 3 : 0;
+    spsLabel.setText(QString::number(sps, 'f', precision) + "sps");
 }
 
 bool MainWindow::isDemoRunning()
@@ -471,6 +398,48 @@ void MainWindow::enableDemo(bool enabled
     }
 }
 
+void MainWindow::showSecondary(QWidget* wid)
+{
+    if (secondaryPlot != NULL)
+    {
+        secondaryPlot->deleteLater();
+    }
+
+    secondaryPlot = wid;
+    ui->splitter->addWidget(wid);
+    ui->splitter->setStretchFactor(0, 1);
+    ui->splitter->setStretchFactor(1, 0);
+}
+
+void MainWindow::hideSecondary()
+{
+    if (secondaryPlot == NULL)
+    {
+        qFatal("Secondary plot doesn't exist!");
+    }
+
+    secondaryPlot->deleteLater();
+    secondaryPlot = NULL;
+}
+
+void MainWindow::showBarPlot(bool show)
+{
+    if (show)
+    {
+        auto plot = new BarPlot(&stream, &plotMenu);
+        plot->setYAxis(plotControlPanel.autoScale(),
+                       plotControlPanel.yMin(),
+                       plotControlPanel.yMax());
+        connect(&plotControlPanel, &PlotControlPanel::yScaleChanged,
+                plot, &BarPlot::setYAxis);
+        showSecondary(plot);
+    }
+    else
+    {
+        hideSecondary();
+    }
+}
+
 void MainWindow::onExportCsv()
 {
     bool wasPaused = ui->actionPause->isChecked();
@@ -492,7 +461,7 @@ void MainWindow::onExportCsv()
 
 PlotViewSettings MainWindow::viewSettings() const
 {
-    return plotMan->viewSettings();
+    return plotMenu.viewSettings();
 }
 
 void MainWindow::messageHandler(QtMsgType type,
@@ -541,11 +510,12 @@ void MainWindow::saveAllSettings(QSettin
     saveMWSettings(settings);
     portControl.saveSettings(settings);
     dataFormatPanel.saveSettings(settings);
-    channelMan.saveSettings(settings);
+    stream.saveSettings(settings);
     plotControlPanel.saveSettings(settings);
-    plotMan->saveSettings(settings);
+    plotMenu.saveSettings(settings);
     commandPanel.saveSettings(settings);
     recordPanel.saveSettings(settings);
+    updateCheckDialog.saveSettings(settings);
 }
 
 void MainWindow::loadAllSettings(QSettings* settings)
@@ -553,11 +523,12 @@ void MainWindow::loadAllSettings(QSettin
     loadMWSettings(settings);
     portControl.loadSettings(settings);
     dataFormatPanel.loadSettings(settings);
-    channelMan.loadSettings(settings);
+    stream.loadSettings(settings);
     plotControlPanel.loadSettings(settings);
-    plotMan->loadSettings(settings);
+    plotMenu.loadSettings(settings);
     commandPanel.loadSettings(settings);
     recordPanel.loadSettings(settings);
+    updateCheckDialog.loadSettings(settings);
 }
 
 void MainWindow::saveMWSettings(QSettings* settings)
diff --git a/src/mainwindow.h b/src/mainwindow.h
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -40,11 +40,12 @@
 #include "plotcontrolpanel.h"
 #include "recordpanel.h"
 #include "ui_about_dialog.h"
-#include "framebuffer.h"
-#include "channelmanager.h"
+#include "stream.h"
 #include "snapshotmanager.h"
 #include "plotmanager.h"
-#include "datarecorder.h"
+#include "plotmenu.h"
+#include "updatecheckdialog.h"
+#include "samplecounter.h"
 
 namespace Ui {
 class MainWindow;
@@ -75,18 +76,28 @@ private:
     unsigned int numOfSamples;
 
     QList curves;
-    ChannelManager channelMan;
+    // ChannelManager channelMan;
+    Stream stream;
     PlotManager* plotMan;
+    QWidget* secondaryPlot;
     SnapshotManager snapshotMan;
-    DataRecorder recorder;       // operated by `recordPanel`
+    SampleCounter sampleCounter;
 
     QLabel spsLabel;
     CommandPanel commandPanel;
     DataFormatPanel dataFormatPanel;
     RecordPanel recordPanel;
     PlotControlPanel plotControlPanel;
+    PlotMenu plotMenu;
+    UpdateCheckDialog updateCheckDialog;
 
+    /// Returns true if demo is running
     bool isDemoRunning();
+    /// Display a secondary plot in the splitter, removing and
+    /// deleting previous one if it exists
+    void showSecondary(QWidget* wid);
+    /// Hide secondary plot
+    void hideSecondary();
     /// Stores settings for all modules
     void saveAllSettings(QSettings* settings);
     /// Load settings for all modules
@@ -101,14 +112,14 @@ private:
 
 private slots:
     void onPortToggled(bool open);
-    void onPortError(QSerialPort::SerialPortError error);
-
+    void onSourceChanged(Source* source);
     void onNumOfSamplesChanged(int value);
-    void onNumOfChannelsChanged(unsigned value);
 
     void clearPlot();
-    void onSpsChanged(unsigned sps);
+    void onSpsChanged(float sps);
     void enableDemo(bool enabled);
+    void showBarPlot(bool show);
+
     void onExportCsv();
     void onSaveSettings();
     void onLoadSettings();
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -14,15 +14,26 @@
    SerialPlot
   
   
-   
+   
     - 
-     
+     
       
-       
+       
         0
         0
        
       
+      
+       Qt::Horizontal
+      
+      
+       
+        
+         0
+         0
+        
+       
+      
      
     
 
     - 
@@ -90,7 +101,7 @@
      0
      0
      653
-     27
+     24
     
    
    
    
-   
   
@@ -142,7 +158,8 @@
     false
    
    
-    
+    
+     ..
    
    
     Pause
@@ -156,7 +173,8 @@
   
   
    
-    
+    
+     ..
    
    
     Clear
@@ -221,6 +239,38 @@
     Load Settings from a File
    
   
+  
+   
+    &Check Update
+   
+  
+  
+   
+    true
+   
+   
+    Bar Plot
+   
+  
+  
+   
+    true
+   
+   
+    Vertical
+   
+  
+  
+   
+    true
+   
+   
+    true
+   
+   
+    Horizontal
+   
+  
  
  
  
diff --git a/src/plot.cpp b/src/plot.cpp
--- a/src/plot.cpp
+++ b/src/plot.cpp
@@ -39,6 +39,7 @@ Plot::Plot(QWidget* parent) :
     isAutoScaled = true;
     symbolSize = 0;
     numOfSamples = 1;
+    plotWidth = 1;
     showSymbols = Plot::ShowSymbolsAuto;
 
     QObject::connect(&zoomer, &Zoomer::unzoomed, this, &Plot::unzoomed);
@@ -73,6 +74,16 @@ Plot::Plot(QWidget* parent) :
     demoIndicator.setText(demoText);
     demoIndicator.hide();
     demoIndicator.attach(this);
+
+    // init no channels are visible indicator
+    QwtText noChannelText(" No Visible Channels ");
+    noChannelText.setColor(QColor("white"));
+    noChannelText.setBackgroundBrush(Qt::darkBlue);
+    noChannelText.setBorderRadius(4);
+    noChannelText.setRenderFlags(Qt::AlignHCenter | Qt::AlignVCenter);
+    noChannelIndicator.setText(noChannelText);
+    noChannelIndicator.hide();
+    noChannelIndicator.attach(this);
 }
 
 Plot::~Plot()
@@ -99,17 +110,18 @@ void Plot::setXAxis(double xMin, double 
     _xMin = xMin;
     _xMax = xMax;
 
+    zoomer.setXLimits(xMin, xMax);
     zoomer.zoom(0); // unzoom
 
     // set axis
-    setAxisScale(QwtPlot::xBottom, xMin, xMax);
+    // setAxisScale(QwtPlot::xBottom, xMin, xMax);
     replot(); // Note: if we don't replot here scale at startup isn't set correctly
 
     // reset zoom base
-    auto base = zoomer.zoomBase();
-    base.setLeft(xMin);
-    base.setRight(xMax);
-    zoomer.setZoomBase(base);
+    // auto base = zoomer.zoomBase();
+    // base.setLeft(xMin);
+    // base.setRight(xMax);
+    // zoomer.setZoomBase(base);
 
     onXScaleChanged();
 }
@@ -163,6 +175,12 @@ void Plot::showDemoIndicator(bool show)
     replot();
 }
 
+void Plot::showNoChannel(bool show)
+{
+    noChannelIndicator.setVisible(show);
+    replot();
+}
+
 void Plot::unzoom()
 {
     zoomer.zoom(0);
@@ -195,36 +213,6 @@ void Plot::darkBackground(bool enabled)
     replot();
 }
 
-/*
-  Below crude drawing demostrates how color selection occurs for
-  given channel index
-
-  0°                     <--Hue Value-->                           360°
-  |* . o . + . o . * . o . + . o . * . o . + . o . * . o . + . o . |
-
-  * -> 0-3
-  + -> 4-7
-  o -> 8-15
-  . -> 16-31
-
- */
-QColor Plot::makeColor(unsigned int channelIndex)
-{
-    auto i = channelIndex;
-
-    if (i < 4)
-    {
-        return QColor::fromHsv(360*i/4, 255, 230);
-    }
-    else
-    {
-        double p = floor(log2(i));
-        double n = pow(2, p);
-        i = i - n;
-        return QColor::fromHsv(360*i/n + 360/pow(2,p+1), 255, 230);
-    }
-}
-
 void Plot::flashSnapshotOverlay(bool light)
 {
     if (snapshotOverlay != NULL) delete snapshotOverlay;
@@ -285,7 +273,8 @@ void Plot::calcSymbolSize()
     auto scaleDist = sw->scaleDraw()->scaleMap().sDist();
     auto fullScaleDist = zoomer.zoomBase().width();
     auto zoomRate = fullScaleDist / scaleDist;
-    float samplesInView = numOfSamples / zoomRate;
+    float plotWidthNumSamp = abs(numOfSamples * plotWidth / (_xMax - _xMin));
+    float samplesInView = plotWidthNumSamp / zoomRate;
     int symDisPx = round(paintDist / samplesInView);
 
     if (symDisPx < SYMBOL_SHOW_AT_WIDTH)
@@ -331,3 +320,9 @@ void Plot::setNumOfSamples(unsigned valu
     numOfSamples = value;
     onXScaleChanged();
 }
+
+void Plot::setPlotWidth(double width)
+{
+    plotWidth = width;
+    zoomer.setHViewSize(width);
+}
diff --git a/src/plot.h b/src/plot.h
--- a/src/plot.h
+++ b/src/plot.h
@@ -50,13 +50,12 @@ public:
     Plot(QWidget* parent = 0);
     ~Plot();
 
-    static QColor makeColor(unsigned int channelIndex);
-
 public slots:
     void showGrid(bool show = true);
     void showMinorGrid(bool show = true);
     void showLegend(bool show = true);
     void showDemoIndicator(bool show = true);
+    void showNoChannel(bool show = true);
     void unzoom();
     void darkBackground(bool enabled = true);
     void setYAxis(bool autoScaled, double yMin = 0, double yMax = 1);
@@ -72,6 +71,8 @@ public slots:
 
     void setNumOfSamples(unsigned value);
 
+    void setPlotWidth(double width);
+
 protected:
     /// update the display of symbols depending on `symbolSize`
     void updateSymbols();
@@ -81,6 +82,7 @@ private:
     double yMin, yMax;
     double _xMin, _xMax;
     unsigned numOfSamples;
+    double plotWidth;
     int symbolSize;
     Zoomer zoomer;
     ScaleZoomer sZoomer;
@@ -88,6 +90,7 @@ private:
     PlotSnapshotOverlay* snapshotOverlay;
     QwtPlotLegendItem legend;
     QwtPlotTextLabel demoIndicator;
+    QwtPlotTextLabel noChannelIndicator;
     ShowSymbols showSymbols;
 
     void resetAxes();
diff --git a/src/plotcontrolpanel.cpp b/src/plotcontrolpanel.cpp
--- a/src/plotcontrolpanel.cpp
+++ b/src/plotcontrolpanel.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -20,6 +20,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include 
 
@@ -29,7 +30,9 @@
 #include "setting_defines.h"
 
 /// Confirm if #samples is being set to a value greater than this
-const int NUMSAMPLES_CONFIRM_AT = 10000;
+const int NUMSAMPLES_CONFIRM_AT = 1000000;
+/// Precision used for channel info table numbers
+const int DOUBLESP_PRECISION = 6;
 
 /// Used for scale range selection combobox
 struct Range
@@ -40,6 +43,25 @@ struct Range
 
 Q_DECLARE_METATYPE(Range);
 
+/// Used for customizing double precision in tables
+class SpinBoxDelegate : public QStyledItemDelegate
+{
+public:
+    QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option,
+                          const QModelIndex &index) const Q_DECL_OVERRIDE
+        {
+            auto w = QStyledItemDelegate::createEditor(
+                parent, option, index);
+
+            auto sp = qobject_cast(w);
+            if (sp)
+            {
+                sp->setDecimals(DOUBLESP_PRECISION);
+            }
+            return w;
+        }
+};
+
 PlotControlPanel::PlotControlPanel(QWidget *parent) :
     QWidget(parent),
     ui(new Ui::PlotControlPanel),
@@ -47,10 +69,16 @@ PlotControlPanel::PlotControlPanel(QWidg
     resetNamesAct(tr("Reset Names"), this),
     resetColorsAct(tr("Reset Colors"), this),
     showAllAct(tr("Show All"), this),
+    hideAllAct(tr("Hide All"), this),
+    resetGainsAct(tr("Reset All Gain"), this),
+    resetOffsetsAct(tr("Reset All Offset"), this),
     resetMenu(tr("Reset Menu"), this)
 {
     ui->setupUi(this);
 
+    delegate = new SpinBoxDelegate();
+    ui->tvChannelInfo->setItemDelegate(delegate);
+
     warnNumOfSamples = true;    // TODO: load from settings
     _numOfSamples = ui->spNumOfSamples->value();
 
@@ -86,9 +114,28 @@ PlotControlPanel::PlotControlPanel(QWidg
     connect(ui->spXmax, SIGNAL(valueChanged(double)),
             this, SLOT(onXScaleChanged()));
 
+    connect(ui->spXmax, static_cast(&QDoubleSpinBox::valueChanged),
+            [this](double v)
+            {
+                // set limit just a little below
+                double step = pow(10, -1 * ui->spXmin->decimals());
+                ui->spXmin->setMaximum(v - step);
+            });
+
     connect(ui->spXmin, SIGNAL(valueChanged(double)),
             this, SLOT(onXScaleChanged()));
 
+    connect(ui->spXmin, static_cast(&QDoubleSpinBox::valueChanged),
+            [this](double v)
+            {
+                // set limit just a little above
+                double step = pow(10, -1 * ui->spXmax->decimals());
+                ui->spXmax->setMinimum(v + step);
+            });
+
+    connect(ui->spPlotWidth, SIGNAL(valueChanged(int)),
+            this, SLOT(onPlotWidthChanged()));
+
     // init scale range preset list
     for (int nbits = 8; nbits <= 24; nbits++) // signed binary formats
     {
@@ -119,12 +166,19 @@ PlotControlPanel::PlotControlPanel(QWidg
     ui->colorSelector->setDisplayMode(color_widgets::ColorPreview::AllAlpha);
     ui->colorSelector->setDisabled(true);
 
-    // reset button
+    // reset buttons
+    resetAct.setToolTip(tr("Reset channel names and colors"));
     resetMenu.addAction(&resetNamesAct);
     resetMenu.addAction(&resetColorsAct);
-    resetMenu.addAction(&showAllAct);
+    resetMenu.addAction(&resetGainsAct);
+    resetMenu.addAction(&resetOffsetsAct);
     resetAct.setMenu(&resetMenu);
     ui->tbReset->setDefaultAction(&resetAct);
+
+    showAllAct.setToolTip(tr("Show all channels"));
+    hideAllAct.setToolTip(tr("Hide all channels"));
+    ui->tbShowAll->setDefaultAction(&showAllAct);
+    ui->tbHideAll->setDefaultAction(&hideAllAct);
 }
 
 PlotControlPanel::~PlotControlPanel()
@@ -275,6 +329,7 @@ void PlotControlPanel::onIndexChecked(bo
 
         emit xScaleChanged(false, ui->spXmin->value(), ui->spXmax->value());
     }
+    emit plotWidthChanged(plotWidth());
 }
 
 void PlotControlPanel::onXScaleChanged()
@@ -282,9 +337,29 @@ void PlotControlPanel::onXScaleChanged()
     if (!xAxisAsIndex())
     {
         emit xScaleChanged(false, ui->spXmin->value(), ui->spXmax->value());
+        emit plotWidthChanged(plotWidth());
     }
 }
 
+double PlotControlPanel::plotWidth() const
+{
+    double value = ui->spPlotWidth->value();
+    if (!xAxisAsIndex())
+    {
+        // scale by xmin and xmax
+        auto xmax = ui->spXmax->value();
+        auto xmin = ui->spXmin->value();
+        double scale = (xmax - xmin) / _numOfSamples;
+        value *= scale;
+    }
+    return value;
+}
+
+void PlotControlPanel::onPlotWidthChanged()
+{
+    emit plotWidthChanged(plotWidth());
+}
+
 void PlotControlPanel::setChannelInfoModel(ChannelInfoModel* model)
 {
     ui->tvChannelInfo->setModel(model);
@@ -355,13 +430,17 @@ void PlotControlPanel::setChannelInfoMod
     connect(&resetAct, &QAction::triggered, model, &ChannelInfoModel::resetInfos);
     connect(&resetNamesAct, &QAction::triggered, model, &ChannelInfoModel::resetNames);
     connect(&resetColorsAct, &QAction::triggered, model, &ChannelInfoModel::resetColors);
-    connect(&showAllAct, &QAction::triggered, model, &ChannelInfoModel::resetVisibility);
+    connect(&resetGainsAct, &QAction::triggered, model, &ChannelInfoModel::resetGains);
+    connect(&resetOffsetsAct, &QAction::triggered, model, &ChannelInfoModel::resetOffsets);
+    connect(&showAllAct, &QAction::triggered, [model]{model->resetVisibility(true);});
+    connect(&hideAllAct, &QAction::triggered, [model]{model->resetVisibility(false);});
 }
 
 void PlotControlPanel::saveSettings(QSettings* settings)
 {
     settings->beginGroup(SettingGroup_Plot);
     settings->setValue(SG_Plot_NumOfSamples, numOfSamples());
+    settings->setValue(SG_Plot_PlotWidth, ui->spPlotWidth->value());
     settings->setValue(SG_Plot_IndexAsX, xAxisAsIndex());
     settings->setValue(SG_Plot_XMax, xMax());
     settings->setValue(SG_Plot_XMin, xMin());
@@ -376,6 +455,8 @@ void PlotControlPanel::loadSettings(QSet
     settings->beginGroup(SettingGroup_Plot);
     ui->spNumOfSamples->setValue(
         settings->value(SG_Plot_NumOfSamples, numOfSamples()).toInt());
+    ui->spPlotWidth->setValue(
+        settings->value(SG_Plot_PlotWidth, ui->spPlotWidth->value()).toInt());
     ui->cbIndex->setChecked(
         settings->value(SG_Plot_IndexAsX, xAxisAsIndex()).toBool());
     ui->spXmax->setValue(settings->value(SG_Plot_XMax, xMax()).toDouble());
diff --git a/src/plotcontrolpanel.h b/src/plotcontrolpanel.h
--- a/src/plotcontrolpanel.h
+++ b/src/plotcontrolpanel.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -24,6 +24,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include "channelinfomodel.h"
 
@@ -46,6 +47,8 @@ public:
     bool   xAxisAsIndex() const;
     double xMax() const;
     double xMin() const;
+    /// Returns the plot width adjusted for x axis scaling.
+    double plotWidth() const;
 
     void setChannelInfoModel(ChannelInfoModel* model);
 
@@ -58,6 +61,7 @@ signals:
     void numOfSamplesChanged(int value);
     void yScaleChanged(bool autoScaled, double yMin = 0, double yMax = 1);
     void xScaleChanged(bool asIndex, double xMin = 0, double xMax = 1);
+    void plotWidthChanged(double width);
 
 private:
     Ui::PlotControlPanel *ui;
@@ -67,8 +71,10 @@ private:
     /// User can disable this setting in the checkbox
     bool warnNumOfSamples;
 
-    QAction resetAct, resetNamesAct, resetColorsAct, showAllAct;
+    QAction resetAct, resetNamesAct, resetColorsAct, showAllAct,
+        hideAllAct, resetGainsAct, resetOffsetsAct;
     QMenu resetMenu;
+    QStyledItemDelegate* delegate;
 
     /// Show a confirmation dialog before setting #samples to a big value
     bool askNSConfirmation(int value);
@@ -80,6 +86,7 @@ private slots:
     void onRangeSelected();
     void onIndexChecked(bool checked);
     void onXScaleChanged();
+    void onPlotWidthChanged();
 };
 
 #endif // PLOTCONTROLPANEL_H
diff --git a/src/plotcontrolpanel.ui b/src/plotcontrolpanel.ui
--- a/src/plotcontrolpanel.ui
+++ b/src/plotcontrolpanel.ui
@@ -6,8 +6,8 @@
    
     0
     0
-    706
-    187
+    704
+    195
    
   
   
@@ -17,7 +17,7 @@
    
- 
     
      
-      
+      
        0
        0
       
@@ -41,14 +41,14 @@
       
- 
        
         
-         
+         
           0
           0
          
         
         
          
-          300
+          30000
           170
          
         
@@ -62,6 +62,9 @@
       
 
       - 
        
+        
+         3
+        
         
          QLayout::SetMaximumSize
         
@@ -95,12 +98,32 @@
           
            
             1
-            20
+            1
            
           
          
         
 
         - 
+         
+          
+           Show all channels
+          
+          
+           Show All
+          
+         
+        
 
+        - 
+         
+          
+           Hide all channels
+          
+          
+           Hide All
+          
+         
+        
 
+        - 
          
           
            Reset
@@ -132,15 +155,30 @@
      
      
- 
       
+       
+        
+       
        
-        Number Of Samples:
+        Buffer Size:
        
       
      
 
      - 
       
+       
+        
+         100
+         0
+        
+       
+       
+        
+         100
+         16777215
+        
+       
        
-        length of X axis
+        Length of acquisition as number of samples
        
        
         false
@@ -149,14 +187,14 @@
         2
        
        
-        1000000
+        10000000
        
        
         1000
        
       
      
 
-     - 
+     
- 
       
        
         Index as X AXis
@@ -166,7 +204,7 @@
        
       
      
 
-     - 
+     
- 
       
        
- 
         
@@ -237,7 +275,7 @@
        
 
       
       
-     - 
+     
- 
       
        
         Auto Scale Y Axis
@@ -247,7 +285,7 @@
        
       
      
 
-     - 
+     
- 
       
        
- 
         
@@ -318,34 +356,59 @@
        
 
       
       
-     - 
+     
- 
       
        
         Select Range Preset:
        
       
      
 
-     - 
+     
- 
       
      
 
+     - 
+      
+       
+        
+       
+       
+        Plot Width:
+       
+      
+     
 
+     - 
+      
+       
+        
+         100
+         0
+        
+       
+       
+        
+         100
+         16777215
+        
+       
+       
+        Width of X axis as maximum number of samples that are shown in plot
+       
+       
+        false
+       
+       
+        2
+       
+       
+        100000
+       
+       
+        1000
+       
+      
+     
 
              
    
-   - 
-    
-     
-      Qt::Horizontal
-     
-     
-      QSizePolicy::MinimumExpanding
-     
-     
-      
-       1
-       20
-      
-     
-    
-   
 
   
  
  
diff --git a/src/plotmanager.cpp b/src/plotmanager.cpp
--- a/src/plotmanager.cpp
+++ b/src/plotmanager.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -17,7 +17,7 @@
   along with serialplot.  If not, see .
 */
 
-#include 
+#include 
 #include 
 #include 
 #include "qwt_symbol.h"
@@ -27,25 +27,69 @@
 #include "utils.h"
 #include "setting_defines.h"
 
-PlotManager::PlotManager(QWidget* plotArea, ChannelInfoModel* infoModel, QObject *parent) :
-    QObject(parent),
-    _plotArea(plotArea),
-    showGridAction("&Grid", this),
-    showMinorGridAction("&Minor Grid", this),
-    unzoomAction("&Unzoom", this),
-    darkBackgroundAction("&Dark Background", this),
-    showLegendAction("&Legend", this),
-    showMultiAction("Multi &Plot", this),
-    setSymbolsAction("Symbols", this)
+PlotManager::PlotManager(QWidget* plotArea, PlotMenu* menu,
+                         const Stream* stream, QObject* parent) :
+    QObject(parent)
 {
+    construct(plotArea, menu);
+    _stream = stream;
+    if (_stream == NULL) return;
+
+    // connect to ChannelInfoModel
+    infoModel = _stream->infoModel();
+    connect(infoModel, &QAbstractItemModel::dataChanged,
+                this, &PlotManager::onChannelInfoChanged);
+    connect(infoModel, &QAbstractItemModel::modelReset,
+            [this]()
+            {
+                onChannelInfoChanged(infoModel->index(0, 0), // start
+                                     infoModel->index(infoModel->rowCount()-1, 0), // end
+                                     {}); // roles ignored
+            });
+
+    connect(stream, &Stream::numChannelsChanged, this, &PlotManager::onNumChannelsChanged);
+    connect(stream, &Stream::dataAdded, this, &PlotManager::replot);
+
+    // add initial curves if any?
+    for (unsigned int i = 0; i < stream->numChannels(); i++)
+    {
+        addCurve(stream->channel(i)->name(), stream->channel(i)->yData());
+    }
+
+}
+
+PlotManager::PlotManager(QWidget* plotArea, PlotMenu* menu,
+                         Snapshot* snapshot, QObject *parent) :
+    QObject(parent)
+{
+    construct(plotArea, menu);
+
+    setNumOfSamples(snapshot->numSamples());
+    setPlotWidth(snapshot->numSamples());
+    infoModel = snapshot->infoModel();
+
+    for (unsigned ci = 0; ci < snapshot->numChannels(); ci++)
+    {
+        addCurve(snapshot->channelName(ci), snapshot->yData[ci]);
+    }
+
+    connect(infoModel, &QAbstractItemModel::dataChanged,
+            this, &PlotManager::onChannelInfoChanged);
+}
+
+void PlotManager::construct(QWidget* plotArea, PlotMenu* menu)
+{
+    _menu = menu;
+    _plotArea = plotArea;
     _autoScaled = true;
     _yMin = 0;
     _yMax = 1;
     _xAxisAsIndex = true;
     isDemoShown = false;
-    _infoModel = infoModel;
     _numOfSamples = 1;
+    _plotWidth = 1;
     showSymbols = Plot::ShowSymbolsAuto;
+    emptyPlot = NULL;
 
     // initalize layout and single widget
     isMulti = false;
@@ -53,93 +97,28 @@ PlotManager::PlotManager(QWidget* plotAr
     setupLayout(isMulti);
     addPlotWidget();
 
-    // initialize menu actions
-    showGridAction.setToolTip("Show Grid");
-    showMinorGridAction.setToolTip("Show Minor Grid");
-    unzoomAction.setToolTip("Unzoom the Plot");
-    darkBackgroundAction.setToolTip("Enable Dark Plot Background");
-    showLegendAction.setToolTip("Display the Legend on Plot");
-    showMultiAction.setToolTip("Display All Channels Separately");
-    setSymbolsAction.setToolTip("Show/Hide symbols");
-
-    showGridAction.setShortcut(QKeySequence("G"));
-    showMinorGridAction.setShortcut(QKeySequence("M"));
-
-    showGridAction.setCheckable(true);
-    showMinorGridAction.setCheckable(true);
-    darkBackgroundAction.setCheckable(true);
-    showLegendAction.setCheckable(true);
-    showMultiAction.setCheckable(true);
-
-    showGridAction.setChecked(false);
-    showMinorGridAction.setChecked(false);
-    darkBackgroundAction.setChecked(false);
-    showLegendAction.setChecked(true);
-    showMultiAction.setChecked(false);
-
-    showMinorGridAction.setEnabled(false);
+    // connect to  menu
+    connect(menu, &PlotMenu::symbolShowChanged, this, &PlotManager:: setSymbols);
 
-    // setup symbols menu
-    setSymbolsAutoAct = setSymbolsMenu.addAction("Show When Zoomed");
-    setSymbolsAutoAct->setCheckable(true);
-    setSymbolsAutoAct->setChecked(true);
-    connect(setSymbolsAutoAct, SELECT::OVERLOAD_OF(&QAction::triggered),
-            [this](bool checked)
-            {
-                if (checked) setSymbols(Plot::ShowSymbolsAuto);
-            });
-    setSymbolsShowAct = setSymbolsMenu.addAction("Always Show");
-    setSymbolsShowAct->setCheckable(true);
-    connect(setSymbolsShowAct, SELECT::OVERLOAD_OF(&QAction::triggered),
-            [this](bool checked)
-            {
-                if (checked) setSymbols(Plot::ShowSymbolsShow);
-            });
-    setSymbolsHideAct = setSymbolsMenu.addAction("Always Hide");
-    setSymbolsHideAct->setCheckable(true);
-    connect(setSymbolsHideAct, SELECT::OVERLOAD_OF(&QAction::triggered),
-            [this](bool checked)
-            {
-                if (checked) setSymbols(Plot::ShowSymbolsHide);
-            });
-    setSymbolsAction.setMenu(&setSymbolsMenu);
-
-    // add symbol actions to same group so that they appear as radio buttons
-    auto group = new QActionGroup(this);
-    group->addAction(setSymbolsAutoAct);
-    group->addAction(setSymbolsShowAct);
-    group->addAction(setSymbolsHideAct);
+    connect(&menu->showGridAction, SELECT::OVERLOAD_OF(&QAction::toggled),
+            this, &PlotManager::showGrid);
+    connect(&menu->showMinorGridAction, SELECT::OVERLOAD_OF(&QAction::toggled),
+            this, &PlotManager::showMinorGrid);
+    connect(&menu->darkBackgroundAction, SELECT::OVERLOAD_OF(&QAction::toggled),
+            this, &PlotManager::darkBackground);
+    connect(&menu->showLegendAction, SELECT::OVERLOAD_OF(&QAction::toggled),
+            this, &PlotManager::showLegend);
+    connect(&menu->showMultiAction, SELECT::OVERLOAD_OF(&QAction::toggled),
+            this, &PlotManager::setMulti);
+    connect(&menu->unzoomAction, &QAction::triggered,
+            this, &PlotManager::unzoom);
 
-    connect(&showGridAction, SELECT::OVERLOAD_OF(&QAction::triggered),
-            this, &PlotManager::showGrid);
-    connect(&showGridAction, SELECT::OVERLOAD_OF(&QAction::triggered),
-            &showMinorGridAction, &QAction::setEnabled);
-    connect(&showMinorGridAction, SELECT::OVERLOAD_OF(&QAction::triggered),
-            this, &PlotManager::showMinorGrid);
-    connect(&unzoomAction, &QAction::triggered, this, &PlotManager::unzoom);
-    connect(&darkBackgroundAction, SELECT::OVERLOAD_OF(&QAction::triggered),
-            this, &PlotManager::darkBackground);
-    connect(&showLegendAction, SELECT::OVERLOAD_OF(&QAction::triggered),
-            this, &PlotManager::showLegend);
-    connect(&showLegendAction, SELECT::OVERLOAD_OF(&QAction::triggered),
-            this, &PlotManager::showLegend);
-    connect(&showMultiAction, SELECT::OVERLOAD_OF(&QAction::triggered),
-            this, &PlotManager::setMulti);
-
-    // connect to channel info model
-    if (_infoModel != NULL)     // TODO: remove when snapshots have infomodel
-    {
-        connect(_infoModel, &QAbstractItemModel::dataChanged,
-                this, &PlotManager::onChannelInfoChanged);
-
-        connect(_infoModel, &QAbstractItemModel::modelReset,
-                [this]()
-                {
-                    onChannelInfoChanged(_infoModel->index(0, 0), // start
-                                         _infoModel->index(_infoModel->rowCount()-1, 0), // end
-                                         {}); // roles ignored
-                });
-    }
+    // initial settings from menu actions
+    showGrid(menu->showGridAction.isChecked());
+    showMinorGrid(menu->showMinorGridAction.isChecked());
+    darkBackground(menu->darkBackgroundAction.isChecked());
+    showLegend(menu->showLegendAction.isChecked());
+    setMulti(menu->showMultiAction.isChecked());
 }
 
 PlotManager::~PlotManager()
@@ -156,6 +135,28 @@ PlotManager::~PlotManager()
     }
 
     if (scrollArea != NULL) delete scrollArea;
+    if (emptyPlot != NULL) delete emptyPlot;
+}
+
+void PlotManager::onNumChannelsChanged(unsigned value)
+{
+    unsigned int oldNum = numOfCurves();
+    unsigned numOfChannels = value;
+
+    if (numOfChannels > oldNum)
+    {
+        // add new channels
+        for (unsigned int i = oldNum; i < numOfChannels; i++)
+        {
+            addCurve(_stream->channel(i)->name(), _stream->channel(i)->yData());
+        }
+    }
+    else if(numOfChannels < oldNum)
+    {
+        removeCurves(oldNum - numOfChannels);
+    }
+
+    replot();
 }
 
 void PlotManager::onChannelInfoChanged(const QModelIndex &topLeft,
@@ -189,6 +190,8 @@ void PlotManager::onChannelInfoChanged(c
         }
     }
 
+    checkNoVisChannels();
+
     // replot single widget
     if (!isMulti)
     {
@@ -198,6 +201,20 @@ void PlotManager::onChannelInfoChanged(c
     }
 }
 
+void PlotManager::checkNoVisChannels()
+{
+    // if all channels are hidden show indicator
+    bool allhidden = std::none_of(curves.cbegin(), curves.cend(),
+                                  [](QwtPlotCurve* c) {return c->isVisible();});
+
+    plotWidgets[0]->showNoChannel(allhidden);
+    if (isMulti)
+    {
+        plotWidgets[0]->showNoChannel(allhidden);
+        plotWidgets[0]->setVisible(true);
+    }
+}
+
 void PlotManager::setMulti(bool enabled)
 {
     if (enabled == isMulti) return;
@@ -224,7 +241,9 @@ void PlotManager::setMulti(bool enabled)
         // add new widgets and attach
         for (auto curve : curves)
         {
-            curve->attach(addPlotWidget());
+            auto plot = addPlotWidget();
+            plot->setVisible(curve->isVisible());
+            curve->attach(plot);
         }
     }
     else
@@ -238,6 +257,12 @@ void PlotManager::setMulti(bool enabled)
             curve->attach(plot);
         }
     }
+
+    // will skip if no plot widgets exist (can happen during constructor)
+    if (plotWidgets.length())
+    {
+        checkNoVisChannels();
+    }
 }
 
 void PlotManager::setupLayout(bool multiPlot)
@@ -284,15 +309,17 @@ Plot* PlotManager::addPlotWidget()
     plotWidgets.append(plot);
     layout->addWidget(plot);
 
-    plot->darkBackground(darkBackgroundAction.isChecked());
-    plot->showGrid(showGridAction.isChecked());
-    plot->showMinorGrid(showMinorGridAction.isChecked());
-    plot->showLegend(showLegendAction.isChecked());
+    plot->darkBackground(_menu->darkBackgroundAction.isChecked());
+    plot->showGrid(_menu->showGridAction.isChecked());
+    plot->showMinorGrid(_menu->showMinorGridAction.isChecked());
+    plot->showLegend(_menu->showLegendAction.isChecked());
+    plot->setSymbols(_menu->showSymbols());
+
     plot->showDemoIndicator(isDemoShown);
     plot->setYAxis(_autoScaled, _yMin, _yMax);
     plot->setNumOfSamples(_numOfSamples);
-    plot->setSymbols(showSymbols);
 
+    plot->setPlotWidth(_plotWidth);
     if (_xAxisAsIndex)
     {
         plot->setXAxis(0, _numOfSamples);
@@ -305,7 +332,7 @@ Plot* PlotManager::addPlotWidget()
     return plot;
 }
 
-void PlotManager::addCurve(QString title, FrameBuffer* buffer)
+void PlotManager::addCurve(QString title, const FrameBuffer* buffer)
 {
     auto curve = new QwtPlotCurve(title);
     auto series = new FrameBufferSeries(buffer);
@@ -314,20 +341,13 @@ void PlotManager::addCurve(QString title
     _addCurve(curve);
 }
 
-void PlotManager::addCurve(QString title, QVector data)
-{
-    auto curve = new QwtPlotCurve(title);
-    curve->setSamples(data);
-    _addCurve(curve);
-}
-
 void PlotManager::_addCurve(QwtPlotCurve* curve)
 {
     // store and init the curve
     curves.append(curve);
 
     unsigned index = curves.size()-1;
-    auto color = _infoModel->color(index);
+    auto color = infoModel->color(index);
     curve->setPen(color);
 
     // create the plot for the curve if we are on multi display
@@ -367,13 +387,6 @@ unsigned PlotManager::numOfCurves()
     return curves.size();
 }
 
-void PlotManager::setTitle(unsigned index, QString title)
-{
-    curves[index]->setTitle(title);
-
-    plotWidget(index)->replot();
-}
-
 Plot* PlotManager::plotWidget(unsigned curveIndex)
 {
     if (isMulti)
@@ -394,19 +407,6 @@ void PlotManager::replot()
     }
 }
 
-QList PlotManager::menuActions()
-{
-    QList actions;
-    actions << &showGridAction;
-    actions << &showMinorGridAction;
-    actions << &unzoomAction;
-    actions << &darkBackgroundAction;
-    actions << &showLegendAction;
-    actions << &showMultiAction;
-    actions << &setSymbolsAction;
-    return actions;
-}
-
 void PlotManager::showGrid(bool show)
 {
     for (auto plot : plotWidgets)
@@ -483,7 +483,6 @@ void PlotManager::setXAxis(bool asIndex,
     _xMax = xMax;
     for (auto curve : curves)
     {
-        // TODO: what happens when addCurve(QVector) is used?
         FrameBufferSeries* series = static_cast(curve->data());
         series->setXAxis(asIndex, xMin, xMax);
     }
@@ -505,7 +504,7 @@ void PlotManager::flashSnapshotOverlay()
 {
     for (auto plot : plotWidgets)
     {
-        plot->flashSnapshotOverlay(darkBackgroundAction.isChecked());
+        plot->flashSnapshotOverlay(_menu->darkBackgroundAction.isChecked());
     }
 }
 
@@ -519,114 +518,11 @@ void PlotManager::setNumOfSamples(unsign
     }
 }
 
-PlotViewSettings PlotManager::viewSettings() const
-{
-    return PlotViewSettings(
-        {
-            showGridAction.isChecked(),
-            showMinorGridAction.isChecked(),
-            darkBackgroundAction.isChecked(),
-            showLegendAction.isChecked(),
-            showMultiAction.isChecked(),
-            showSymbols
-        });
-}
-
-void PlotManager::setViewSettings(const PlotViewSettings& settings)
+void PlotManager::setPlotWidth(double width)
 {
-    showGridAction.setChecked(settings.showGrid);
-    showGrid(settings.showGrid);
-    showMinorGridAction.setChecked(settings.showMinorGrid);
-    showMinorGrid(settings.showMinorGrid);
-    darkBackgroundAction.setChecked(settings.darkBackground);
-    darkBackground(settings.darkBackground);
-    showLegendAction.setChecked(settings.showLegend);
-    showLegend(settings.showLegend);
-    showMultiAction.setChecked(settings.showMulti);
-    setMulti(settings.showMulti);
-
-    setSymbols(settings.showSymbols);
-    if (showSymbols == Plot::ShowSymbolsAuto)
+    _plotWidth = width;
+    for (auto plot : plotWidgets)
     {
-        setSymbolsAutoAct->setChecked(true);
-    }
-    else if (showSymbols == Plot::ShowSymbolsShow)
-    {
-        setSymbolsShowAct->setChecked(true);
-    }
-    else
-    {
-        setSymbolsHideAct->setChecked(true);
+        plot->setPlotWidth(width);
     }
 }
-
-void PlotManager::saveSettings(QSettings* settings)
-{
-    settings->beginGroup(SettingGroup_Plot);
-    settings->setValue(SG_Plot_DarkBackground, darkBackgroundAction.isChecked());
-    settings->setValue(SG_Plot_Grid, showGridAction.isChecked());
-    settings->setValue(SG_Plot_MinorGrid, showMinorGridAction.isChecked());
-    settings->setValue(SG_Plot_Legend, showLegendAction.isChecked());
-    settings->setValue(SG_Plot_MultiPlot, showMultiAction.isChecked());
-
-    QString showSymbolsStr;
-    if (showSymbols == Plot::ShowSymbolsAuto)
-    {
-        showSymbolsStr = "auto";
-    }
-    else if (showSymbols == Plot::ShowSymbolsShow)
-    {
-        showSymbolsStr = "show";
-    }
-    else
-    {
-        showSymbolsStr = "hide";
-    }
-    settings->setValue(SG_Plot_Symbols, showSymbolsStr);
-
-    settings->endGroup();
-}
-
-void PlotManager::loadSettings(QSettings* settings)
-{
-    settings->beginGroup(SettingGroup_Plot);
-    darkBackgroundAction.setChecked(
-        settings->value(SG_Plot_DarkBackground, darkBackgroundAction.isChecked()).toBool());
-    darkBackground(darkBackgroundAction.isChecked());
-    showGridAction.setChecked(
-        settings->value(SG_Plot_Grid, showGridAction.isChecked()).toBool());
-    showGrid(showGridAction.isChecked());
-    showMinorGridAction.setChecked(
-        settings->value(SG_Plot_MinorGrid, showMinorGridAction.isChecked()).toBool());
-    showMinorGridAction.setEnabled(showGridAction.isChecked());
-    showMinorGrid(showMinorGridAction.isChecked());
-    showLegendAction.setChecked(
-        settings->value(SG_Plot_Legend, showLegendAction.isChecked()).toBool());
-    showLegend(showLegendAction.isChecked());
-    showMultiAction.setChecked(
-        settings->value(SG_Plot_MultiPlot, showMultiAction.isChecked()).toBool());
-    setMulti(showMultiAction.isChecked());
-
-    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
-    {
-        qCritical() << "Invalid symbol setting:" << showSymbolsStr;
-    }
-
-    settings->endGroup();
-}
diff --git a/src/plotmanager.h b/src/plotmanager.h
--- a/src/plotmanager.h
+++ b/src/plotmanager.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -26,52 +26,34 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 
 #include 
 #include "plot.h"
 #include "framebufferseries.h"
-#include "channelinfomodel.h"
-
-struct PlotViewSettings
-{
-    bool showGrid;
-    bool showMinorGrid;
-    bool darkBackground;
-    bool showLegend;
-    bool showMulti;
-    Plot::ShowSymbols showSymbols;
-};
+#include "stream.h"
+#include "snapshot.h"
+#include "plotmenu.h"
 
 class PlotManager : public QObject
 {
     Q_OBJECT
 
 public:
-    explicit PlotManager(QWidget* plotArea, ChannelInfoModel* infoModel = NULL, QObject *parent = 0);
+    explicit PlotManager(QWidget* plotArea, PlotMenu* menu,
+                         const Stream* stream = NULL,
+                         QObject *parent = 0);
+    explicit PlotManager(QWidget* plotArea, PlotMenu* menu,
+                         Snapshot* snapshot,
+                         QObject *parent = 0);
     ~PlotManager();
     /// Add a new curve with title and buffer. A color is
     /// automatically chosen for curve.
-    void addCurve(QString title, FrameBuffer* buffer);
-    /// Alternative of `addCurve` for static curve data (snapshots).
-    void addCurve(QString title, QVector data);
-    /// Set the displayed title for a curve
-    void setTitle(unsigned index, QString title);
+    void addCurve(QString title, const FrameBuffer* buffer);
     /// Removes curves from the end
     void removeCurves(unsigned number);
     /// Returns current number of curves known by plot manager
     unsigned numOfCurves();
-    /// Returns the list of actions to be inserted into the `View` menu
-    QList menuActions();
-    /// Returns current status of menu actions
-    PlotViewSettings viewSettings() const;
-    /// Set the current state of view
-    void setViewSettings(const PlotViewSettings& settings);
-    /// Stores plot settings into a `QSettings`.
-    void saveSettings(QSettings* settings);
-    /// Loads plot settings from a `QSettings`.
-    void loadSettings(QSettings* settings);
 
 public slots:
     /// Enable/Disable multiple plot display
@@ -88,15 +70,20 @@ public slots:
     void flashSnapshotOverlay();
     /// Should be called to update zoom base
     void setNumOfSamples(unsigned value);
+    /// Maximum width of X axis (limit of hscroll)
+    void setPlotWidth(double width);
 
 private:
     bool isMulti;
     QWidget* _plotArea;
+    PlotMenu* _menu;
     QVBoxLayout* layout; ///< layout of the `plotArea`
     QScrollArea* scrollArea;
     QList curves;
     QList plotWidgets;
-    ChannelInfoModel* _infoModel;
+    Plot* emptyPlot;  ///< for displaying when all channels are hidden
+    const Stream* _stream;       ///< attached stream, can be `NULL`
+    const ChannelInfoModel* infoModel;
     bool isDemoShown;
     bool _autoScaled;
     double _yMin;
@@ -105,21 +92,12 @@ private:
     double _xMin;
     double _xMax;
     unsigned _numOfSamples;
+    double _plotWidth;
     Plot::ShowSymbols showSymbols;
 
-    // menu actions
-    QAction showGridAction;
-    QAction showMinorGridAction;
-    QAction unzoomAction;
-    QAction darkBackgroundAction;
-    QAction showLegendAction;
-    QAction showMultiAction;
-    QAction setSymbolsAction;
-    QMenu setSymbolsMenu;
-    QAction* setSymbolsAutoAct;
-    QAction* setSymbolsShowAct;
-    QAction* setSymbolsHideAct;
-
+    /// Common constructor
+    void construct(QWidget* plotArea, PlotMenu* menu);
+    /// Setups the layout for multi or single plot
     void setupLayout(bool multiPlot);
     /// Inserts a new plot widget to the current layout.
     Plot* addPlotWidget();
@@ -127,7 +105,8 @@ private:
     Plot* plotWidget(unsigned curveIndex);
     /// Common part of overloaded `addCurve` functions
     void _addCurve(QwtPlotCurve* curve);
-    void setSymbols(Plot::ShowSymbols shown);
+    /// Check and make sure "no visible channels" text is shown
+    void checkNoVisChannels();
 
 private slots:
     void showGrid(bool show = true);
@@ -135,7 +114,9 @@ private slots:
     void showLegend(bool show = true);
     void unzoom();
     void darkBackground(bool enabled = true);
+    void setSymbols(Plot::ShowSymbols shown);
 
+    void onNumChannelsChanged(unsigned value);
     void onChannelInfoChanged(const QModelIndex & topLeft,
                               const QModelIndex & bottomRight,
                               const QVector & roles = QVector ());
diff --git a/src/plotmenu.cpp b/src/plotmenu.cpp
new file mode 100644
--- /dev/null
+++ b/src/plotmenu.cpp
@@ -0,0 +1,224 @@
+/*
+  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 "plotmenu.h"
+#include "setting_defines.h"
+#include "utils.h"
+
+PlotMenu::PlotMenu(QWidget* parent) :
+    QMenu(tr("&View"), parent),
+    showGridAction("&Grid", this),
+    showMinorGridAction("&Minor Grid", this),
+    unzoomAction("&Unzoom", this),
+    darkBackgroundAction("&Dark Background", this),
+    showLegendAction("&Legend", this),
+    showMultiAction("Multi &Plot", this),
+    setSymbolsAction("&Symbols", this),
+    setSymbolsAutoAct("Show When &Zoomed", this),
+    setSymbolsShowAct("Always &Show", this),
+    setSymbolsHideAct("Always &Hide", this)
+{
+    showGridAction.setToolTip("Show Grid");
+    showMinorGridAction.setToolTip("Show Minor Grid");
+    unzoomAction.setToolTip("Unzoom the Plot");
+    darkBackgroundAction.setToolTip("Enable Dark Plot Background");
+    showLegendAction.setToolTip("Display the Legend on Plot");
+    showMultiAction.setToolTip("Display All Channels Separately");
+    setSymbolsAction.setToolTip("Show/Hide symbols");
+
+    showGridAction.setShortcut(QKeySequence("G"));
+    showMinorGridAction.setShortcut(QKeySequence("M"));
+
+    showGridAction.setCheckable(true);
+    showMinorGridAction.setCheckable(true);
+    darkBackgroundAction.setCheckable(true);
+    showLegendAction.setCheckable(true);
+    showMultiAction.setCheckable(true);
+
+    showGridAction.setChecked(false);
+    showMinorGridAction.setChecked(false);
+    darkBackgroundAction.setChecked(false);
+    showLegendAction.setChecked(true);
+    showMultiAction.setChecked(false);
+
+    // minor grid is only enabled when _major_ grid is enabled
+    showMinorGridAction.setEnabled(false);
+    connect(&showGridAction, SELECT::OVERLOAD_OF(&QAction::triggered),
+            &showMinorGridAction, &QAction::setEnabled);
+
+    // setup set symbols menu
+    setSymbolsMenu.addAction(&setSymbolsAutoAct);
+    setSymbolsAutoAct.setCheckable(true);
+    setSymbolsAutoAct.setChecked(true);
+    connect(&setSymbolsAutoAct, SELECT::OVERLOAD_OF(&QAction::triggered),
+            [this](bool checked)
+            {
+                if (checked) emit symbolShowChanged(Plot::ShowSymbolsAuto);
+            });
+
+    setSymbolsMenu.addAction(&setSymbolsShowAct);
+    setSymbolsShowAct.setCheckable(true);
+    connect(&setSymbolsShowAct, SELECT::OVERLOAD_OF(&QAction::triggered),
+            [this](bool checked)
+            {
+                if (checked) symbolShowChanged(Plot::ShowSymbolsShow);
+            });
+
+    setSymbolsMenu.addAction(&setSymbolsHideAct);
+    setSymbolsHideAct.setCheckable(true);
+    connect(&setSymbolsHideAct, SELECT::OVERLOAD_OF(&QAction::triggered),
+            [this](bool checked)
+            {
+                if (checked) symbolShowChanged(Plot::ShowSymbolsHide);
+            });
+
+    // add symbol actions to same group so that they appear as radio buttons
+    auto group = new QActionGroup(this);
+    group->addAction(&setSymbolsAutoAct);
+    group->addAction(&setSymbolsShowAct);
+    group->addAction(&setSymbolsHideAct);
+
+    setSymbolsAction.setMenu(&setSymbolsMenu);
+
+    // add all actions to create this menu
+    addAction(&showGridAction);
+    addAction(&showMinorGridAction);
+    addAction(&unzoomAction);
+    addAction(&darkBackgroundAction);
+    addAction(&showLegendAction);
+    addAction(&showMultiAction);
+    addAction(&setSymbolsAction);
+}
+
+PlotMenu::PlotMenu(PlotViewSettings s, QWidget* parent) :
+    PlotMenu(parent)
+{
+    showGridAction.setChecked(s.showGrid);
+    showMinorGridAction.setChecked(s.showMinorGrid);
+    darkBackgroundAction.setChecked(s.darkBackground);
+    showLegendAction.setChecked(s.showLegend);
+    showMultiAction.setChecked(s.showMulti);
+    switch (s.showSymbols)
+    {
+        case Plot::ShowSymbolsAuto:
+             setSymbolsAutoAct.setChecked(true);
+            break;
+        case Plot::ShowSymbolsShow:
+            setSymbolsShowAct.setChecked(true);
+            break;
+        case Plot::ShowSymbolsHide:
+            setSymbolsHideAct.setChecked(true);
+            break;
+    }
+}
+
+PlotViewSettings PlotMenu::viewSettings() const
+{
+    return PlotViewSettings(
+        {
+            showGridAction.isChecked(),
+            showMinorGridAction.isChecked(),
+            darkBackgroundAction.isChecked(),
+            showLegendAction.isChecked(),
+            showMultiAction.isChecked(),
+            showSymbols()
+        });
+}
+
+Plot::ShowSymbols PlotMenu::showSymbols() const
+{
+    if (setSymbolsAutoAct.isChecked())
+    {
+        return Plot::ShowSymbolsAuto;
+    }
+    else if (setSymbolsShowAct.isChecked())
+    {
+        return Plot::ShowSymbolsShow;
+    }
+    else // setSymbolsHideAct.isChecked()
+    {
+        return Plot::ShowSymbolsHide;
+    }
+}
+
+void PlotMenu::saveSettings(QSettings* settings)
+{
+    settings->beginGroup(SettingGroup_Plot);
+    settings->setValue(SG_Plot_DarkBackground, darkBackgroundAction.isChecked());
+    settings->setValue(SG_Plot_Grid, showGridAction.isChecked());
+    settings->setValue(SG_Plot_MinorGrid, showMinorGridAction.isChecked());
+    settings->setValue(SG_Plot_Legend, showLegendAction.isChecked());
+    settings->setValue(SG_Plot_MultiPlot, showMultiAction.isChecked());
+
+    QString showSymbolsStr;
+    if (showSymbols() == Plot::ShowSymbolsAuto)
+    {
+        showSymbolsStr = "auto";
+    }
+    else if (showSymbols() == Plot::ShowSymbolsShow)
+    {
+        showSymbolsStr = "show";
+    }
+    else
+    {
+        showSymbolsStr = "hide";
+    }
+    settings->setValue(SG_Plot_Symbols, showSymbolsStr);
+
+    settings->endGroup();
+}
+
+void PlotMenu::loadSettings(QSettings* settings)
+{
+    settings->beginGroup(SettingGroup_Plot);
+    darkBackgroundAction.setChecked(
+        settings->value(SG_Plot_DarkBackground, darkBackgroundAction.isChecked()).toBool());
+    showGridAction.setChecked(
+        settings->value(SG_Plot_Grid, showGridAction.isChecked()).toBool());
+    showMinorGridAction.setChecked(
+        settings->value(SG_Plot_MinorGrid, showMinorGridAction.isChecked()).toBool());
+    showMinorGridAction.setEnabled(showGridAction.isChecked());
+    showLegendAction.setChecked(
+        settings->value(SG_Plot_Legend, showLegendAction.isChecked()).toBool());
+    showMultiAction.setChecked(
+        settings->value(SG_Plot_MultiPlot, showMultiAction.isChecked()).toBool());
+
+    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
+    {
+        qCritical() << "Invalid symbol setting:" << showSymbolsStr;
+    }
+
+    settings->endGroup();
+}
diff --git a/src/plotmenu.h b/src/plotmenu.h
new file mode 100644
--- /dev/null
+++ b/src/plotmenu.h
@@ -0,0 +1,73 @@
+/*
+  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 .
+*/
+
+#ifndef PLOTMENU_H
+#define PLOTMENU_H
+
+#include 
+#include 
+#include 
+
+#include "plot.h"
+
+/// Used to quickly transfer view options between different menus
+struct PlotViewSettings
+{
+    bool showGrid;
+    bool showMinorGrid;
+    bool darkBackground;
+    bool showLegend;
+    bool showMulti;
+    Plot::ShowSymbols showSymbols;
+};
+
+class PlotMenu : public QMenu
+{
+    Q_OBJECT
+
+public:
+    PlotMenu(QWidget* parent = 0);
+    PlotMenu(PlotViewSettings s, QWidget* parent = 0);
+
+    QAction showGridAction;
+    QAction showMinorGridAction;
+    QAction unzoomAction;
+    QAction darkBackgroundAction;
+    QAction showLegendAction;
+    QAction showMultiAction;
+    QAction setSymbolsAction;
+    QMenu setSymbolsMenu;
+    QAction setSymbolsAutoAct;
+    QAction setSymbolsShowAct;
+    QAction setSymbolsHideAct;
+
+    /// Returns a bundle of current view settings (menu selections)
+    PlotViewSettings viewSettings() const;
+    /// Selected "show symbol" option
+    Plot::ShowSymbols showSymbols() const;
+    /// Stores plot settings into a `QSettings`.
+    void saveSettings(QSettings* settings);
+    /// Loads plot settings from a `QSettings`.
+    void loadSettings(QSettings* settings);
+
+signals:
+    void symbolShowChanged(Plot::ShowSymbols shown);
+};
+
+#endif // PLOTMENU_H
diff --git a/src/portcontrol.cpp b/src/portcontrol.cpp
--- a/src/portcontrol.cpp
+++ b/src/portcontrol.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2016 Hasan Yavuz Özderya
+  Copyright © 2017 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -48,6 +48,8 @@ PortControl::PortControl(QSerialPort* po
     ui->setupUi(this);
 
     serialPort = port;
+    connect(serialPort, SIGNAL(error(QSerialPort::SerialPortError)),
+            this, SLOT(onPortError(QSerialPort::SerialPortError)));
 
     // setup actions
     openAction.setCheckable(true);
@@ -130,6 +132,40 @@ PortControl::PortControl(QSerialPort* po
                      SELECT::OVERLOAD_OF(&QButtonGroup::buttonClicked),
                      this, &PortControl::selectFlowControl);
 
+    // initialize signal leds
+    ui->ledDTR->setOn(true);
+    ui->ledRTS->setOn(true);
+
+    // connect output signals
+    connect(ui->pbDTR, &QPushButton::clicked, [this]()
+            {
+                // toggle DTR
+                ui->ledDTR->toggle();
+                if (serialPort->isOpen())
+                {
+                    serialPort->setDataTerminalReady(ui->ledDTR->isOn());
+                }
+            });
+
+    connect(ui->pbRTS, &QPushButton::clicked, [this]()
+            {
+                // toggle RTS
+                ui->ledRTS->toggle();
+                if (serialPort->isOpen())
+                {
+                    serialPort->setRequestToSend(ui->ledRTS->isOn());
+                }
+            });
+
+    // setup pin update leds
+    ui->ledDCD->setColor(Qt::yellow);
+    ui->ledDSR->setColor(Qt::yellow);
+    ui->ledRI->setColor(Qt::yellow);
+    ui->ledCTS->setColor(Qt::yellow);
+
+    pinUpdateTimer.setInterval(1000); // ms
+    connect(&pinUpdateTimer, &QTimer::timeout, this, &PortControl::updatePinLeds);
+
     loadPortList();
     loadBaudRateList();
     ui->cbBaudRate->setCurrentIndex(ui->cbBaudRate->findText("9600"));
@@ -221,6 +257,7 @@ void PortControl::togglePort()
 {
     if (serialPort->isOpen())
     {
+        pinUpdateTimer.stop();
         serialPort->close();
         qDebug() << "Closed port:" << serialPort->portName();
         emit portToggled(false);
@@ -258,6 +295,14 @@ void PortControl::togglePort()
             selectStopBits((QSerialPort::StopBits) stopBitsButtons.checkedId());
             selectFlowControl((QSerialPort::FlowControl) flowControlButtons.checkedId());
 
+            // set output signals
+            serialPort->setDataTerminalReady(ui->ledDTR->isOn());
+            serialPort->setRequestToSend(ui->ledRTS->isOn());
+
+            // update pin signals
+            updatePinLeds();
+            pinUpdateTimer.start();
+
             qDebug() << "Opened port:" << serialPort->portName();
             emit portToggled(true);
         }
@@ -319,6 +364,73 @@ void PortControl::onTbPortListActivated(
     ui->cbPortList->setCurrentIndex(index);
 }
 
+void PortControl::onPortError(QSerialPort::SerialPortError error)
+{
+    switch(error)
+    {
+        case QSerialPort::NoError :
+            break;
+        case QSerialPort::ResourceError :
+            qWarning() << "Port error: resource unavaliable; most likely device removed.";
+            if (serialPort->isOpen())
+            {
+                qWarning() << "Closing port on resource error: " << serialPort->portName();
+                togglePort();
+            }
+            loadPortList();
+            break;
+        case QSerialPort::DeviceNotFoundError:
+            qCritical() << "Device doesn't exists: " << serialPort->portName();
+            break;
+        case QSerialPort::PermissionError:
+            qCritical() << "Permission denied. Either you don't have \
+required privileges or device is already opened by another process.";
+            break;
+        case QSerialPort::OpenError:
+            qWarning() << "Device is already opened!";
+            break;
+        case QSerialPort::NotOpenError:
+            qCritical() << "Device is not open!";
+            break;
+        case QSerialPort::ParityError:
+            qCritical() << "Parity error detected.";
+            break;
+        case QSerialPort::FramingError:
+            qCritical() << "Framing error detected.";
+            break;
+        case QSerialPort::BreakConditionError:
+            qCritical() << "Break condition is detected.";
+            break;
+        case QSerialPort::WriteError:
+            qCritical() << "An error occurred while writing data.";
+            break;
+        case QSerialPort::ReadError:
+            qCritical() << "An error occurred while reading data.";
+            break;
+        case QSerialPort::UnsupportedOperationError:
+            qCritical() << "Operation is not supported.";
+            break;
+        case QSerialPort::TimeoutError:
+            qCritical() << "A timeout error occurred.";
+            break;
+        case QSerialPort::UnknownError:
+            qCritical() << "Unknown error! Error: " << serialPort->errorString();
+            break;
+        default:
+            qCritical() << "Unhandled port error: " << error;
+            break;
+    }
+}
+
+void PortControl::updatePinLeds(void)
+{
+    auto pins = serialPort->pinoutSignals();
+    ui->ledDCD->setOn(pins & QSerialPort::DataCarrierDetectSignal);
+    ui->ledDSR->setOn(pins & QSerialPort::DataSetReadySignal);
+    ui->ledRI->setOn(pins & QSerialPort::RingIndicatorSignal);
+    ui->ledCTS->setOn(pins & QSerialPort::ClearToSendSignal);
+}
+
 QString PortControl::currentParityText()
 {
     return paritySettingMap.value(
diff --git a/src/portcontrol.h b/src/portcontrol.h
--- a/src/portcontrol.h
+++ b/src/portcontrol.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2016 Hasan Yavuz Özderya
+  Copyright © 2017 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -28,6 +28,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #include "portlist.h"
 
@@ -65,6 +66,9 @@ private:
     QComboBox tbPortList;
     PortList portList;
 
+    /// Used to refresh pinout signal leds periodically
+    QTimer pinUpdateTimer;
+
     /// Returns the currently selected (entered) "portName" in the UI
     QString selectedPortName();
     /// Returns currently selected parity as text to be saved in settings
@@ -86,9 +90,10 @@ public slots:
 
 private slots:
     void openActionTriggered(bool checked);
-
     void onCbPortListActivated(int index);
     void onTbPortListActivated(int index);
+    void onPortError(QSerialPort::SerialPortError error);
+    void updatePinLeds(void);
 
 signals:
     void portToggled(bool open);
diff --git a/src/portcontrol.ui b/src/portcontrol.ui
--- a/src/portcontrol.ui
+++ b/src/portcontrol.ui
@@ -33,6 +33,9 @@
            0
           
          
+         
+          You can enter a port name even if it's not listed, such as pseudo terminals.
+         
          
           true
          
@@ -40,6 +43,9 @@
        
        - 
         
+         
+          You can enter a custom baud rate if it's supported by your OS/adapter.
+         
          
           Qt::ImhPreferNumbers
          
@@ -70,7 +76,7 @@
           
          
          
-          
+          Reload the list of ports
          
          
           ↺
@@ -301,6 +307,224 @@
       
      
 
      - 
+      
+       
+        2
+       
+       
- 
+        
+         
+          
+           15
+           15
+          
+         
+         
+          
+           15
+           15
+          
+         
+         
+          Request To Send
+         
+        
+       
 
+       - 
+        
+         
+          Qt::Horizontal
+         
+         
+          
+           40
+           20
+          
+         
+        
+       
 
+       - 
+        
+         
+          
+           40
+           20
+          
+         
+         
+          Data Terminal Ready
+         
+         
+          DTR
+         
+        
+       
 
+       - 
+        
+         
+          
+           15
+           15
+          
+         
+         
+          
+           15
+           15
+          
+         
+         
+          Data Terminal Ready
+         
+        
+       
 
+       - 
+        
+         
+          
+           15
+           15
+          
+         
+         
+          
+           15
+           15
+          
+         
+         
+          Data Set Ready
+         
+        
+       
 
+       - 
+        
+         
+          Data Set Ready
+         
+         
+          DSR
+         
+         
+          Qt::AlignCenter
+         
+        
+       
 
+       - 
+        
+         
+          
+           40
+           20
+          
+         
+         
+          Request To Send
+         
+         
+          RTS
+         
+        
+       
 
+       - 
+        
+         
+          Data Carrier Detect
+         
+         
+          DCD
+         
+         
+          Qt::AlignCenter
+         
+        
+       
 
+       - 
+        
+         
+          
+           15
+           15
+          
+         
+         
+          
+           15
+           15
+          
+         
+         
+          Data Carrier Detect
+         
+        
+       
 
+       - 
+        
+         
+          Ring Indicator
+         
+         
+          RI
+         
+         
+          Qt::AlignCenter
+         
+        
+       
 
+       - 
+        
+         
+          
+           15
+           15
+          
+         
+         
+          
+           15
+           15
+          
+         
+         
+          Ring Indicator
+         
+        
+       
 
+       - 
+        
+         
+          
+           15
+           15
+          
+         
+         
+          
+           15
+           15
+          
+         
+         
+          Clear To Send
+         
+        
+       
 
+       - 
+        
+         
+          Clear To Send
+         
+         
+          CTS
+         
+         
+          Qt::AlignCenter
+         
+        
+       
 
+      
+      
+     - 
       
        
         Qt::Vertical
@@ -308,7 +532,7 @@
        
         
          20
-         40
+         1
         
        
       
@@ -317,6 +541,14 @@
    
 
   
  
+ 
+  
+   LedWidget
+   QWidget
+   
+   1
+  
+ 
  
   cbPortList
   pbReloadPorts
diff --git a/src/portlist.cpp b/src/portlist.cpp
--- a/src/portlist.cpp
+++ b/src/portlist.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2016 Hasan Yavuz Özderya
+  Copyright © 2017 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -50,7 +50,8 @@ void PortListItem::construct(QString nam
     {
         text += QString(" ") + description;
     }
-    if (vid && pid)
+    // Note: in some cases internal ports or RS232 ports may have VID&PID
+    if (vid && pid && !name.contains("tty"))
     {
         text += QString("[%1:").arg(vid, 4, 16, QChar('0'));
         text += QString("%1]").arg(pid, 4, 16, QChar('0'));
diff --git a/src/readonlybuffer.cpp b/src/readonlybuffer.cpp
new file mode 100644
--- /dev/null
+++ b/src/readonlybuffer.cpp
@@ -0,0 +1,103 @@
+/*
+  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 
+#include 
+
+#include "readonlybuffer.h"
+
+ReadOnlyBuffer::ReadOnlyBuffer(const FrameBuffer* source) :
+    ReadOnlyBuffer(source, 0, source->size())
+{
+    // intentionally empty, see ↑
+}
+
+ReadOnlyBuffer::ReadOnlyBuffer(const FrameBuffer* source, unsigned start, unsigned n)
+{
+    Q_ASSERT(source->size() > 0);
+    Q_ASSERT(start + n <= source->size());
+
+    _size = n;
+    data = new double[_size];
+
+    for (unsigned i = 0; i < n; i++)
+    {
+        data[i] = source->sample(start + i);
+    }
+
+    /// if not exact copy of source re-calculate limits
+    if (start == 0 && n == source->size())
+    {
+        _limits = source->limits();
+    }
+    else
+    {
+        updateLimits();
+    }
+}
+
+ReadOnlyBuffer::ReadOnlyBuffer(const double* source, unsigned ssize)
+{
+    Q_ASSERT(source != nullptr && ssize);
+
+    _size = ssize;
+    data = new double[_size];
+    memcpy(data, source, sizeof(double) * ssize);
+    updateLimits();
+}
+
+ReadOnlyBuffer::~ReadOnlyBuffer()
+{
+    delete[] data;
+}
+
+unsigned ReadOnlyBuffer::size() const
+{
+    return _size;
+}
+
+double ReadOnlyBuffer::sample(unsigned i) const
+{
+    return data[i];
+}
+
+Range ReadOnlyBuffer::limits() const
+{
+    return _limits;
+}
+
+void ReadOnlyBuffer::updateLimits()
+{
+    Q_ASSERT(_size);
+
+    _limits.start = data[0];
+    _limits.end = data[0];
+
+    for (unsigned i = 0; i < _size; i++)
+    {
+        if (data[i] > _limits.end)
+        {
+            _limits.end = data[i];
+        }
+        else if (data[i] < _limits.start)
+        {
+            _limits.start = data[i];
+        }
+    }
+}
diff --git a/src/readonlybuffer.h b/src/readonlybuffer.h
new file mode 100644
--- /dev/null
+++ b/src/readonlybuffer.h
@@ -0,0 +1,62 @@
+/*
+  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 .
+*/
+
+#ifndef READONLYBUFFER_H
+#define READONLYBUFFER_H
+
+#include "framebuffer.h"
+
+/// A read only frame buffer used for storing snapshot data. Main advantage of
+/// this compared to `RingBuffer` is that reading data should be somewhat
+/// faster.
+class ReadOnlyBuffer : public FrameBuffer
+{
+public:
+    /// Creates a buffer with data copied from `source`. Source buffer cannot be
+    /// empty.
+    ReadOnlyBuffer(const FrameBuffer* source);
+
+    /// Creates a buffer from a slice of the `source`.
+    ///
+    /// @param start start of the slice
+    /// @param n number of samples
+    ///
+    /// @important (start + n) should be smaller or equal than `source->size()`,
+    /// otherwise it's an error.
+    ReadOnlyBuffer(const FrameBuffer* source, unsigned start, unsigned n);
+
+    /// Creates a buffer with data copied from an array
+    ReadOnlyBuffer(const double* source, unsigned ssize);
+
+    ~ReadOnlyBuffer();
+
+    virtual unsigned size() const;
+    virtual double sample(unsigned i) const;
+    virtual Range limits() const;
+
+private:
+    double* data;    ///< data storage
+    unsigned _size;  ///< data size
+    Range _limits;   ///< limits cache
+
+    // TODO: duplicate with `RingBuffer`
+    void updateLimits(); ///< Updates limits cache
+};
+
+#endif // READONLYBUFFER_H
diff --git a/src/recordpanel.cpp b/src/recordpanel.cpp
--- a/src/recordpanel.cpp
+++ b/src/recordpanel.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -23,21 +23,25 @@
 #include 
 #include 
 #include 
+#include 
+#include 
+#include 
 #include 
+#include 
 
 #include "recordpanel.h"
 #include "ui_recordpanel.h"
 #include "setting_defines.h"
 
-RecordPanel::RecordPanel(DataRecorder* recorder, ChannelManager* channelMan, QWidget *parent) :
+RecordPanel::RecordPanel(Stream* stream, QWidget *parent) :
     QWidget(parent),
     ui(new Ui::RecordPanel),
     recordToolBar(tr("Record Toolbar")),
-    recordAction(QIcon::fromTheme("media-record"), tr("Record"), this)
+    recordAction(QIcon::fromTheme("media-record"), tr("Record"), this),
+    recorder(this)
 {
     overwriteSelected = false;
-    _recorder = recorder;
-    _channelMan = channelMan;
+    _stream = stream;
 
     ui->setupUi(this);
 
@@ -58,16 +62,25 @@ RecordPanel::RecordPanel(DataRecorder* r
     connect(ui->cbDisableBuffering, &QCheckBox::toggled,
             [this](bool enabled)
             {
-                _recorder->disableBuffering = enabled;
+                recorder.disableBuffering = enabled;
             });
 
     connect(ui->cbWindowsLE, &QCheckBox::toggled,
             [this](bool enabled)
             {
-                _recorder->windowsLE = enabled;
+                recorder.windowsLE = enabled;
             });
 
     connect(&recordAction, &QAction::toggled, ui->cbWindowsLE, &QWidget::setDisabled);
+    connect(&recordAction, &QAction::toggled, ui->cbTimestamp, &QWidget::setDisabled);
+    connect(&recordAction, &QAction::toggled, ui->leSeparator, &QWidget::setDisabled);
+    connect(&recordAction, &QAction::toggled, ui->pbBrowse, &QWidget::setDisabled);
+
+    QCompleter *completer = new QCompleter(this);
+    // TODO: QDirModel is deprecated, use QFileSystemModel (but it doesn't work)
+    completer->setModel(new QDirModel(completer));
+    completer->setCaseSensitivity(Qt::CaseInsensitive);
+    ui->leFileName->setCompleter(completer);
 }
 
 RecordPanel::~RecordPanel()
@@ -80,11 +93,6 @@ QToolBar* RecordPanel::toolbar()
     return &recordToolBar;
 }
 
-bool RecordPanel::isRecording()
-{
-    return recordAction.isChecked();
-}
-
 bool RecordPanel::recordPaused()
 {
     return ui->cbRecordPaused->isChecked();
@@ -101,13 +109,75 @@ bool RecordPanel::selectFile()
     }
     else
     {
-        selectedFile = fileName;
-        ui->lbFileName->setText(selectedFile);
+        setSelectedFile(fileName);
         overwriteSelected = QFile::exists(fileName);
         return true;
     }
 }
 
+QString RecordPanel::selectedFile() const
+{
+    return ui->leFileName->text();
+}
+
+void RecordPanel::setSelectedFile(QString f)
+{
+    ui->leFileName->setText(f);
+}
+
+QString RecordPanel::getSelectedFile()
+{
+    if (selectedFile().isEmpty())
+    {
+        if (!selectFile()) return QString();
+    }
+
+    // assume that file name contains a time format specifier
+    if (selectedFile().contains("%"))
+    {
+        auto ts = formatTimeStamp(selectedFile());
+        if (!QFile::exists(ts) || // file doesn't exists
+            confirmOverwrite(ts)) // exists but user accepted overwrite
+        {
+            return ts;
+        }
+        return QString();
+    }
+
+    // if no timestamp and file exists try autoincrement option
+    if (!overwriteSelected && QFile::exists(selectedFile()))
+    {
+        if (ui->cbAutoIncrement->isChecked())
+        {
+            if (!incrementFileName()) return QString();
+        }
+        else
+        {
+            if (!confirmOverwrite(selectedFile()))
+                return QString();
+        }
+    }
+
+    return selectedFile();
+}
+
+QString RecordPanel::formatTimeStamp(QString t) const
+{
+    auto maxSize = t.size() + 1024;
+    auto r = new char[maxSize];
+
+    time_t rawtime;
+    struct tm * timeinfo;
+
+    time(&rawtime);
+    timeinfo = localtime (&rawtime);
+    strftime(r, maxSize, t.toLatin1().data(), timeinfo);
+
+    auto rs = QString(r);
+    delete r;
+    return rs;
+}
+
 void RecordPanel::onRecord(bool start)
 {
     if (!start)
@@ -126,22 +196,11 @@ void RecordPanel::onRecord(bool start)
     }
 
     // check file name
-    if (!canceled && selectedFile.isEmpty() && !selectFile())
-    {
-        canceled = true;
-    }
-
-    if (!canceled && !overwriteSelected && QFile::exists(selectedFile))
+    QString fn;
+    if (!canceled)
     {
-        if (ui->cbAutoIncrement->isChecked())
-        {
-            // TODO: should we increment even if user selected to replace?
-            canceled = !incrementFileName();
-        }
-        else
-        {
-            canceled = !confirmOverwrite(selectedFile);
-        }
+        fn = getSelectedFile();
+        canceled = fn.isEmpty();
     }
 
     if (canceled)
@@ -151,13 +210,15 @@ void RecordPanel::onRecord(bool start)
     else
     {
         overwriteSelected = false;
-        startRecording();
+        // TODO: show more visible error message when recording fails
+        if (!startRecording(fn))
+            recordAction.setChecked(false);
     }
 }
 
 bool RecordPanel::incrementFileName(void)
 {
-    QFileInfo fileInfo(selectedFile);
+    QFileInfo fileInfo(selectedFile());
 
     QString base = fileInfo.completeBaseName();
     QRegularExpression regex("(.*?)(\\d+)(?!.*\\d)(.*)");
@@ -192,10 +253,9 @@ bool RecordPanel::incrementFileName(void
     }
     else
     {
-        selectedFile = autoFileName;
+        setSelectedFile(autoFileName);
     }
 
-    ui->lbFileName->setText(selectedFile);
     return true;
 }
 
@@ -222,7 +282,7 @@ bool RecordPanel::confirmOverwrite(QStri
     }
     else if (mb.clickedButton() == bOverwrite)
     {
-        selectedFile = fileName;
+        setSelectedFile(fileName);
         return true;
     }
     else                    // select button
@@ -231,21 +291,29 @@ bool RecordPanel::confirmOverwrite(QStri
     }
 }
 
-void RecordPanel::startRecording(void)
+bool RecordPanel::startRecording(QString fileName)
 {
     QStringList channelNames;
     if (ui->cbHeader->isChecked())
     {
-        channelNames = _channelMan->infoModel()->channelNames();
+        channelNames = _stream->infoModel()->channelNames();
     }
-    _recorder->startRecording(selectedFile, getSeparator(), channelNames);
-    emit recordStarted();
+    if (recorder.startRecording(fileName, getSeparator(),
+                                channelNames, ui->cbTimestamp->isChecked()))
+    {
+        _stream->connectFollower(&recorder);
+        return true;
+    }
+    else
+    {
+        return false;
+    }
 }
 
 void RecordPanel::stopRecording(void)
 {
-    emit recordStopped();
-    _recorder->stopRecording();
+    recorder.stopRecording();
+    _stream->disconnectFollower(&recorder);
 }
 
 void RecordPanel::onPortClose()
@@ -272,6 +340,7 @@ void RecordPanel::saveSettings(QSettings
     settings->setValue(SG_Record_StopOnClose, ui->cbStopOnClose->isChecked());
     settings->setValue(SG_Record_Header, ui->cbHeader->isChecked());
     settings->setValue(SG_Record_DisableBuffering, ui->cbDisableBuffering->isChecked());
+    settings->setValue(SG_Record_Timestamp, ui->cbTimestamp->isChecked());
     settings->setValue(SG_Record_Separator, ui->leSeparator->text());
     settings->endGroup();
 }
@@ -289,6 +358,8 @@ void RecordPanel::loadSettings(QSettings
         settings->value(SG_Record_Header, ui->cbHeader->isChecked()).toBool());
     ui->cbDisableBuffering->setChecked(
         settings->value(SG_Record_DisableBuffering, ui->cbDisableBuffering->isChecked()).toBool());
+    ui->cbTimestamp->setChecked(
+        settings->value(SG_Record_Timestamp, ui->cbTimestamp->isChecked()).toBool());
     ui->leSeparator->setText(settings->value(SG_Record_Separator, ui->leSeparator->text()).toString());
     settings->endGroup();
 }
diff --git a/src/recordpanel.h b/src/recordpanel.h
--- a/src/recordpanel.h
+++ b/src/recordpanel.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -26,7 +26,7 @@
 #include 
 
 #include "datarecorder.h"
-#include "channelmanager.h"
+#include "stream.h"
 
 namespace Ui {
 class RecordPanel;
@@ -37,13 +37,11 @@ class RecordPanel : public QWidget
     Q_OBJECT
 
 public:
-    explicit RecordPanel(DataRecorder* recorder, ChannelManager* channelMan,
-                         QWidget* parent = 0);
+    explicit RecordPanel(Stream* stream, QWidget* parent = 0);
     ~RecordPanel();
 
     QToolBar* toolbar();
 
-    bool isRecording();
     bool recordPaused();
 
     /// Stores settings into a `QSettings`
@@ -64,10 +62,9 @@ private:
     Ui::RecordPanel *ui;
     QToolBar recordToolBar;
     QAction recordAction;
-    QString selectedFile;
     bool overwriteSelected;
-    DataRecorder* _recorder;
-    ChannelManager* _channelMan;
+    DataRecorder recorder;
+    Stream* _stream;
 
     /**
      * @brief Increments the file name.
@@ -92,7 +89,26 @@ private:
      */
     bool confirmOverwrite(QString fileName);
 
-    void startRecording(void);
+    /// Returns filename in edit box. May be invalid!
+    QString selectedFile() const;
+    /// Sets the filename in edit box.
+    void setSelectedFile(QString f);
+
+    /**
+     * Tries to get a valid file name by handling user interactions and
+     * automatic naming (increment, timestamp etc).
+     *
+     * Returned file name can be used immediately. File name box should also be
+     * set to selected file name.
+     *
+     * @return empty if failure otherwise valid filename
+     */
+    QString getSelectedFile();
+
+    /// Formats timestamp in given text
+    QString formatTimeStamp(QString t) const;
+
+    bool startRecording(QString fileName);
     void stopRecording(void);
 
     /// Returns separator text from ui. "\t" is converted to TAB
diff --git a/src/recordpanel.ui b/src/recordpanel.ui
--- a/src/recordpanel.ui
+++ b/src/recordpanel.ui
@@ -7,7 +7,7 @@
     0
     0
     627
-    261
+    207
    
   
   
@@ -19,6 +19,22 @@
      - 
       
        
- 
+        
+         
+          
+           0
+           0
+          
+         
+         
+          You can use C `strftime` function format specifiers for timestamps in your file name.
+         
+         
+          Enter file name or browse
+         
+        
+       
 
+       - 
         
          
           Select record file
@@ -28,23 +44,56 @@
          
         
        
 
-       - 
-        
-         
-          
-           0
-           0
-          
-         
-         
-          Select file...
-         
-        
-       
 
       
       
      - 
       
+       
- 
+        
+         
+          Stop recording when port closed
+         
+         
+          true
+         
+        
+       
 
+       - 
+        
+         
+          Increments file name automatically everytime a new recording starts
+         
+         
+          Auto increment file name
+         
+         
+          true
+         
+        
+       
 
+       - 
+        
+         
+          Do not buffer when writing to file. Check this if you are using other software to open the file during recording.
+         
+         
+          Disable buffering
+         
+        
+       
 
+       - 
+        
+         
+          Use CR+LF as line endings. Some windows software may not show lines correctly otherwise. Can't be changed during recording.
+         
+         
+          Windows Style Line Endings
+         
+         
+          false
+         
+        
+       
 
        - 
         
          
@@ -71,52 +120,6 @@
          
         
        
 
-       - 
-        
-         
-          Do not buffer when writing to file. Check this if you are using other software to open the file during recording.
-         
-         
-          Disable buffering
-         
-        
-       
 
-       - 
-        
-         
-          Stop recording when port closed
-         
-         
-          true
-         
-        
-       
 
-       - 
-        
-         
-          Increments file name automatically everytime a new recording starts
-         
-         
-          Auto increment file name
-         
-         
-          true
-         
-        
-       
 
-       - 
-        
-         
-          Use CR+LF as line endings. Some windows software may not show lines correctly otherwise. Can't be changed during recording.
-         
-         
-          Windows Style Line Endings
-         
-         
-          false
-         
-        
-       
 
        - 
         
          
@@ -130,6 +133,16 @@
          
         
        
 
+       - 
+        
+         
+          Insert timestamp (milliseconds from epoch) as first column
+         
+         
+          Insert timestamp
+         
+        
+       
 
       
       
      - 
diff --git a/src/ringbuffer.cpp b/src/ringbuffer.cpp
new file mode 100644
--- /dev/null
+++ b/src/ringbuffer.cpp
@@ -0,0 +1,172 @@
+/*
+  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 
+
+#include "ringbuffer.h"
+
+RingBuffer::RingBuffer(unsigned n)
+{
+    _size = n;
+    data = new double[_size]();
+    headIndex = 0;
+
+    limInvalid = false;
+    limCache = {0, 0};
+}
+
+RingBuffer::~RingBuffer()
+{
+    delete[] data;
+}
+
+unsigned RingBuffer::size() const
+{
+    return _size;
+}
+
+double RingBuffer::sample(unsigned i) const
+{
+    unsigned index = headIndex + i;
+    if (index >= _size) index -= _size;
+    return data[index];
+}
+
+Range RingBuffer::limits() const
+{
+    if (limInvalid) updateLimits();
+    return limCache;
+}
+
+void RingBuffer::resize(unsigned n)
+{
+    Q_ASSERT(n != _size);
+
+    int offset = (int) n - (int) _size;
+    if (offset == 0) return;
+
+    double* newData = new double[n];
+
+    // move data to new array
+    int fill_start = offset > 0 ? offset : 0;
+
+    for (int i = fill_start; i < int(n); i++)
+    {
+        newData[i] = sample(i - offset);
+    }
+
+    // fill the beginning of the new data
+    if (fill_start > 0)
+    {
+        for (int i = 0; i < fill_start; i++)
+        {
+            newData[i] = 0;
+        }
+    }
+
+    // data is ready, clean up and re-point
+    delete data;
+    data = newData;
+    headIndex = 0;
+    _size = n;
+
+    // invalidate bounding rectangle
+    limInvalid = true;
+}
+
+void RingBuffer::addSamples(double* samples, unsigned n)
+{
+    unsigned shift = n;
+    if (shift < _size)
+    {
+        unsigned x = _size - headIndex; // distance of `head` to end
+
+        if (shift <= x) // there is enough room at the end of array
+        {
+            for (unsigned i = 0; i < shift; i++)
+            {
+                data[i+headIndex] = samples[i];
+            }
+
+            if (shift == x) // we used all the room at the end
+            {
+                headIndex = 0;
+            }
+            else
+            {
+                headIndex += shift;
+            }
+        }
+        else // there isn't enough room
+        {
+            for (unsigned i = 0; i < x; i++) // fill the end part
+            {
+                data[i+headIndex] = samples[i];
+            }
+            for (unsigned i = 0; i < (shift-x); i++) // continue from the beginning
+            {
+                data[i] = samples[i+x];
+            }
+            headIndex = shift-x;
+        }
+    }
+    else // number of new samples equal or bigger than current size (doesn't fit)
+    {
+        int x = shift - _size;
+        for (unsigned i = 0; i < _size; i++)
+        {
+            data[i] = samples[i+x];
+        }
+        headIndex = 0;
+    }
+
+    // invalidate cache
+    limInvalid = true;
+}
+
+void RingBuffer::clear()
+{
+    for (unsigned i=0; i < _size; i++)
+    {
+        data[i] = 0.;
+    }
+
+    limCache = {0, 0};
+    limInvalid = false;
+}
+
+void RingBuffer::updateLimits() const
+{
+    limCache.start = data[0];
+    limCache.end = data[0];
+
+    for (unsigned i = 0; i < _size; i++)
+    {
+        if (data[i] > limCache.end)
+        {
+            limCache.end = data[i];
+        }
+        else if (data[i] < limCache.start)
+        {
+            limCache.start = data[i];
+        }
+    }
+
+    limInvalid = false;
+}
diff --git a/src/ringbuffer.h b/src/ringbuffer.h
new file mode 100644
--- /dev/null
+++ b/src/ringbuffer.h
@@ -0,0 +1,49 @@
+/*
+  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 .
+*/
+
+#ifndef RINGBUFFER_H
+#define RINGBUFFER_H
+
+#include "framebuffer.h"
+
+/// A fast buffer implementation for storing data.
+class RingBuffer : public WFrameBuffer
+{
+public:
+    RingBuffer(unsigned n);
+    ~RingBuffer();
+
+    virtual unsigned size() const;
+    virtual double sample(unsigned i) const;
+    virtual Range limits() const;
+    virtual void resize(unsigned n);
+    virtual void addSamples(double* samples, unsigned n);
+    virtual void clear();
+
+private:
+    unsigned _size;            ///< size of `data`
+    double* data;              ///< storage
+    unsigned headIndex;        ///< indicates the actual `0` index of the ring buffer
+
+    mutable bool limInvalid;   ///< Indicates that limits needs to be re-calculated
+    mutable Range limCache;    ///< Cache for limits()
+    void updateLimits() const; ///< Updates limits cache
+};
+
+#endif
diff --git a/src/samplecounter.cpp b/src/samplecounter.cpp
new file mode 100644
--- /dev/null
+++ b/src/samplecounter.cpp
@@ -0,0 +1,44 @@
+/*
+  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 
+#include "samplecounter.h"
+
+SampleCounter::SampleCounter()
+{
+    prevTimeMs = QDateTime::currentMSecsSinceEpoch();
+    count = 0;
+}
+
+#include 
+
+void SampleCounter::feedIn(const SamplePack& data)
+{
+    count += data.numSamples();
+
+    qint64 current = QDateTime::currentMSecsSinceEpoch();
+    auto diff = current - prevTimeMs;
+    if (diff > 1000) // 1sec
+    {
+        emit spsChanged(1000 * float(count) / diff);
+
+        prevTimeMs = current;
+        count = 0;
+    }
+}
diff --git a/src/samplecounter.h b/src/samplecounter.h
new file mode 100644
--- /dev/null
+++ b/src/samplecounter.h
@@ -0,0 +1,47 @@
+/*
+  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 .
+*/
+
+#ifndef SAMPLECOUNTER_H
+#define SAMPLECOUNTER_H
+
+#include 
+#include "sink.h"
+
+/// A `Sink` class for counting and reporting number of samples per second.
+class SampleCounter : public QObject, public Sink
+{
+    Q_OBJECT
+
+public:
+    SampleCounter();
+
+protected:
+    // implementations for `Sink`
+    virtual void feedIn(const SamplePack& data);
+
+signals:
+    /// Emitted per second if SPS value has changed.
+    void spsChanged(float value);
+
+private:
+    qint64 prevTimeMs;
+    unsigned count;
+};
+
+#endif // SAMPLECOUNTER_H
diff --git a/src/samplepack.cpp b/src/samplepack.cpp
new file mode 100644
--- /dev/null
+++ b/src/samplepack.cpp
@@ -0,0 +1,98 @@
+/*
+  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 
+#include 
+
+#include "samplepack.h"
+
+SamplePack::SamplePack(unsigned ns, unsigned nc, bool x)
+{
+    Q_ASSERT(ns > 0 && nc > 0);
+
+    _numSamples = ns;
+    _numChannels = nc;
+
+    _yData = new double[_numSamples * _numChannels]();
+    if (x)
+    {
+        _xData = new double[_numSamples]();
+    }
+    else
+    {
+        _xData = nullptr;
+    }
+}
+
+SamplePack::SamplePack(const SamplePack& other) :
+    SamplePack(other.numSamples(), other.numChannels(), other.hasX())
+{
+    size_t dataSize = sizeof(double) * numSamples();
+    if (hasX())
+        memcpy(xData(), other.xData(), dataSize);
+    memcpy(_yData, other._yData, dataSize * numChannels());
+}
+
+SamplePack::~SamplePack()
+{
+    delete[] _yData;
+    if (_xData != nullptr)
+    {
+        delete[] _xData;
+    }
+}
+
+bool SamplePack::hasX() const
+{
+    return _xData != nullptr;
+}
+
+unsigned SamplePack::numChannels() const
+{
+    return _numChannels;
+}
+
+unsigned SamplePack::numSamples() const
+{
+    return _numSamples;
+}
+
+double* SamplePack::xData() const
+{
+    Q_ASSERT(_xData != nullptr);
+
+    return _xData;
+}
+
+double* SamplePack::data(unsigned channel) const
+{
+    Q_ASSERT(channel < _numChannels);
+
+    return &_yData[channel * _numSamples];
+}
+
+double* SamplePack::xData()
+{
+    return const_cast(static_cast(*this).xData());
+}
+
+double* SamplePack::data(unsigned channel)
+{
+    return const_cast(static_cast(*this).data(channel));
+}
diff --git a/src/samplepack.h b/src/samplepack.h
new file mode 100644
--- /dev/null
+++ b/src/samplepack.h
@@ -0,0 +1,50 @@
+/*
+  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 .
+*/
+
+#ifndef SAMPLEPACK_H
+#define SAMPLEPACK_H
+
+class SamplePack
+{
+public:
+    /**
+     * @param ns number of samples
+     * @param nc number of channels
+     * @param x has X channel
+     */
+    SamplePack(unsigned ns, unsigned nc, bool x = false);
+    SamplePack(const SamplePack& other);
+    ~SamplePack();
+
+    bool hasX() const;
+    unsigned numChannels() const;
+    unsigned numSamples() const;
+    double* xData() const;
+    double* data(unsigned channel) const;
+
+    double* xData();
+    double* data(unsigned channel);
+
+private:
+    unsigned _numSamples, _numChannels;
+    double* _xData;
+    double* _yData;
+};
+
+#endif // SAMPLEPACK_H
diff --git a/src/scrollzoomer.cpp b/src/scrollzoomer.cpp
--- a/src/scrollzoomer.cpp
+++ b/src/scrollzoomer.cpp
@@ -42,6 +42,10 @@ ScrollZoomer::ScrollZoomer( QWidget *can
     d_vScrollData( NULL ),
     d_inZoom( false )
 {
+    xMin = 0.;
+    xMax = 10000.;
+    hViewSize = 10000;
+
     for ( int axis = 0; axis < QwtPlot::axisCnt; axis++ )
         d_alignCanvasToScales[ axis ] = false;
 
@@ -50,6 +54,8 @@ ScrollZoomer::ScrollZoomer( QWidget *can
 
     d_hScrollData = new ScrollData;
     d_vScrollData = new ScrollData;
+    hscrollmove = false;
+    vscrollmove = false;
 }
 
 ScrollZoomer::~ScrollZoomer()
@@ -59,6 +65,39 @@ ScrollZoomer::~ScrollZoomer()
     delete d_hScrollData;
 }
 
+void ScrollZoomer::setXLimits(double min, double max)
+{
+    xMin = min;
+    xMax = max;
+    setZoomBase();
+}
+
+void ScrollZoomer::setHViewSize(double size)
+{
+    hscrollmove = true;
+    hViewSize = size;
+    setZoomBase();
+    hscrollmove = false;
+}
+
+void ScrollZoomer::setZoomBase(bool doReplot)
+{
+    QwtPlotZoomer::setZoomBase(doReplot);
+    auto zb = zoomBase();
+    auto zs = zoomStack();
+    zb.setRight(xMax);
+    if ((xMax - xMin) < hViewSize)
+    {
+        zb.setLeft(xMin);
+    }
+    else
+    {
+        zb.setLeft(xMax-hViewSize);
+    }
+    zs[0] = zb;
+    setZoomStack(zs);
+}
+
 void ScrollZoomer::rescale()
 {
     QwtScaleWidget *xScale = plot()->axisWidget( xAxis() );
@@ -112,7 +151,45 @@ void ScrollZoomer::rescale()
         }
     }
 
-    QwtPlotZoomer::rescale();
+    // NOTE: Below snippet is copied from QwtPlotZoomer::rescale() just so that
+    // we can refrain from updating y axis when moving horizontal scrollbar, so
+    // that auto-scale isn't disrupted. Also we don't want to jump around in
+    // x-axis when moving vertical scroll.
+    {
+        QwtPlot *plt = plot();
+        if ( !plt )
+            return;
+
+        const QRectF &rect = zoomStack()[zoomRectIndex()];
+        if ( rect != scaleRect() )
+        {
+            const bool doReplot = plt->autoReplot();
+            plt->setAutoReplot( false );
+
+            if (!vscrollmove)
+            {
+                double x1 = rect.left();
+                double x2 = rect.right();
+                if ( !plt->axisScaleDiv( xAxis() ).isIncreasing() )
+                    qSwap( x1, x2 );
+
+                plt->setAxisScale( xAxis(), x1, x2 );
+            }
+
+            if (!hscrollmove)
+            {
+                double y1 = rect.top();
+                double y2 = rect.bottom();
+                if ( !plt->axisScaleDiv( yAxis() ).isIncreasing() )
+                    qSwap( y1, y2 );
+
+                plt->setAxisScale( yAxis(), y1, y2 );
+
+                plt->setAutoReplot( doReplot );
+            }
+            plt->replot();
+        }
+    }
     updateScrollBars();
 }
 
@@ -237,6 +314,11 @@ bool ScrollZoomer::eventFilter( QObject 
                 layoutScrollBars( rect );
                 break;
             }
+            case QEvent::Show:
+            {
+                layoutScrollBars( canvas()->contentsRect() );
+                break;
+            }
             case QEvent::ChildRemoved:
             {
                 const QObject *child =
@@ -265,8 +347,8 @@ bool ScrollZoomer::needScrollBar( Qt::Or
     if ( orientation == Qt::Horizontal )
     {
         mode = d_hScrollData->mode;
-        baseMin = zoomBase().left();
-        baseMax = zoomBase().right();
+        baseMin = xMin;
+        baseMax = xMax;
         zoomMin = zoomRect().left();
         zoomMax = zoomRect().right();
     }
@@ -323,7 +405,7 @@ void ScrollZoomer::updateScrollBars()
         ScrollBar *sb = scrollBar( Qt::Horizontal );
         sb->setPalette( plot()->palette() );
         sb->setInverted( !plot()->axisScaleDiv( xAxis ).isIncreasing() );
-        sb->setBase( zoomBase().left(), zoomBase().right() );
+        sb->setBase( xMin, xMax );
         sb->moveSlider( zoomRect().left(), zoomRect().right() );
 
         if ( !sb->isVisibleTo( canvas() ) )
@@ -462,9 +544,17 @@ void ScrollZoomer::scrollBarMoved(
     Q_UNUSED( max );
 
     if ( o == Qt::Horizontal )
+    {
+        hscrollmove = true;
         moveTo( QPointF( min, zoomRect().top() ) );
+        hscrollmove = false;
+    }
     else
+    {
+        vscrollmove = true;
         moveTo( QPointF( zoomRect().left(), min ) );
+        vscrollmove = false;
+    }
 
     Q_EMIT zoomed( zoomRect() );
 }
@@ -487,3 +577,30 @@ int ScrollZoomer::oppositeAxis( int axis
 
     return axis;
 }
+
+void ScrollZoomer::moveTo( const QPointF &pos )
+{
+    // QwtPlotZoomer::moveTo(pos);
+    // return;
+
+    double x = pos.x();
+    double y = pos.y();
+
+    if ( x < xMin )
+        x = xMin;
+    if ( x > xMax - zoomRect().width() )
+        x = xMax - zoomRect().width();
+
+    if ( y < zoomBase().top() )
+        y = zoomBase().top();
+    if ( y > zoomBase().bottom() - zoomRect().height() )
+        y = zoomBase().bottom() - zoomRect().height();
+
+    if ( x != zoomRect().left() || y != zoomRect().top() )
+    {
+        auto zs = zoomStack();
+        zs[zoomRectIndex()].moveTo( x, y );
+        setZoomStack(zs, zoomRectIndex());
+        rescale();
+    }
+}
diff --git a/src/scrollzoomer.h b/src/scrollzoomer.h
--- a/src/scrollzoomer.h
+++ b/src/scrollzoomer.h
@@ -50,8 +50,14 @@ public:
 
     virtual bool eventFilter( QObject *, QEvent * );
 
+    void setXLimits(double min, double max);
+    void setHViewSize(double size);
+    virtual void setZoomBase(bool doReplot = true);
     virtual void rescale();
 
+public Q_SLOTS:
+    virtual void moveTo( const QPointF & );
+
 protected:
     virtual ScrollBar *scrollBar( Qt::Orientation );
     virtual void updateScrollBars();
@@ -61,6 +67,10 @@ private Q_SLOTS:
     void scrollBarMoved( Qt::Orientation o, double min, double max );
 
 private:
+    QRectF d_limits;
+    double xMin, xMax;
+    double hViewSize;
+
     bool needScrollBar( Qt::Orientation ) const;
     int oppositeAxis( int ) const;
 
@@ -71,6 +81,8 @@ private:
 
     bool d_inZoom;
     bool d_alignCanvasToScales[ QwtPlot::axisCnt ];
+    bool hscrollmove;
+    bool vscrollmove;
 };
 
 #endif
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 © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 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_UpdateCheck[] = "UpdateCheck";
 
 // mainwindow setting keys
 const char SG_MainWindow_Size[] = "size";
@@ -57,6 +58,8 @@ const char SG_Binary_Endianness[] = "end
 
 // ascii reader keys
 const char SG_ASCII_NumOfChannels[] = "numOfChannels";
+const char SG_ASCII_Delimiter[] = "delimiter";
+const char SG_ASCII_CustomDelimiter[] = "customDelimiter";
 
 // framed reader keys
 const char SG_CustomFrame_NumOfChannels[] = "numOfChannels";
@@ -68,14 +71,19 @@ const char SG_CustomFrame_Endianness[] =
 const char SG_CustomFrame_Checksum[] = "checksum";
 const char SG_CustomFrame_DebugMode[] = "debugMode";
 
-// channel manager keys
+// channel info keys
 const char SG_Channels_Channel[] = "channel";
 const char SG_Channels_Name[] = "name";
 const char SG_Channels_Color[] = "color";
 const char SG_Channels_Visible[] = "visible";
+const char SG_Channels_Gain[] = "gain";
+const char SG_Channels_GainEn[] = "gainEnabled";
+const char SG_Channels_Offset[] = "offset";
+const char SG_Channels_OffsetEn[] = "offsetEnabled";
 
 // plot settings keys
 const char SG_Plot_NumOfSamples[] = "numOfSamples";
+const char SG_Plot_PlotWidth[] = "plotWidth";
 const char SG_Plot_IndexAsX[] = "indexAsX";
 const char SG_Plot_XMax[] = "xMax";
 const char SG_Plot_XMin[] = "xMin";
@@ -102,5 +110,10 @@ const char SG_Record_StopOnClose[]      
 const char SG_Record_Header[]           = "header";
 const char SG_Record_Separator[]        = "separator";
 const char SG_Record_DisableBuffering[] = "disableBuffering";
+const char SG_Record_Timestamp[]        = "timestamp";
+
+// update check settings keys
+const char SG_UpdateCheck_Periodic[]  = "periodicCheck";
+const char SG_UpdateCheck_LastCheck[] = "lastCheck";
 
 #endif // SETTING_DEFINES_H
diff --git a/src/sink.cpp b/src/sink.cpp
new file mode 100644
--- /dev/null
+++ b/src/sink.cpp
@@ -0,0 +1,70 @@
+/*
+  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 
+#include "sink.h"
+
+void Sink::connectFollower(Sink* sink)
+{
+    Q_ASSERT(!followers.contains(sink));
+
+    followers.append(sink);
+    sink->setNumChannels(_numChannels, _hasX);
+}
+
+void Sink::disconnectFollower(Sink* sink)
+{
+    Q_ASSERT(followers.contains(sink));
+
+    followers.removeOne(sink);
+}
+
+void Sink::feedIn(const SamplePack& data)
+{
+    for (auto sink : followers)
+    {
+        sink->feedIn(data);
+    }
+}
+
+void Sink::setNumChannels(unsigned nc, bool x)
+{
+    _numChannels = nc;
+    _hasX = x;
+    for (auto sink : followers)
+    {
+        sink->setNumChannels(nc, x);
+    }
+}
+
+void Sink::setSource(Source* s)
+{
+    Q_ASSERT((source == nullptr) != (s == nullptr));
+    source = s;
+}
+
+const Source* Sink::connectedSource() const
+{
+    return source;
+}
+
+Source* Sink::connectedSource()
+{
+    return const_cast(static_cast(*this).connectedSource());
+}
diff --git a/src/sink.h b/src/sink.h
new file mode 100644
--- /dev/null
+++ b/src/sink.h
@@ -0,0 +1,73 @@
+/*
+  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 .
+*/
+
+#ifndef SINK_H
+#define SINK_H
+
+#include 
+#include "samplepack.h"
+
+class Source;
+
+class Sink
+{
+public:
+    /// Placeholder virtual destructor
+    virtual ~Sink() {};
+
+    /// Connects a sink to get any data that this sink
+    /// gets. Connecting an already connected sink is an error.
+    void connectFollower(Sink* sink);
+
+    /// Disconnects a follower. Disconnecting an unconnected sink is
+    /// an error.
+    void disconnectFollower(Sink* sink);
+
+    /// Returns the connected source. `nullptr` if it's not connected.
+    const Source* connectedSource() const;
+    Source* connectedSource();
+
+protected:
+    /// Entry point for incoming data. Re-implementations should
+    /// call this function to feed followers.
+    virtual void feedIn(const SamplePack& data);
+
+    /// Is set by connected source. Re-implementations should call
+    /// this function to update followers.
+    virtual void setNumChannels(unsigned nc, bool x);
+
+    /// Set by the connected source when its connected. When
+    /// disconnecting it's set to `nullptr`.
+    ///
+    /// @note Previous source is disconnected.
+    ///
+    /// @important Trying to connect a source while its already
+    /// connected is an error.
+    void setSource(Source* s);
+
+    friend Source;
+
+private:
+    QList followers;
+    Source* source = nullptr;   ///< source that this sink is connected to
+    bool _hasX;
+    unsigned _numChannels;
+};
+
+#endif // SINK_H
diff --git a/src/snapshot.cpp b/src/snapshot.cpp
--- a/src/snapshot.cpp
+++ b/src/snapshot.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -25,14 +25,14 @@
 #include "snapshot.h"
 #include "snapshotview.h"
 
-Snapshot::Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel) :
+Snapshot::Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel, bool saved) :
     QObject(parent),
     cInfoModel(infoModel),
     _showAction(this),
     _deleteAction("&Delete", this)
 {
     _name = name;
-    _saved = false;
+    _saved = saved;
 
     view = NULL;
     mainWindow = parent;
@@ -108,9 +108,24 @@ void Snapshot::setName(QString name)
     emit nameChanged(this);
 }
 
+unsigned Snapshot::numChannels() const
+{
+    return yData.size();
+}
+
+unsigned Snapshot::numSamples() const
+{
+    return yData[0]->size();
+}
+
+const ChannelInfoModel* Snapshot::infoModel() const
+{
+    return &cInfoModel;
+}
+
 ChannelInfoModel* Snapshot::infoModel()
 {
-    return &cInfoModel;
+    return const_cast(static_cast(*this).infoModel());
 }
 
 QString Snapshot::channelName(unsigned channel)
@@ -120,31 +135,27 @@ QString Snapshot::channelName(unsigned c
 
 void Snapshot::save(QString fileName)
 {
-    // TODO: remove code duplication (MainWindow::onExportCsv)
     QSaveFile file(fileName);
 
     if (file.open(QIODevice::WriteOnly | QIODevice::Text))
     {
         QTextStream fileStream(&file);
 
-        unsigned numOfChannels = data.size();
-        unsigned numOfSamples = data[0].size();
-
         // print header
-        for (unsigned int ci = 0; ci < numOfChannels; ci++)
+        for (unsigned int ci = 0; ci < numChannels(); ci++)
         {
             fileStream << channelName(ci);
-            if (ci != numOfChannels-1) fileStream << ",";
+            if (ci != numChannels()-1) fileStream << ",";
         }
         fileStream << '\n';
 
         // print rows
-        for (unsigned int i = 0; i < numOfSamples; i++)
+        for (unsigned int i = 0; i < numSamples(); i++)
         {
-            for (unsigned int ci = 0; ci < numOfChannels; ci++)
+            for (unsigned int ci = 0; ci < numChannels(); ci++)
             {
-                fileStream << data[ci][i].y();
-                if (ci != numOfChannels-1) fileStream << ",";
+                fileStream << yData[ci]->sample(i);
+                if (ci != numChannels()-1) fileStream << ",";
             }
             fileStream << '\n';
         }
diff --git a/src/snapshot.h b/src/snapshot.h
--- a/src/snapshot.h
+++ b/src/snapshot.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -27,6 +27,7 @@
 #include 
 
 #include "channelinfomodel.h"
+#include "readonlybuffer.h"
 
 class SnapshotView;
 class MainWindow;
@@ -36,15 +37,19 @@ class Snapshot : public QObject
     Q_OBJECT
 
 public:
-    Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel);
+    Snapshot(MainWindow* parent, QString name, ChannelInfoModel infoModel, bool saved = false);
     ~Snapshot();
 
-    QVector> data;
+    // TODO: yData of snapshot shouldn't be public, preferable should be handled in constructor
+    QVector yData;
     QAction* showAction();
     QAction* deleteAction();
 
     QString name();
     QString displayName(); ///< `name()` plus '*' if snapshot is not saved
+    unsigned numChannels() const; ///< number of channels in this snapshot
+    unsigned numSamples() const;  ///< number of samples in every channel
+    const ChannelInfoModel* infoModel() const;
     ChannelInfoModel* infoModel();
     void setName(QString name);
     QString channelName(unsigned channel);
diff --git a/src/snapshotmanager.cpp b/src/snapshotmanager.cpp
--- a/src/snapshotmanager.cpp
+++ b/src/snapshotmanager.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -22,6 +22,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 #include 
 #include 
@@ -31,14 +32,14 @@
 #include "snapshotmanager.h"
 
 SnapshotManager::SnapshotManager(MainWindow* mainWindow,
-                                 ChannelManager* channelMan) :
+                                 Stream* stream) :
     _menu("&Snapshots"),
     _takeSnapshotAction("&Take Snapshot", this),
     loadSnapshotAction("&Load Snapshots", this),
     clearAction("&Clear Snapshots", this)
 {
     _mainWindow = mainWindow;
-    _channelMan = channelMan;
+    _stream = stream;
 
     _takeSnapshotAction.setToolTip("Take a snapshot of current plot");
     _takeSnapshotAction.setShortcut(QKeySequence("F5"));
@@ -63,21 +64,14 @@ SnapshotManager::~SnapshotManager()
     }
 }
 
-Snapshot* SnapshotManager::makeSnapshot()
+Snapshot* SnapshotManager::makeSnapshot() const
 {
     QString name = QTime::currentTime().toString("'Snapshot ['HH:mm:ss']'");
-    auto snapshot = new Snapshot(_mainWindow, name, *(_channelMan->infoModel()));
-
-    unsigned numOfChannels = _channelMan->numOfChannels();
-    unsigned numOfSamples = _channelMan->numOfSamples();
+    auto snapshot = new Snapshot(_mainWindow, name, *(_stream->infoModel()));
 
-    for (unsigned ci = 0; ci < numOfChannels; ci++)
+    for (unsigned ci = 0; ci < _stream->numChannels(); ci++)
     {
-        snapshot->data.append(QVector(numOfSamples));
-        for (unsigned i = 0; i < numOfSamples; i++)
-        {
-            snapshot->data[ci][i] = QPointF(i, _channelMan->channelBuffer(ci)->sample(i));
-        }
+        snapshot->yData.append(new ReadOnlyBuffer(_stream->channel(ci)->yData()));
     }
 
     return snapshot;
@@ -158,18 +152,20 @@ void SnapshotManager::loadSnapshotFromFi
     unsigned numOfChannels = channelNames.size();
 
     // read data
-    QVector> data(numOfChannels);
+    QVector> data(numOfChannels);
+    QTextStream ts(&file);
+    QString line;
     unsigned lineNum = 1;
-    while (file.canReadLine())
+    while (ts.readLineInto(&line))
     {
         // parse line
-        auto line = QString(file.readLine());
         auto split = line.split(',');
 
         if (split.size() != (int) numOfChannels)
         {
             qCritical() << "Parsing error at line " << lineNum
                         << ": number of columns is not consistent.";
+            qCritical() << "Line " << lineNum << ": " << line;
             return;
         }
 
@@ -186,16 +182,20 @@ void SnapshotManager::loadSnapshotFromFi
                             << "\" to double.";
                 return;
             }
-            data[ci].append(QPointF(lineNum-1, y));
+            data[ci].append(y);
         }
         lineNum++;
     }
 
-    ChannelInfoModel channelInfo(channelNames);
-
+    // create snapshot
     auto snapshot = new Snapshot(
-        _mainWindow, QFileInfo(fileName).baseName(), ChannelInfoModel(channelNames));
-    snapshot->data = data;
+        _mainWindow, QFileInfo(fileName).baseName(),
+        ChannelInfoModel(channelNames), true);
+
+    for (unsigned ci = 0; ci < numOfChannels; ci++)
+    {
+        snapshot->yData.append(new ReadOnlyBuffer(data[ci].data(), data[ci].size()));
+    }
 
     addSnapshot(snapshot, false);
 }
diff --git a/src/snapshotmanager.h b/src/snapshotmanager.h
--- a/src/snapshotmanager.h
+++ b/src/snapshotmanager.h
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -24,8 +24,7 @@
 #include 
 #include 
 
-#include "framebuffer.h"
-#include "channelmanager.h"
+#include "stream.h"
 #include "snapshot.h"
 
 class MainWindow;
@@ -35,7 +34,7 @@ class SnapshotManager : public QObject
     Q_OBJECT
 
 public:
-    SnapshotManager(MainWindow* mainWindow, ChannelManager* channelMan);
+    SnapshotManager(MainWindow* mainWindow, Stream* stream);
     ~SnapshotManager();
 
     QMenu* menu();
@@ -43,13 +42,13 @@ public:
 
     /// Creates a dynamically allocated snapshot object but doesn't record it in snapshots list.
     /// @note Caller is responsible for deletion of the returned `Snapshot` object.
-    Snapshot* makeSnapshot();
+    Snapshot* makeSnapshot() const;
 
     bool isAllSaved(); ///< returns `true` if all snapshots are saved to a file
 
 private:
     MainWindow* _mainWindow;
-    ChannelManager* _channelMan;
+    Stream* _stream;
 
     QList snapshots;
 
diff --git a/src/snapshotview.cpp b/src/snapshotview.cpp
--- a/src/snapshotview.cpp
+++ b/src/snapshotview.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -23,26 +23,27 @@
 SnapshotView::SnapshotView(MainWindow* parent, Snapshot* snapshot) :
     QMainWindow(parent),
     ui(new Ui::SnapshotView),
-    renameDialog(this)
+    renameDialog(this),
+    plotMenu(parent->viewSettings())
 {
     _snapshot = snapshot;
 
     ui->setupUi(this);
 
-    plotMan = new PlotManager(ui->plotArea, snapshot->infoModel(), this);
-    plotMan->setViewSettings(parent->viewSettings());
+    plotMan = new PlotManager(ui->plotArea, &plotMenu, snapshot, this);
 
     ui->menuSnapshot->insertAction(ui->actionClose, snapshot->deleteAction());
     this->setWindowTitle(snapshot->displayName());
 
     // initialize curves
-    unsigned numOfChannels = snapshot->data.size();
-    unsigned numOfSamples = snapshot->data[0].size();
-    for (unsigned ci = 0; ci < numOfChannels; ci++)
-    {
-        plotMan->addCurve(snapshot->channelName(ci), snapshot->data[ci]);
-    }
-    plotMan->setNumOfSamples(numOfSamples);
+    // unsigned numOfChannels = snapshot->data.size();
+    // unsigned numOfSamples = snapshot->data[0].size();
+    // for (unsigned ci = 0; ci < numOfChannels; ci++)
+    // {
+    //     plotMan->addCurve(snapshot->channelName(ci), snapshot->data[ci]);
+    // }
+    // plotMan->setNumOfSamples(numOfSamples);
+    // plotMan->setPlotWidth(numOfSamples);
 
     renameDialog.setWindowTitle("Rename Snapshot");
     renameDialog.setLabelText("Enter new name:");
@@ -52,11 +53,8 @@ SnapshotView::SnapshotView(MainWindow* p
     connect(ui->actionSave, &QAction::triggered,
             this, &SnapshotView::save);
 
-    // add 'View' menu items
-    for (auto a : plotMan->menuActions())
-    {
-        ui->menuView->addAction(a);
-    }
+    // add "View" menu
+    menuBar()->insertMenu(NULL, &plotMenu);
 }
 
 SnapshotView::~SnapshotView()
diff --git a/src/snapshotview.h b/src/snapshotview.h
--- a/src/snapshotview.h
+++ b/src/snapshotview.h
@@ -31,6 +31,7 @@
 
 #include "mainwindow.h"
 #include "plotmanager.h"
+#include "plotmenu.h"
 #include "snapshot.h"
 
 namespace Ui {
@@ -54,6 +55,7 @@ private:
     Snapshot* _snapshot;
     QInputDialog renameDialog;
     PlotManager* plotMan;
+    PlotMenu plotMenu;
 
     void closeEvent(QCloseEvent *event);
 
diff --git a/src/snapshotview.ui b/src/snapshotview.ui
--- a/src/snapshotview.ui
+++ b/src/snapshotview.ui
@@ -26,7 +26,7 @@
      0
      0
      544
-     25
+     24
     
    
 
    
-   
    
-   
   
   
    
diff --git a/src/source.cpp b/src/source.cpp
new file mode 100644
--- /dev/null
+++ b/src/source.cpp
@@ -0,0 +1,79 @@
+/*
+  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 
+
+#include "source.h"
+
+Source::~Source()
+{
+    for (auto sink : sinks)
+    {
+        sink->setSource(nullptr);
+    }
+}
+
+void Source::connectSink(Sink* sink)
+{
+    Q_ASSERT(!sinks.contains(sink));
+
+    auto prevSource = sink->connectedSource();
+    if (prevSource != nullptr)
+    {
+        prevSource->disconnect(sink);
+    }
+
+    sinks.append(sink);
+    sink->setSource(this);
+    sink->setNumChannels(numChannels(), hasX());
+}
+
+void Source::disconnect(Sink* sink)
+{
+    Q_ASSERT(sinks.contains(sink));
+    Q_ASSERT(sink->connectedSource() == this);
+
+    sink->setSource(nullptr);
+    sinks.removeOne(sink);
+}
+
+void Source::disconnectSinks()
+{
+    while (!sinks.isEmpty())
+    {
+        auto sink = sinks.takeFirst();
+        sink->setSource(nullptr);
+    }
+}
+
+void Source::feedOut(const SamplePack& data) const
+{
+    for (auto sink : sinks)
+    {
+        sink->feedIn(data);
+    }
+}
+
+void Source::updateNumChannels() const
+{
+    for (auto sink : sinks)
+    {
+        sink->setNumChannels(numChannels(), hasX());
+    }
+}
diff --git a/src/source.h b/src/source.h
new file mode 100644
--- /dev/null
+++ b/src/source.h
@@ -0,0 +1,64 @@
+/*
+  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 .
+*/
+
+#ifndef SOURCE_H
+#define SOURCE_H
+
+#include 
+
+#include "sink.h"
+#include "samplepack.h"
+
+class Source
+{
+public:
+    /// Virtual destructor. Must be called by implementors to notify sinks.
+    virtual ~Source();
+
+    /// Returns true if source has X data
+    virtual bool hasX() const = 0;
+
+    /// Returns number of channels
+    virtual unsigned numChannels() const = 0;
+
+    /// Connects a sink to this source.
+    ///
+    /// If `Sink` is already connected to a source, it's disconnected first.
+    void connectSink(Sink* sink);
+
+    /// Disconnects an already connected sink. Trying to disconnect an
+    /// unconnected sink is an error.
+    void disconnect(Sink* sink);
+
+    /// Disconnects all connected sinks.
+    void disconnectSinks();
+
+protected:
+    /// Feeds "in" given data to connected sinks
+    virtual void feedOut(const SamplePack& data) const;
+
+    /// Updates "number of channels" of connected sinks. Must be
+    /// called when num. channels or hasX changes.
+    void updateNumChannels() const;
+
+private:
+    QList sinks;
+};
+
+#endif // SOURCE_H
diff --git a/src/stream.cpp b/src/stream.cpp
new file mode 100644
--- /dev/null
+++ b/src/stream.cpp
@@ -0,0 +1,248 @@
+/*
+  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 "stream.h"
+#include "ringbuffer.h"
+#include "indexbuffer.h"
+
+Stream::Stream(unsigned nc, bool x, unsigned ns) :
+    _infoModel(nc)
+{
+    _numSamples = ns;
+    _paused = false;
+
+    // create xdata buffer
+    _hasx = x;
+    if (x)
+    {
+        xData = new RingBuffer(ns);
+    }
+    else
+    {
+        xData = new IndexBuffer(ns);
+    }
+
+    // create channels
+    for (unsigned i = 0; i < nc; i++)
+    {
+        auto c = new StreamChannel(i, xData, new RingBuffer(ns), &_infoModel);
+        channels.append(c);
+    }
+}
+
+Stream::~Stream()
+{
+    for (auto ch : channels)
+    {
+        delete ch;
+    }
+    delete xData;
+}
+
+bool Stream::hasX() const
+{
+    return _hasx;
+}
+
+unsigned Stream::numChannels() const
+{
+    return channels.length();
+}
+
+unsigned Stream::numSamples() const
+{
+    return _numSamples;
+}
+
+const StreamChannel* Stream::channel(unsigned index) const
+{
+    Q_ASSERT(index < numChannels());
+    return channels[index];
+}
+
+StreamChannel* Stream::channel(unsigned index)
+{
+    return const_cast(static_cast(*this).channel(index));
+}
+
+const ChannelInfoModel* Stream::infoModel() const
+{
+    return &_infoModel;
+}
+
+ChannelInfoModel* Stream::infoModel()
+{
+    return const_cast(static_cast(*this).infoModel());
+}
+
+void Stream::setNumChannels(unsigned nc, bool x)
+{
+    unsigned oldNum = numChannels();
+    if (oldNum == nc && x == _hasx) return;
+
+    // adjust the number of channels
+    if (nc > oldNum)
+    {
+        for (unsigned i = oldNum; i < nc; i++)
+        {
+            auto c = new StreamChannel(i, xData, new RingBuffer(_numSamples), &_infoModel);
+            channels.append(c);
+        }
+    }
+    else if (nc < oldNum)
+    {
+        for (unsigned i = oldNum-1; i > nc-1; i--)
+        {
+            delete channels.takeLast();
+        }
+    }
+
+    // change the xdata
+    if (x != _hasx)
+    {
+        if (x)
+        {
+            xData = new RingBuffer(_numSamples);
+        }
+        else
+        {
+            xData = new IndexBuffer(_numSamples);
+        }
+
+        for (auto c : channels)
+        {
+            c->setX(xData);
+        }
+        _hasx = x;
+    }
+
+    if (nc != oldNum)
+    {
+        _infoModel.setNumOfChannels(nc);
+        // TODO: how about X change?
+        emit numChannelsChanged(nc);
+    }
+
+    Sink::setNumChannels(nc, x);
+}
+
+const SamplePack* Stream::applyGainOffset(const SamplePack& pack) const
+{
+    Q_ASSERT(infoModel()->gainOrOffsetEn());
+
+    SamplePack* mPack = new SamplePack(pack);
+    unsigned ns = pack.numSamples();
+
+    for (unsigned ci = 0; ci < numChannels(); ci++)
+    {
+        // TODO: we could use some kind of map (int32, int64 would suffice) to speed things up
+        bool gainEn = infoModel()->gainEn(ci);
+        bool offsetEn = infoModel()->offsetEn(ci);
+        if (gainEn || offsetEn)
+        {
+            double* mdata = mPack->data(ci);
+
+            double gain = infoModel()->gain(ci);
+            double offset = infoModel()->offset(ci);
+
+            if (gainEn)
+            {
+                for (unsigned i = 0; i < ns; i++)
+                {
+                    mdata[i] *= gain;
+                }
+            }
+            if (offsetEn)
+            {
+                for (unsigned i = 0; i < ns; i++)
+                {
+                    mdata[i] += offset;
+                }
+            }
+        }
+    }
+
+    return mPack;
+}
+
+void Stream::feedIn(const SamplePack& pack)
+{
+    Q_ASSERT(pack.numChannels() == numChannels() &&
+             pack.hasX() == hasX());
+
+    if (_paused) return;
+
+    unsigned ns = pack.numSamples();
+    if (_hasx)
+    {
+        static_cast(xData)->addSamples(pack.xData(), ns);
+    }
+
+    // modified pack that gain and offset is applied to
+    const SamplePack* mPack = nullptr;
+    if (infoModel()->gainOrOffsetEn())
+        mPack = applyGainOffset(pack);
+
+    for (unsigned ci = 0; ci < numChannels(); ci++)
+    {
+        auto buf = static_cast(channels[ci]->yData());
+        double* data = (mPack == nullptr) ? pack.data(ci) : mPack->data(ci);
+        buf->addSamples(data, ns);
+    }
+
+    Sink::feedIn((mPack == nullptr) ? pack : *mPack);
+
+    if (mPack != nullptr) delete mPack;
+    emit dataAdded();
+}
+
+void Stream::pause(bool paused)
+{
+    _paused = paused;
+}
+
+void Stream::clear()
+{
+    for (auto c : channels)
+    {
+        static_cast(c->yData())->clear();
+    }
+}
+
+void Stream::setNumSamples(unsigned value)
+{
+    if (value == _numSamples) return;
+    _numSamples = value;
+
+    xData->resize(value);
+    for (auto c : channels)
+    {
+        static_cast(c->yData())->resize(value);
+    }
+}
+
+void Stream::saveSettings(QSettings* settings) const
+{
+    _infoModel.saveSettings(settings);
+}
+
+void Stream::loadSettings(QSettings* settings)
+{
+    _infoModel.loadSettings(settings);
+}
diff --git a/src/stream.h b/src/stream.h
new file mode 100644
--- /dev/null
+++ b/src/stream.h
@@ -0,0 +1,117 @@
+/*
+  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 .
+*/
+
+#ifndef STREAM_H
+#define STREAM_H
+
+#include 
+#include 
+#include 
+#include 
+
+#include "sink.h"
+#include "source.h"
+#include "channelinfomodel.h"
+#include "streamchannel.h"
+#include "framebuffer.h"
+
+/**
+ * Main waveform storage class. It consists of channels. Channels are
+ * synchronized with each other.
+ *
+ * Implements `Sink` class for data entry. It's expected to be
+ * connected to a `Device` source.
+ */
+class Stream : public QObject, public Sink
+{
+    Q_OBJECT
+
+public:
+    /**
+     * @param nc number of channels
+     * @param x has X data input
+     * @param ns number of samples
+     */
+    Stream(unsigned nc = 0, bool x = false, unsigned ns = 0);
+    ~Stream();
+
+    // implementations for `Source`
+    virtual bool hasX() const;
+    virtual unsigned numChannels() const;
+
+    unsigned numSamples() const;
+    const StreamChannel* channel(unsigned index) const;
+    StreamChannel* channel(unsigned index);
+    const ChannelInfoModel* infoModel() const;
+    ChannelInfoModel* infoModel();
+
+    /// Saves channel information
+    void saveSettings(QSettings* settings) const;
+    /// Load channel information
+    void loadSettings(QSettings* settings);
+
+protected:
+    // implementations for `Sink`
+    virtual void setNumChannels(unsigned nc, bool x);
+    virtual void feedIn(const SamplePack& pack);
+
+signals:
+    void numChannelsChanged(unsigned value);
+    void numSamplesChanged(unsigned value);
+    void channelAdded(const StreamChannel* chan);
+    void channelNameChanged(unsigned channel, QString name); // TODO: does it stay?
+    void dataAdded(); ///< emitted when data added to channel man.
+
+public slots:
+    // TODO: these won't be public
+    // void setNumChannels(unsigned number);
+    void setNumSamples(unsigned value);
+
+    /// When paused data feed is ignored
+    void pause(bool paused);
+
+    /// Clears buffer data (fills with 0)
+    void clear();
+
+private:
+    unsigned _numSamples;
+    bool _paused;
+
+    bool _hasx;
+    ResizableBuffer* xData;
+    QList channels;
+
+    ChannelInfoModel _infoModel;
+
+    /**
+     * Applies gain and offset to given pack.
+     *
+     * Caller is responsible for deleting returned `SamplePack`.
+     *
+     * @note Should be called only when gain or offset is enabled. Guard with
+     * `ChannelInfoModel::gainOrOffsetEn()`.
+     *
+     * @param pack input data
+     * @return modified data
+     */
+    const SamplePack* applyGainOffset(const SamplePack& pack) const;
+};
+
+
+#endif // STREAM_H
diff --git a/src/streamchannel.cpp b/src/streamchannel.cpp
new file mode 100644
--- /dev/null
+++ b/src/streamchannel.cpp
@@ -0,0 +1,44 @@
+/*
+  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 "streamchannel.h"
+
+StreamChannel::StreamChannel(unsigned i, const FrameBuffer* x,
+              FrameBuffer* y, ChannelInfoModel* info)
+{
+    _index = i;
+    _x = x;
+    _y = y;
+    _info = info;
+}
+
+StreamChannel::~StreamChannel()
+{
+    delete _y;
+}
+
+unsigned StreamChannel::index() const {return _index;}
+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 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;};
diff --git a/src/streamchannel.h b/src/streamchannel.h
new file mode 100644
--- /dev/null
+++ b/src/streamchannel.h
@@ -0,0 +1,60 @@
+/*
+  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 .
+*/
+
+#ifndef STREAMCHANNEL_H
+#define STREAMCHANNEL_H
+
+#include "framebuffer.h"
+#include "channelinfomodel.h"
+
+class StreamChannel
+{
+public:
+    /**
+     * Creates a stream channel.
+     *
+     * @param i index of the channel
+     * @param x x axis buffer
+     * @param y data buffer of this channel, takes ownership
+     * @param info channel info model
+     */
+    StreamChannel(unsigned i,
+                  const FrameBuffer* x,
+                  FrameBuffer* y,
+                  ChannelInfoModel* info);
+    ~StreamChannel();
+
+    unsigned index() const;
+    QString name() const;
+    QColor color() const;
+    bool visible() const;
+    const FrameBuffer* xData() const;
+    FrameBuffer* yData();
+    const FrameBuffer* yData() const;
+    const ChannelInfoModel* info() const;
+    void setX(const FrameBuffer* x);
+
+private:
+    unsigned _index;
+    const FrameBuffer* _x;
+    FrameBuffer* _y;
+    ChannelInfoModel* _info;
+};
+
+#endif // STREAMCHANNEL_H
diff --git a/src/updatecheckdialog.cpp b/src/updatecheckdialog.cpp
new file mode 100644
--- /dev/null
+++ b/src/updatecheckdialog.cpp
@@ -0,0 +1,105 @@
+/*
+  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 "setting_defines.h"
+#include "updatecheckdialog.h"
+#include "ui_updatecheckdialog.h"
+
+UpdateCheckDialog::UpdateCheckDialog(QWidget *parent) :
+    QDialog(parent),
+    ui(new Ui::UpdateCheckDialog)
+{
+    ui->setupUi(this);
+
+    // by default start from yesterday, so that we check at first run
+    lastCheck = QDate::currentDate().addDays(-1);
+
+    connect(&updateChecker, &UpdateChecker::checkFailed,
+            [this](QString errorMessage)
+            {
+                lastCheck = QDate::currentDate();
+                ui->label->setText(QString("Update check failed.\n") + errorMessage);
+            });
+
+    connect(&updateChecker, &UpdateChecker::checkFinished,
+            [this](bool found, QString newVersion, QString downloadUrl)
+            {
+                QString text;
+                if (!found)
+                {
+                    text = "There is no update yet.";
+                }
+                else
+                {
+                    show();
+#ifdef UPDATE_TYPE_PKGMAN
+                    text = QString("There is a new version: %1. "
+                                   "Use your package manager to update"
+                                   " or click to download.")\
+                        .arg(newVersion).arg(downloadUrl);
+#else
+                    text = QString("Found update to version %1. Click to download.")\
+                        .arg(newVersion).arg(downloadUrl);
+#endif
+                }
+
+                lastCheck = QDate::currentDate();
+                ui->label->setText(text);
+            });
+}
+
+UpdateCheckDialog::~UpdateCheckDialog()
+{
+    delete ui;
+}
+
+void UpdateCheckDialog::showEvent(QShowEvent *event)
+{
+    updateChecker.checkUpdate();
+    ui->label->setText("Checking update...");
+}
+
+void UpdateCheckDialog::closeEvent(QShowEvent *event)
+{
+    if (updateChecker.isChecking()) updateChecker.cancelCheck();
+}
+
+void UpdateCheckDialog::saveSettings(QSettings* settings)
+{
+    settings->beginGroup(SettingGroup_UpdateCheck);
+    settings->setValue(SG_UpdateCheck_Periodic, ui->cbPeriodic->isChecked());
+    settings->setValue(SG_UpdateCheck_LastCheck, lastCheck.toString(Qt::ISODate));
+    settings->endGroup();
+}
+
+void UpdateCheckDialog::loadSettings(QSettings* settings)
+{
+    settings->beginGroup(SettingGroup_UpdateCheck);
+    ui->cbPeriodic->setChecked(settings->value(SG_UpdateCheck_Periodic,
+                                               ui->cbPeriodic->isChecked()).toBool());
+    auto lastCheckS = settings->value(SG_UpdateCheck_LastCheck, lastCheck.toString(Qt::ISODate)).toString();
+    lastCheck = QDate::fromString(lastCheckS, Qt::ISODate);
+    settings->endGroup();
+
+    // start the periodic update if required
+    if (ui->cbPeriodic->isChecked() && lastCheck < QDate::currentDate())
+    {
+        updateChecker.checkUpdate();
+    }
+}
diff --git a/src/updatecheckdialog.h b/src/updatecheckdialog.h
new file mode 100644
--- /dev/null
+++ b/src/updatecheckdialog.h
@@ -0,0 +1,54 @@
+/*
+  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 .
+*/
+
+#ifndef UPDATECHECKDIALOG_H
+#define UPDATECHECKDIALOG_H
+
+#include 
+#include 
+#include 
+#include "updatechecker.h"
+
+namespace Ui {
+class UpdateCheckDialog;
+}
+
+class UpdateCheckDialog : public QDialog
+{
+    Q_OBJECT
+
+public:
+    explicit UpdateCheckDialog(QWidget *parent = 0);
+    ~UpdateCheckDialog();
+
+    /// Stores update settings into a `QSettings`.
+    void saveSettings(QSettings* settings);
+    /// Loads update settings from a `QSettings`.
+    void loadSettings(QSettings* settings);
+
+private:
+    Ui::UpdateCheckDialog *ui;
+    UpdateChecker updateChecker;
+    QDate lastCheck;
+
+    void showEvent(QShowEvent *event);
+    void closeEvent(QShowEvent *event);
+};
+
+#endif // UPDATECHECKDIALOG_H
diff --git a/src/updatecheckdialog.ui b/src/updatecheckdialog.ui
new file mode 100644
--- /dev/null
+++ b/src/updatecheckdialog.ui
@@ -0,0 +1,71 @@
+
+
+ UpdateCheckDialog
+ 
+  
+   
+    0
+    0
+    400
+    148
+   
+  
+  
+   Check Update
+  
+  
+   - 
+    
+     
+      Checking update...
+     
+     
+      true
+     
+    
+   
 
+   - 
+    
+     
+      Updates will be checked only once a day at first start of the application
+     
+     
+      Check updates periodically
+     
+     
+      true
+     
+    
+   
 
+   - 
+    
+     
+      Qt::Horizontal
+     
+     
+      QDialogButtonBox::Close
+     
+    
+   
 
+  
+ 
+ 
+ 
+  
+   buttonBox
+   clicked(QAbstractButton*)
+   UpdateCheckDialog
+   close()
+   
+    
+     199
+     125
+    
+    
+     199
+     73
+    
+   
+  
+ 
+
diff --git a/src/updatechecker.cpp b/src/updatechecker.cpp
new file mode 100644
--- /dev/null
+++ b/src/updatechecker.cpp
@@ -0,0 +1,222 @@
+/*
+  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 
+#include 
+#include 
+#include 
+#include 
+#include 
+#include 
+
+#include "updatechecker.h"
+
+// This link returns the list of downloads in JSON format. Note that we only use
+// the first page because results are sorted new to old.
+const char BB_DOWNLOADS_URL[] = "https://api.bitbucket.org/2.0/repositories/hyozd/serialplot/downloads?fields=values.name,values.links.self.href";
+
+UpdateChecker::UpdateChecker(QObject *parent) :
+    QObject(parent), nam(this)
+{
+    activeReply = NULL;
+
+    connect(&nam, &QNetworkAccessManager::finished,
+            this, &UpdateChecker::onReqFinished);
+}
+
+bool UpdateChecker::isChecking() const
+{
+    return activeReply != NULL && !activeReply->isFinished();
+}
+
+void UpdateChecker::checkUpdate()
+{
+    if (isChecking()) return;
+
+    auto req = QNetworkRequest(QUrl(BB_DOWNLOADS_URL));
+    activeReply = nam.get(req);
+}
+
+void UpdateChecker::cancelCheck()
+{
+    if (activeReply != NULL) activeReply->abort();
+}
+
+void UpdateChecker::onReqFinished(QNetworkReply* reply)
+{
+    if (reply->error() != QNetworkReply::NoError)
+    {
+        emit checkFailed(QString("Network error: ") + reply->errorString());
+    }
+    else
+    {
+        QJsonParseError error;
+        auto data = QJsonDocument::fromJson(reply->readAll(), &error);
+        if (error.error != QJsonParseError::NoError)
+        {
+            emit checkFailed(QString("JSon parsing error: ") + error.errorString());
+        }
+        else
+        {
+            QList files;
+            if (!parseData(data, files))
+            {
+                // TODO: emit detailed data contents for logging
+                emit checkFailed("Data parsing error.");
+            }
+            else
+            {
+                FileInfo updateFile;
+                if (findUpdate(files, updateFile))
+                {
+                    emit checkFinished(
+                        true, updateFile.version.toString(), updateFile.link);
+                }
+                else
+                {
+                    emit checkFinished(false, "", "");
+                }
+            }
+        }
+    }
+    reply->deleteLater();
+    activeReply = NULL;
+}
+
+bool UpdateChecker::parseData(const QJsonDocument& data, QList& files) const
+{
+    /* Data is expected to be in this form:
+
+    {
+       "values": [
+       {
+         "name": "serialplot-0.9.1-x86_64.AppImage",
+         "links": {
+           "self": {
+             "href": "https://api.bitbucket.org/2.0/repositories/hyOzd/serialplot/downloads/serialplot-0.9.1-x86_64.AppImage"
+            }
+          }
+       }, ... ]
+    }
+    */
+
+    if (!data.isObject()) return false;
+
+    auto values = data.object()["values"];
+    if (values == QJsonValue::Undefined || !values.isArray()) return false;
+
+    for (auto value : values.toArray())
+    {
+        if (!value.isObject()) return false;
+
+        auto name = value.toObject().value("name");
+        if (name.isUndefined() || !name.isString())
+             return false;
+
+        auto links = value.toObject().value("links");
+        if (links.isUndefined() || !links.isObject())
+            return false;
+
+        auto self = links.toObject().value("self");
+        if (self.isUndefined() || !self.isObject())
+            return false;
+
+        auto href = self.toObject().value("href");
+        if (href.isUndefined() || !href.isString())
+            return false;
+
+        FileInfo finfo;
+        finfo.name = name.toString();
+        finfo.link = href.toString();
+        finfo.hasVersion = VersionNumber::extract(name.toString(), finfo.version);
+
+        if (finfo.name.contains("amd64") ||
+            finfo.name.contains("x86_64") ||
+            finfo.name.contains("win64"))
+        {
+            finfo.arch = FileArch::amd64;
+        }
+        else if (finfo.name.contains("win32") ||
+                 finfo.name.contains("i386"))
+        {
+            finfo.arch = FileArch::_i386;
+        }
+        else
+        {
+            finfo.arch = FileArch::unknown;
+        }
+
+        files += finfo;
+    }
+
+    return true;
+}
+
+bool UpdateChecker::findUpdate(const QList& files, FileInfo& foundFile) const
+{
+    QList fflist;
+
+    // filter the file list according to extension and version number
+    for (int i = 0; i < files.length(); i++)
+    {
+        // file type to look
+#if defined(Q_OS_WIN)
+        const char ext[] = ".exe";
+#else  // of course linux
+        const char ext[] = ".appimage";
+#endif
+
+        // file architecture to look
+#if defined(Q_PROCESSOR_X86_64)
+        const FileArch arch = FileArch::amd64;
+#elif defined(Q_PROCESSOR_X86_32)
+        const FileArch arch = FileArch::_i386;
+#elif defined(Q_PROCESSOR_ARM)
+        const FileArch arch = FileArch::arm;
+#else
+        #error Unknown architecture for update file detection.
+#endif
+
+        // filter the file list
+        auto file = files[i];
+        if (file.name.contains(ext, Qt::CaseInsensitive) &&
+            file.arch == arch &&
+            file.hasVersion && file.version > CurrentVersion)
+        {
+            fflist += file;
+        }
+    }
+
+    // sort and find most up to date file
+    if (!fflist.empty())
+    {
+        std::sort(fflist.begin(), fflist.end(),
+                  [](const FileInfo& a, const FileInfo& b)
+                  {
+                      return a.version > b.version;
+                  });
+
+        foundFile = fflist[0];
+        return true;
+    }
+    else
+    {
+        return false;
+    }
+}
diff --git a/src/updatechecker.h b/src/updatechecker.h
new file mode 100644
--- /dev/null
+++ b/src/updatechecker.h
@@ -0,0 +1,77 @@
+/*
+  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 .
+*/
+
+#ifndef UPDATECHECKER_H
+#define UPDATECHECKER_H
+
+#include 
+#include 
+#include 
+#include 
+
+#include "versionnumber.h"
+
+class UpdateChecker : public QObject
+{
+    Q_OBJECT
+public:
+    explicit UpdateChecker(QObject *parent = 0);
+
+    bool isChecking() const;
+
+signals:
+    void checkFinished(bool found, QString newVersion, QString downloadUrl);
+    void checkFailed(QString errorMessage);
+
+public slots:
+    void checkUpdate();
+    void cancelCheck();
+
+private:
+    enum class FileArch
+    {
+        unknown,
+        _i386,
+        amd64,
+        arm
+    };
+
+    struct FileInfo
+    {
+        QString name;
+        QString link;
+        bool hasVersion;
+        VersionNumber version;
+        FileArch arch;
+    };
+
+    QNetworkAccessManager nam;
+    QNetworkReply* activeReply;
+
+    /// Parses json and creates a list of files
+    bool parseData(const QJsonDocument& data, QList& files) const;
+    /// Finds the update file in the file list. Returns `-1` if no new version
+    /// is found.
+    bool findUpdate(const QList& files, FileInfo& foundFile) const;
+
+private slots:
+    void onReqFinished(QNetworkReply* reply);
+};
+
+#endif // UPDATECHECKER_H
diff --git a/src/versionnumber.cpp b/src/versionnumber.cpp
new file mode 100644
--- /dev/null
+++ b/src/versionnumber.cpp
@@ -0,0 +1,103 @@
+/*
+  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 
+
+#include "versionnumber.h"
+
+VersionNumber::VersionNumber(unsigned mj, unsigned mn, unsigned pt)
+{
+    major = mj;
+    minor = mn;
+    patch = pt;
+}
+
+QString VersionNumber::toString() const
+{
+    return QString("%1.%2.%3").arg(major).arg(minor).arg(patch);
+}
+
+bool VersionNumber::extract(const QString& str, VersionNumber& number)
+{
+    QRegularExpression regexp("(?:[-_vV \\t]|^)(?\\d+)"
+                              "(?:\\.(?\\d+))(?:\\.(?\\d+))?[-_ \\t]?");
+    auto match = regexp.match(str, 0, QRegularExpression::PartialPreferCompleteMatch);
+
+    if (!(match.hasMatch() || match.hasPartialMatch())) return false;
+
+    number.major = match.captured("major").toUInt();
+
+    auto zeroIfNull = [](QString str) -> unsigned
+        {
+            if (str.isNull()) return 0;
+            return str.toUInt();
+        };
+
+    number.minor = zeroIfNull(match.captured("minor"));
+    number.patch = zeroIfNull(match.captured("patch"));
+
+    return true;
+}
+
+bool operator==(const VersionNumber& lhs, const VersionNumber& rhs)
+{
+    return lhs.major == rhs.major &&
+           lhs.minor == rhs.minor &&
+           lhs.patch == rhs.patch;
+}
+
+bool operator<(const VersionNumber& lhs, const VersionNumber& rhs)
+{
+    if (lhs.major < rhs.major)
+    {
+        return true;
+    }
+    else if (lhs.major == rhs.major)
+    {
+        if (lhs.minor < rhs.minor)
+        {
+             return true;
+        }
+        else if (lhs.minor == rhs.minor)
+        {
+            if (lhs.patch < rhs.patch) return true;
+        }
+    }
+    return false;
+}
+
+bool operator>(const VersionNumber& lhs, const VersionNumber& rhs)
+{
+    if (lhs.major > rhs.major)
+    {
+        return true;
+    }
+    else if (lhs.major == rhs.major)
+    {
+        if (lhs.minor > rhs.minor)
+        {
+             return true;
+        }
+        else if (lhs.minor == rhs.minor)
+        {
+            if (lhs.patch > rhs.patch) return true;
+        }
+    }
+    return false;
+}
diff --git a/src/versionnumber.h b/src/versionnumber.h
new file mode 100644
--- /dev/null
+++ b/src/versionnumber.h
@@ -0,0 +1,46 @@
+/*
+  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 .
+*/
+
+#ifndef VERSIONNUMBER_H
+#define VERSIONNUMBER_H
+
+#include 
+
+struct VersionNumber
+{
+    unsigned major = 0;
+    unsigned minor = 0;
+    unsigned patch = 0;
+
+    VersionNumber(unsigned mj=0, unsigned mn=0, unsigned pt=0);
+
+    /// Convert version number to string.
+    QString toString() const;
+
+    /// Extracts the version number from given string.
+    static bool extract(const QString& str, VersionNumber& number);
+};
+
+bool operator==(const VersionNumber& lhs, const VersionNumber& rhs);
+bool operator<(const VersionNumber& lhs, const VersionNumber& rhs);
+bool operator>(const VersionNumber& lhs, const VersionNumber& rhs);
+
+const VersionNumber CurrentVersion(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
+
+#endif // VERSIONNUMBER_H
diff --git a/src/zoomer.cpp b/src/zoomer.cpp
--- a/src/zoomer.cpp
+++ b/src/zoomer.cpp
@@ -28,6 +28,8 @@ Zoomer::Zoomer(QWidget* widget, bool doR
 {
     is_panning = false;
 
+    setTrackerMode(AlwaysOn);
+
     // set corner widget between the scrollbars with default background color
     auto cornerWidget = new QWidget();
     auto bgColor = cornerWidget->palette().color(QPalette::Window).name();
@@ -62,7 +64,7 @@ QwtText Zoomer::trackerTextF(const QPoin
     QwtText b = ScrollZoomer::trackerTextF(pos);
 
     const QPolygon pa = selection();
-    if (pa.count() < 2)
+    if (!isActive() || pa.count() < 2)
     {
         return b;
     }
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,5 +1,5 @@
 #
-# Copyright © 2017 Hasan Yavuz Özderya
+# Copyright © 2018 Hasan Yavuz Özderya
 #
 # This file is part of serialplot.
 #
@@ -19,16 +19,77 @@
 
 # Find the QtWidgets library
 find_package(Qt5Widgets)
+find_package(Qt5Test)
 
 include_directories("../src")
 
 add_executable(Test EXCLUDE_FROM_ALL
   test.cpp
+  test_stream.cpp
+  ../src/samplepack.cpp
+  ../src/sink.cpp
+  ../src/source.cpp
+  ../src/indexbuffer.cpp
+  ../src/linindexbuffer.cpp
+  ../src/ringbuffer.cpp
+  ../src/readonlybuffer.cpp
+  ../src/stream.cpp
+  ../src/streamchannel.cpp
+  ../src/channelinfomodel.cpp  
   ../src/datachunk.cpp
-  ../src/chunkedbuffer.cpp)
+  ../src/chunkedbuffer.cpp
+  )
 add_test(NAME test1 COMMAND Test)
 qt5_use_modules(Test Widgets)
 
+qt5_wrap_ui(UI_FILES_T
+  ../src/binarystreamreadersettings.ui
+  ../src/asciireadersettings.ui
+  ../src/framedreadersettings.ui
+  ../src/demoreadersettings.ui
+  ../src/numberformatbox.ui
+  ../src/endiannessbox.ui
+  )
+
+# test for readers
+add_executable(TestReaders EXCLUDE_FROM_ALL
+  test_readers.cpp
+  ../src/samplepack.cpp
+  ../src/sink.cpp
+  ../src/source.cpp
+  ../src/abstractreader.cpp
+  ../src/binarystreamreader.cpp
+  ../src/binarystreamreadersettings.cpp
+  ../src/asciireader.cpp
+  ../src/asciireadersettings.cpp
+  ../src/framedreader.cpp
+  ../src/framedreadersettings.cpp
+  ../src/demoreader.cpp
+  ../src/demoreadersettings.cpp
+  ../src/commandedit.cpp
+  ../src/endiannessbox.cpp
+  ../src/numberformatbox.cpp
+  ../src/numberformat.cpp
+  ${UI_FILES_T}
+  )
+qt5_use_modules(TestReaders Widgets Test)
+add_test(NAME test_readers COMMAND TestReaders)
+
+# test for recroder
+add_executable(TestRecorder EXCLUDE_FROM_ALL
+  test_recorder.cpp
+  ../src/samplepack.cpp
+  ../src/sink.cpp
+  ../src/source.cpp
+  ../src/datarecorder.cpp
+)
+qt5_use_modules(TestRecorder Widgets Test)
+add_test(NAME test_recorder COMMAND TestRecorder)
+
 set(CMAKE_CTEST_COMMAND ctest -V)
 add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND})
-add_dependencies(check Test)
+add_dependencies(check
+  Test
+  TestReaders
+  TestRecorder
+  )
diff --git a/tests/catch.hpp b/tests/catch.hpp
--- a/tests/catch.hpp
+++ b/tests/catch.hpp
@@ -11542,4 +11542,3 @@ using Catch::Detail::Approx;
 #endif
 
 #endif // TWOBLUECUBES_SINGLE_INCLUDE_CATCH_HPP_INCLUDED
-
diff --git a/tests/test.cpp b/tests/test.cpp
--- a/tests/test.cpp
+++ b/tests/test.cpp
@@ -1,5 +1,5 @@
 /*
-  Copyright © 2017 Hasan Yavuz Özderya
+  Copyright © 2018 Hasan Yavuz Özderya
 
   This file is part of serialplot.
 
@@ -21,9 +21,370 @@
 #define CATCH_CONFIG_MAIN  // This tells Catch to provide a main() - only do this in one cpp file
 #include "catch.hpp"
 
+#include "samplepack.h"
+#include "source.h"
+#include "indexbuffer.h"
+#include "linindexbuffer.h"
+#include "ringbuffer.h"
+#include "readonlybuffer.h"
 #include "datachunk.h"
 #include "chunkedbuffer.h"
 
+#include "test_helpers.h"
+
+TEST_CASE("samplepack with no X", "[memory]")
+{
+    SamplePack pack(100, 3, false);
+
+    REQUIRE_FALSE(pack.hasX());
+    REQUIRE(pack.numChannels() == 3);
+    REQUIRE(pack.numSamples() == 100);
+
+    double* chan0 = pack.data(0);
+    double* chan1 = pack.data(1);
+    double* chan2 = pack.data(2);
+
+    REQUIRE(chan0 == chan1 - 100);
+    REQUIRE(chan1 == chan2 - 100);
+}
+
+TEST_CASE("samplepack with X", "[memory]")
+{
+    SamplePack pack(100, 3, true);
+
+    REQUIRE(pack.hasX());
+    REQUIRE(pack.numChannels() == 3);
+    REQUIRE(pack.numSamples() == 100);
+    REQUIRE(pack.xData() != nullptr);
+}
+
+TEST_CASE("samplepack copy", "[memory]")
+{
+    SamplePack pack(10, 3, true);
+
+    // fill test data
+    for (int i = 0; i < 10; i++)
+    {
+        pack.xData()[i] = i;
+        pack.data(0)[i] = i+5;
+        pack.data(1)[i] = i*2;
+        pack.data(2)[i] = i*3;
+    }
+
+    SamplePack other = pack;
+    // compare
+    for (int i = 0; i < 10; i++)
+    {
+        REQUIRE(other.xData()[i] == i);
+        REQUIRE(other.data(0)[i] == i+5);
+        REQUIRE(other.data(1)[i] == i*2);
+        REQUIRE(other.data(2)[i] == i*3);
+    }
+}
+
+TEST_CASE("sink", "[memory, stream]")
+{
+    TestSink sink;
+    SamplePack pack(100, 3, false);
+
+    sink.setNumChannels(3, false);
+    REQUIRE(sink.numChannels() == 3);
+
+    sink.feedIn(pack);
+    REQUIRE(sink.totalFed == 100);
+    sink.feedIn(pack);
+    REQUIRE(sink.totalFed == 200);
+
+    TestSink follower;
+
+    sink.connectFollower(&follower);
+    REQUIRE(follower.numChannels() == 3);
+    REQUIRE(follower.hasX() == false);
+
+    sink.feedIn(pack);
+    REQUIRE(sink.totalFed == 300);
+    REQUIRE(follower.totalFed == 100);
+
+    sink.setNumChannels(2, true);
+    REQUIRE(follower.numChannels() == 2);
+    REQUIRE(follower.hasX() == true);
+}
+
+TEST_CASE("sink must be created unconnected", "[memory, stream]")
+{
+    TestSink sink;
+    REQUIRE(sink.connectedSource() == NULL);
+}
+
+TEST_CASE("source", "[memory, stream]")
+{
+    TestSink sink;
+
+    TestSource source(3, false);
+
+    REQUIRE(source.numChannels() == 3);
+    REQUIRE(source.hasX() == false);
+
+    source.connectSink(&sink);
+    REQUIRE(sink.numChannels() == 3);
+    REQUIRE(sink.hasX() == false);
+
+    source._setNumChannels(5, true);
+    REQUIRE(sink.numChannels() == 5);
+    REQUIRE(sink.hasX() == true);
+
+    SamplePack pack(100, 5, true);
+    source._feed(pack);
+    REQUIRE(sink.totalFed == 100);
+
+    source.disconnect(&sink);
+    source._feed(pack);
+    REQUIRE(sink.totalFed == 100);
+}
+
+TEST_CASE("source must set/unset sink 'source'", "[memory, stream]")
+{
+    TestSink sink;
+    TestSource source(3, false);
+
+    source.connectSink(&sink);
+    REQUIRE(sink.connectedSource() == &source);
+
+    source.disconnect(&sink);
+    REQUIRE(sink.connectedSource() == NULL);
+}
+
+TEST_CASE("source disconnect all sinks", "[memory, stream]")
+{
+    TestSink sinks[3];
+    TestSource source(3, false);
+
+    // connect sinks
+    for (int i = 0; i < 3; i++)
+    {
+        source.connectSink(&sinks[i]);
+    }
+
+    source.disconnectSinks();
+    for (int i = 0; i < 3; i++)
+    {
+        REQUIRE(sinks[i].connectedSource() == NULL);
+    }
+}
+
+TEST_CASE("IndexBuffer", "[memory, buffer]")
+{
+    IndexBuffer buf(10);
+
+    REQUIRE(buf.size() == 10);
+    for (unsigned i = 0; i < 10; i++)
+    {
+        REQUIRE(buf.sample(i) == i);
+    }
+    auto l = buf.limits();
+    REQUIRE(l.start == 0);
+    REQUIRE(l.end == 9);
+
+    buf.resize(20);
+    REQUIRE(buf.size() == 20);
+    REQUIRE(buf.sample(15) == 15);
+    l = buf.limits();
+    REQUIRE(l.start == 0);
+    REQUIRE(l.end == 19);
+}
+
+TEST_CASE("LinIndexBuffer", "[memory, buffer]")
+{
+    LinIndexBuffer buf(10, 0., 3.0);
+
+    REQUIRE(buf.size() == 10);
+    REQUIRE(buf.sample(0) == 0.);
+    REQUIRE(buf.sample(9) == 3.0);
+    REQUIRE(buf.sample(4) == Approx(1+1/3.));
+
+    auto l = buf.limits();
+    REQUIRE(l.start == 0.);
+    REQUIRE(l.end == 3.);
+
+    buf.resize(20);
+    REQUIRE(buf.size() == 20);
+    REQUIRE(buf.sample(0) == 0.);
+    REQUIRE(buf.sample(9) == Approx(9.*3./19.));
+    REQUIRE(buf.sample(4) == Approx(4.*3./19.));
+    REQUIRE(buf.sample(19) == 3.0);
+
+    l = buf.limits();
+    REQUIRE(l.start == 0.);
+    REQUIRE(l.end == 3.0);
+
+    buf.setLimits({-5., 5.});
+    l = buf.limits();
+    REQUIRE(l.start == -5.0);
+    REQUIRE(l.end == 5.0);
+
+    REQUIRE(buf.sample(0) == -5.0);
+    REQUIRE(buf.sample(19) == 5.0);
+}
+
+TEST_CASE("RingBuffer sizing", "[memory, buffer]")
+{
+    RingBuffer buf(10);
+
+    REQUIRE(buf.size() == 10);
+
+    buf.resize(5);
+    REQUIRE(buf.size() == 5);
+
+    buf.resize(15);
+    REQUIRE(buf.size() == 15);
+}
+
+TEST_CASE("RingBuffer initial values should be 0", "[memory, buffer]")
+{
+    RingBuffer buf(10);
+
+    for (unsigned i = 0; i < 10; i++)
+    {
+        REQUIRE(buf.sample(i) == 0.);
+    }
+}
+
+TEST_CASE("RingBuffer data access", "[memory, buffer]")
+{
+    RingBuffer buf(10);
+    double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+
+    buf.addSamples(values, 10);
+
+    REQUIRE(buf.size() == 10);
+    for (unsigned i = 0; i < 10; i++)
+    {
+        REQUIRE(buf.sample(i) == values[i]);
+    }
+
+    buf.addSamples(values, 5);
+
+    REQUIRE(buf.size() == 10);
+    for (unsigned i = 0; i < 5; i++)
+    {
+        REQUIRE(buf.sample(i) == values[i+5]);
+    }
+    for (unsigned i = 5; i < 10; i++)
+    {
+        REQUIRE(buf.sample(i) == values[i-5]);
+    }
+}
+
+TEST_CASE("making RingBuffer bigger should keep end values", "[memory, buffer]")
+{
+    RingBuffer buf(5);
+    double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+
+    buf.addSamples(values, 5);
+    buf.resize(10);
+
+    REQUIRE(buf.size() == 10);
+    for (unsigned i = 0; i < 5; i++)
+    {
+        REQUIRE(buf.sample(i) == 0);
+    }
+    for (unsigned i = 5; i < 10; i++)
+    {
+        REQUIRE(buf.sample(i) == values[i-5]);
+    }
+}
+
+TEST_CASE("making RingBuffer smaller should keep end values", "[memory, buffer]")
+{
+    RingBuffer buf(10);
+    double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+
+    buf.addSamples(values, 10);
+    buf.resize(5);
+
+    REQUIRE(buf.size() == 5);
+    for (unsigned i = 0; i < 5; i++)
+    {
+        REQUIRE(buf.sample(i) == values[i+5]);
+    }
+}
+
+TEST_CASE("RingBuffer limits", "[memory, buffer]")
+{
+    RingBuffer buf(10);
+    double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+
+    auto lim = buf.limits();
+    REQUIRE(lim.start == 0.);
+    REQUIRE(lim.end == 0.);
+
+    buf.addSamples(values, 10);
+    lim = buf.limits();
+    REQUIRE(lim.start == 1.);
+    REQUIRE(lim.end == 10.);
+
+    buf.addSamples(&values[9], 1);
+    lim = buf.limits();
+    REQUIRE(lim.start == 2.);
+    REQUIRE(lim.end == 10.);
+
+    buf.addSamples(values, 9);
+    buf.addSamples(values, 1);
+    lim = buf.limits();
+    REQUIRE(lim.start == 1.);
+    REQUIRE(lim.end == 9.);
+}
+
+TEST_CASE("RingBuffer clear", "[memory, buffer]")
+{
+    RingBuffer buf(10);
+    double values[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+
+    buf.addSamples(values, 10);
+    buf.clear();
+
+    REQUIRE(buf.size() == 10);
+    for (unsigned i = 0; i < 10; i++)
+    {
+        REQUIRE(buf.sample(i) == 0.);
+    }
+    auto lim = buf.limits();
+    REQUIRE(lim.start == 0.);
+    REQUIRE(lim.end == 0.);
+}
+
+TEST_CASE("ReadOnlyBuffer", "[memory, buffer]")
+{
+    IndexBuffer source(10);
+
+    ReadOnlyBuffer buf(&source);
+
+    REQUIRE(buf.size() == 10);
+    auto lim = buf.limits();
+    REQUIRE(lim.start == 0.);
+    REQUIRE(lim.end == 9.);
+    for (unsigned i = 0; i < 10; i++)
+    {
+        REQUIRE(buf.sample(i) == i);
+    }
+}
+
+TEST_CASE("ReadOnlyBuffer sliced constructor", "[memory, buffer]")
+{
+    IndexBuffer source(10);
+
+    ReadOnlyBuffer buf(&source, 5, 4);
+
+    REQUIRE(buf.size() == 4);
+    auto lim = buf.limits();
+    REQUIRE(lim.start == 5.);
+    REQUIRE(lim.end == 8.);
+    for (unsigned i = 0; i < 4; i++)
+    {
+        REQUIRE(buf.sample(i) == (i + 5));
+    }
+}
+
 TEST_CASE("DataChunk created empty", "[memory]")
 {
     DataChunk c(0, 1000);
diff --git a/tests/test_helpers.h b/tests/test_helpers.h
new file mode 100644
--- /dev/null
+++ b/tests/test_helpers.h
@@ -0,0 +1,86 @@
+#ifndef TEST_HELPERS_H
+#define TEST_HELPERS_H
+
+#include "source.h"
+#include "sink.h"
+
+class TestSink : public Sink
+{
+public:
+    int totalFed;
+    int _numChannels;
+    bool _hasX;
+
+    TestSink()
+        {
+            totalFed = 0;
+            _numChannels = 0;
+            _hasX = false;
+        };
+
+    void feedIn(const SamplePack& data)
+        {
+            REQUIRE(data.numChannels() == numChannels());
+
+            totalFed += data.numSamples();
+
+            Sink::feedIn(data);
+        };
+
+    void setNumChannels(unsigned nc, bool x)
+        {
+            _numChannels = nc;
+            _hasX = x;
+
+            Sink::setNumChannels(nc, x);
+        };
+
+    virtual unsigned numChannels() const
+        {
+            return _numChannels;
+        };
+
+    virtual bool hasX() const
+        {
+            return _hasX;
+        };
+};
+
+class TestSource : public Source
+{
+public:
+    int _numChannels;
+    bool _hasX;
+
+    TestSource(unsigned nc, bool x)
+        {
+            _numChannels = nc;
+            _hasX = x;
+        };
+
+    virtual unsigned numChannels() const
+        {
+            return _numChannels;
+        };
+
+    virtual bool hasX() const
+        {
+            return _hasX;
+        };
+
+    void _feed(const SamplePack& data) const
+        {
+            feedOut(data);
+        };
+
+    void _setNumChannels(unsigned nc, bool x)
+        {
+            _numChannels = nc;
+            _hasX = x;
+
+            updateNumChannels();
+        };
+};
+
+
+#endif // TEST_HELPERS_H
diff --git a/tests/test_readers.cpp b/tests/test_readers.cpp
new file mode 100644
--- /dev/null
+++ b/tests/test_readers.cpp
@@ -0,0 +1,210 @@
+/*
+  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 .
+*/
+
+// This tells Catch to provide a main() - only do this in one cpp file per executable
+#define CATCH_CONFIG_RUNNER
+#include "catch.hpp"
+
+#include 
+#include 
+#include "binarystreamreader.h"
+#include "asciireader.h"
+#include "framedreader.h"
+#include "demoreader.h"
+
+#include "test_helpers.h"
+
+static const int READYREAD_TIMEOUT = 10; // milliseconds
+
+TEST_CASE("reading data with BinaryStreamReader", "[reader]")
+{
+    QBuffer bufferDev;
+    BinaryStreamReader bs(&bufferDev);
+    bs.enable(true);
+
+    TestSink sink;
+    bs.connectSink(&sink);
+
+    REQUIRE(sink._numChannels == 1);
+    REQUIRE(sink._hasX == false);
+
+    bufferDev.open(QIODevice::ReadWrite);
+    const char data[] = {0x01, 0x02, 0x03, 0x04};
+    bufferDev.write(data, 4);
+    bufferDev.seek(0);
+
+    QSignalSpy spy(&bufferDev, SIGNAL(readyRead()));
+    REQUIRE(spy.wait(READYREAD_TIMEOUT));
+    REQUIRE(sink.totalFed == 4);
+}
+
+TEST_CASE("disabled BinaryStreamReader shouldn't read", "[reader]")
+{
+    QBuffer bufferDev;
+    BinaryStreamReader bs(&bufferDev); // disabled by default
+
+    TestSink sink;
+    bs.connectSink(&sink);
+
+    REQUIRE(sink._numChannels == 1);
+    REQUIRE(sink._hasX == false);
+
+    bufferDev.open(QIODevice::ReadWrite);
+    const char data[] = {0x01, 0x02, 0x03, 0x04};
+    bufferDev.write(data, 4);
+    bufferDev.seek(0);
+
+    QSignalSpy spy(&bufferDev, SIGNAL(readyRead()));
+    // readyRead isn't signaled because there are no connections to it
+    REQUIRE_FALSE(spy.wait(READYREAD_TIMEOUT));
+    REQUIRE(sink.totalFed == 0);
+}
+
+TEST_CASE("reading data with AsciiReader", "[reader, ascii]")
+{
+    QBuffer bufferDev;
+    AsciiReader reader(&bufferDev);
+    reader.enable(true);
+
+    TestSink sink;
+    reader.connectSink(&sink);
+
+    REQUIRE(sink._numChannels == 1);
+    REQUIRE(sink._hasX == false);
+
+    // inject data to the buffer
+    bufferDev.open(QIODevice::ReadWrite);
+    bufferDev.write("0,1,3\n0,1,3\n0,1,3\n0,1,3\n");
+    bufferDev.seek(0);
+
+    QSignalSpy spy(&bufferDev, SIGNAL(readyRead()));
+    REQUIRE(spy.wait(READYREAD_TIMEOUT));
+    REQUIRE(sink._numChannels == 3);
+    REQUIRE(sink._hasX == false);
+    REQUIRE(sink.totalFed == 3);
+}
+
+TEST_CASE("AsciiReader shouldn't read when disabled", "[reader, ascii]")
+{
+    QBuffer bufferDev;
+    AsciiReader reader(&bufferDev); // disabled by default
+
+    TestSink sink;
+    reader.connectSink(&sink);
+
+    REQUIRE(sink._numChannels == 1);
+    REQUIRE(sink._hasX == false);
+
+    // inject data to the buffer
+    bufferDev.open(QIODevice::ReadWrite);
+    bufferDev.write("0,1,3\n0,1,3\n0,1,3\n0,1,3\n");
+    bufferDev.seek(0);
+
+    QSignalSpy spy(&bufferDev, SIGNAL(readyRead()));
+    REQUIRE_FALSE(spy.wait(READYREAD_TIMEOUT));
+    REQUIRE(sink._numChannels == 1);
+    REQUIRE(sink._hasX == false);
+    REQUIRE(sink.totalFed == 0);
+}
+
+TEST_CASE("reading data with FramedReader", "[reader]")
+{
+    QBuffer bufferDev;
+    FramedReader reader(&bufferDev);
+    reader.enable(true);
+
+    TestSink sink;
+    reader.connectSink(&sink);
+
+    REQUIRE(sink._numChannels == 1);
+    REQUIRE(sink._hasX == false);
+
+    bufferDev.open(QIODevice::ReadWrite);
+    const uint8_t data[] = {0xAA, 0xBB, 4, 0x01, 0x02, 0x03, 0x04};
+    bufferDev.write((const char*) data, 7);
+    bufferDev.seek(0);
+
+    QSignalSpy spy(&bufferDev, SIGNAL(readyRead()));
+    REQUIRE(spy.wait(READYREAD_TIMEOUT));
+    REQUIRE(sink.totalFed == 4);
+}
+
+TEST_CASE("FramedReader shouldn't read when disabled", "[reader]")
+{
+    QBuffer bufferDev;
+    FramedReader reader(&bufferDev);
+
+    TestSink sink;
+    reader.connectSink(&sink);
+
+    REQUIRE(sink._numChannels == 1);
+    REQUIRE(sink._hasX == false);
+
+    bufferDev.open(QIODevice::ReadWrite);
+    const uint8_t data[] = {0xAA, 0xBB, 4, 0x01, 0x02, 0x03, 0x04};
+    bufferDev.write((const char*) data, 7);
+    bufferDev.seek(0);
+
+    QSignalSpy spy(&bufferDev, SIGNAL(readyRead()));
+    REQUIRE_FALSE(spy.wait(READYREAD_TIMEOUT));
+    REQUIRE(sink.totalFed == 0);
+}
+
+TEST_CASE("Generating data with DemoReader", "[reader, demo]")
+{
+    QBuffer bufferDev;          // not actually used
+    DemoReader demoReader(&bufferDev);
+    demoReader.enable(true);
+
+    TestSink sink;
+    demoReader.connectSink(&sink);
+    REQUIRE(sink._numChannels == 1);
+    REQUIRE(sink._hasX == false);
+
+    // we need to wait somehow, we are not actually looking for signals
+    QSignalSpy spy(&bufferDev, SIGNAL(readyRead()));
+    REQUIRE_FALSE(spy.wait(1000)); // we need some time for demoreader to produce data
+    REQUIRE(sink.totalFed >= 9);
+}
+
+TEST_CASE("DemoReader shouldn't generate data when paused", "[reader, demo]")
+{
+    QBuffer bufferDev;          // not actually used
+    DemoReader demoReader(&bufferDev); // paused by default
+
+    TestSink sink;
+    demoReader.connectSink(&sink);
+    REQUIRE(sink._numChannels == 1);
+
+    // we need to wait somehow, we are not actually looking for signals
+    QSignalSpy spy(&bufferDev, SIGNAL(readyRead()));
+    REQUIRE_FALSE(spy.wait(1000)); // we need some time for demoreader to produce data
+    REQUIRE(sink.totalFed == 0);
+}
+
+// Note: this is added because `QApplication` must be created for widgets
+#include 
+int main(int argc, char* argv[])
+{
+    QApplication a(argc, argv);
+
+    int result = Catch::Session().run( argc, argv );
+
+    return result;
+}
diff --git a/tests/test_recorder.cpp b/tests/test_recorder.cpp
new file mode 100644
--- /dev/null
+++ b/tests/test_recorder.cpp
@@ -0,0 +1,107 @@
+/*
+  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 .
+*/
+
+#define CATCH_CONFIG_MAIN  // This tells Catch to provide a main() - only do this in one cpp file
+#include "catch.hpp"
+
+#include 
+#include "datarecorder.h"
+#include "test_helpers.h"
+
+#define TEST_FILE_NAME   "sp_test_recording.csv"
+
+TEST_CASE("test recording single channel", "[recorder]")
+{
+    DataRecorder rec;
+    TestSource source(1, false);
+
+    // temporary file, remove if exists
+    auto fileName = QDir::tempPath() + QString("/" TEST_FILE_NAME);
+    if (QFile::exists(fileName)) QFile::remove(fileName);
+
+    // connect source → sink
+    source.connectSink(&rec);
+
+    // prepare data
+    QStringList channelNames({"Channel 1"});
+    SamplePack samples(5, 1);
+    for (int i = 0; i < 5; i++)
+    {
+        samples.data(0)[i] = i+1;
+    }
+
+    // test
+    rec.startRecording(fileName, ",", channelNames, false);
+    source._feed(samples);
+    rec.stopRecording();
+
+    // read file contents back
+    QFile recordFile(fileName);
+    REQUIRE(recordFile.open(QIODevice::ReadOnly | QIODevice::Text));
+    // NOTE: mind the extra parantheses, otherwise 'catch' macros fail to compile
+    REQUIRE((recordFile.readLine() == "Channel 1\n"));
+    for (int i = 0; i < 5; i++)
+        REQUIRE((recordFile.readLine() == QString("%1\n").arg(i+1)));
+
+    // cleanup
+    if (QFile::exists(fileName)) QFile::remove(fileName);
+}
+
+TEST_CASE("test recording multiple channels", "[recorder]")
+{
+    DataRecorder rec;
+    TestSource source(3, false);
+
+    // temporary file, remove if exists
+    auto fileName = QDir::tempPath() + QString("/" TEST_FILE_NAME);
+    if (QFile::exists(fileName)) QFile::remove(fileName);
+
+    // connect source → sink
+    source.connectSink(&rec);
+
+    // prepare data
+    QStringList channelNames({"Channel 1", "Channel 2", "Channel 3"});
+    SamplePack samples(5, 3);
+    for (int ci = 0; ci < 3; ci++)
+    {
+        for (int i = 0; i < 5; i++)
+        {
+            samples.data(ci)[i] = (ci+1)*(i+1);
+        }
+    }
+
+    // test
+    rec.startRecording(fileName, ",", channelNames, false);
+    source._feed(samples);
+    rec.stopRecording();
+
+    // read file contents back
+    QFile recordFile(fileName);
+    REQUIRE(recordFile.open(QIODevice::ReadOnly | QIODevice::Text));
+    // NOTE: mind the extra parantheses, otherwise 'catch' macros fail to compile
+    REQUIRE((recordFile.readLine() == "Channel 1,Channel 2,Channel 3\n"));
+    REQUIRE((recordFile.readLine() == "1,2,3\n"));
+    REQUIRE((recordFile.readLine() == "2,4,6\n"));
+    REQUIRE((recordFile.readLine() == "3,6,9\n"));
+    REQUIRE((recordFile.readLine() == "4,8,12\n"));
+    REQUIRE((recordFile.readLine() == "5,10,15\n"));
+
+    // cleanup
+    if (QFile::exists(fileName)) QFile::remove(fileName);
+}
diff --git a/tests/test_stream.cpp b/tests/test_stream.cpp
new file mode 100644
--- /dev/null
+++ b/tests/test_stream.cpp
@@ -0,0 +1,246 @@
+/*
+  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 "stream.h"
+
+#include "catch.hpp"
+#include "test_helpers.h"
+
+TEST_CASE("construction of stream with default values", "[memory, stream]")
+{
+    // default values are an empty stream with no channels
+    Stream s;
+
+    REQUIRE(s.numChannels() == 0);
+    REQUIRE(!s.hasX());
+    REQUIRE(s.numSamples() == 0);
+}
+
+TEST_CASE("construction of stream with parameters", "[memory, stream]")
+{
+    Stream s(4, true, 100);
+
+    REQUIRE(s.numChannels() == 4);
+    REQUIRE(s.hasX());
+    REQUIRE(s.numSamples() == 100);
+
+    for (unsigned i = 0; i < 4; i++)
+    {
+        const StreamChannel* c = s.channel(i);
+        REQUIRE(c != NULL);
+        REQUIRE(c->index() == i);
+    }
+}
+
+TEST_CASE("changing stream number of channels via sink", "[memory, stream, sink]")
+{
+    Stream s;
+    TestSource so(3, false);
+    so.connectSink(&s);
+
+    // nc=3, x= false
+    REQUIRE(s.numChannels() == 3);
+    REQUIRE(!s.hasX());
+    for (unsigned i = 0; i < 3; i++)
+    {
+        const StreamChannel* c = s.channel(i);
+        REQUIRE(c != NULL);
+        REQUIRE(c->index() == i);
+    }
+
+    // increase nc value, add X
+    so._setNumChannels(5, true);
+
+    REQUIRE(s.numChannels() == 5);
+    REQUIRE(s.hasX());
+
+    for (unsigned i = 0; i < 5; i++)
+    {
+        const StreamChannel* c = s.channel(i);
+        REQUIRE(c != NULL);
+        REQUIRE(c->index() == i);
+    }
+
+    // reduce nc value, remove X
+    so._setNumChannels(1, false);
+
+    REQUIRE(s.numChannels() == 1);
+    REQUIRE(!s.hasX());
+
+    for (unsigned i = 0; i < 1; i++)
+    {
+        const StreamChannel* c = s.channel(i);
+        REQUIRE(c != NULL);
+        REQUIRE(c->index() == i);
+    }
+}
+
+TEST_CASE("adding data to a stream with no X", "[memory, stream, data, sink]")
+{
+    Stream s(3, false, 10);
+
+    // prepare data
+    SamplePack pack(5, 3, false);
+    for (unsigned ci = 0; ci < 3; ci++)
+    {
+        for (unsigned i = 0; i < 5; i++)
+        {
+            pack.data(ci)[i] = i;
+        }
+    }
+
+    TestSource so(3, false);
+    so.connectSink(&s);
+
+    // test
+    so._feed(pack);
+
+    for (unsigned ci = 0; ci < 3; ci++)
+    {
+        const StreamChannel* c = s.channel(ci);
+        const FrameBuffer* y = c->yData();
+
+        for (unsigned i = 0; i < 5; i++)
+        {
+            REQUIRE(y->sample(i) == 0);
+        }
+        for (unsigned i = 5; i < 10; i++)
+        {
+            REQUIRE(y->sample(i) == i-5);
+        }
+    }
+}
+
+TEST_CASE("adding data to a stream with X", "[memory, stream, data, sink]")
+{
+    Stream s(3, true, 10);
+
+    // prepare data
+    SamplePack pack(5, 3, true);
+    for (unsigned ci = 0; ci < 3; ci++)
+    {
+        for (unsigned i = 0; i < 5; i++)
+        {
+            pack.data(ci)[i] = i;
+        }
+    }
+    // x data
+    for (unsigned i = 0; i < 5; i++)
+    {
+        pack.xData()[i] = i+10;
+    }
+
+    TestSource so(3, true);
+    so.connectSink(&s);
+
+    // test
+    so._feed(pack);
+
+    for (unsigned ci = 0; ci < 3; ci++)
+    {
+        const StreamChannel* c = s.channel(ci);
+        const FrameBuffer* y = c->yData();
+
+        for (unsigned i = 0; i < 5; i++)
+        {
+            REQUIRE(y->sample(i) == 0);
+        }
+        for (unsigned i = 5; i < 10; i++)
+        {
+            REQUIRE(y->sample(i) == i-5);
+        }
+    }
+
+    // check x
+    const FrameBuffer* x = s.channel(0)->xData();
+    for (unsigned i = 0; i < 5; i++)
+    {
+        REQUIRE(x->sample(i) == 0);
+    }
+    for (unsigned i = 5; i < 10; i++)
+    {
+        REQUIRE(x->sample(i) == (i-5)+10);
+    }
+}
+
+TEST_CASE("paused stream shouldn't store data", "[memory, stream, pause]")
+{
+    Stream s(3, false, 10);
+
+    // prepare data
+    SamplePack pack(5, 3, false);
+    for (unsigned ci = 0; ci < 3; ci++)
+    {
+        for (unsigned i = 0; i < 5; i++)
+        {
+            pack.data(ci)[i] = i;
+        }
+    }
+
+    TestSource so(3, false);
+    so.connectSink(&s);
+
+    // test
+    s.pause(true);
+    so._feed(pack);
+
+    for (unsigned ci = 0; ci < 3; ci++)
+    {
+        const StreamChannel* c = s.channel(ci);
+        const FrameBuffer* y = c->yData();
+
+        for (unsigned i = 0; i < 10; i++)
+        {
+            REQUIRE(y->sample(i) == 0);
+        }
+    }
+}
+
+TEST_CASE("clear stream data", "[memory, stream, pause]")
+{
+    Stream s(3, false, 10);
+
+    // prepare data
+    SamplePack pack(5, 3, false);
+    for (unsigned ci = 0; ci < 3; ci++)
+    {
+        for (unsigned i = 0; i < 5; i++)
+        {
+            pack.data(ci)[i] = i;
+        }
+    }
+
+    TestSource so(3, false);
+    so.connectSink(&s);
+
+    // test
+    so._feed(pack);
+    s.clear();
+
+    for (unsigned ci = 0; ci < 3; ci++)
+    {
+        const StreamChannel* c = s.channel(ci);
+        const FrameBuffer* y = c->yData();
+
+        for (unsigned i = 0; i < 10; i++)
+        {
+            REQUIRE(y->sample(i) == 0);
+        }
+    }
+}