diff --git a/eotimeseriesviewer/profilevisualization.py b/eotimeseriesviewer/profilevisualization.py
index f36d9b4b5f1bf36c5e64a2703271ff97d1976b49..3e4c4c864475eb3ab93ffdb6d87f21055c5f10ad 100644
--- a/eotimeseriesviewer/profilevisualization.py
+++ b/eotimeseriesviewer/profilevisualization.py
@@ -144,482 +144,347 @@ class _SensorPoints(pg.PlotDataItem):
 
 
 
-class PlotSettingsModel2DWidgetDelegate(QStyledItemDelegate):
-    """
+class SensorPixelDataMemoryLayer(QgsVectorLayer):
 
-    """
-    def __init__(self, tableView, temporalProfileLayer, parent=None):
-        assert isinstance(tableView, QTableView)
-        assert isinstance(temporalProfileLayer, TemporalProfileLayer)
-        super(PlotSettingsModel2DWidgetDelegate, self).__init__(parent=parent)
-        self._preferedSize = QgsFieldExpressionWidget().sizeHint()
-        self.mTableView = tableView
-        self.mTemporalProfileLayer = temporalProfileLayer
-        self.mTimeSeries = temporalProfileLayer.timeSeries()
+    def __init__(self, sensor, crs=None):
+        assert isinstance(sensor, SensorInstrument)
+        if crs is None:
+            crs = QgsCoordinateReferenceSystem('EPSG:4862')
+        uri = 'Point?crs={}'.format(crs.authid())
+        super(SensorPixelDataMemoryLayer, self).__init__(uri, 'Pixels_sensor_' + sensor.name(), 'memory', False)
+        self.mSensor = sensor
 
-        self.mSensorLayers = {}
+        #initialize fields
+        assert self.startEditing()
+        # standard field names, types, etc.
+        fieldDefs = [('pxid', QVariant.String, 'integer'),
+                     ('date', QVariant.String, 'char'),
+                     ('doy', QVariant.Int, 'integer'),
+                     ('geo_x', QVariant.Double, 'decimal'),
+                     ('geo_y', QVariant.Double, 'decimal'),
+                     ('px_x', QVariant.Int, 'integer'),
+                     ('px_y', QVariant.Int, 'integer'),
+                     ]
+        # one field for each band
+        for b in range(sensor.nb):
+            fName = 'b{}'.format(b + 1)
+            fieldDefs.append((fName, QVariant.Double, 'decimal'))
 
-    def setItemDelegates(self, tableView):
-        assert isinstance(tableView, QTableView)
-        model = tableView.model()
+        # initialize fields
+        for fieldDef in fieldDefs:
+            field = QgsField(fieldDef[0], fieldDef[1], fieldDef[2])
+            self.addAttribute(field)
+        self.commitChanges()
 
-        assert isinstance(model, PlotSettingsModel2D)
-        for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]:
-            i = model.columnNames.index(c)
-            tableView.setItemDelegateForColumn(i, self)
+    def sensor(self):
+        return self.mSensor
 
-    def getColumnName(self, index):
-        assert index.isValid()
-        model = index.model()
-        assert isinstance(model, PlotSettingsModel2D)
-        return model.columnNames[index.column()]
-    """
-    def sizeHint(self, options, index):
-        s = super(ExpressionDelegate, self).sizeHint(options, index)
-        exprString = self.tableView.model().data(index)
-        l = QLabel()
-        l.setText(exprString)
-        x = l.sizeHint().width() + 100
-        s = QSize(x, s.height())
-        return self._preferedSize
-    """
-    def exampleLyr(self, sensor):
-        # if isinstance(sensor, SensorInstrument):
-        if sensor not in self.mSensorLayers.keys():
+    def nPixels(self):
+        raise NotImplementedError()
 
-            crs = QgsCoordinateReferenceSystem('EPSG:4862')
-            uri = 'Point?crs={}'.format(crs.authid())
-            lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory')
-            f = sensorExampleQgsFeature(sensor)
-            assert isinstance(f, QgsFeature)
-            assert lyr.startEditing()
-            for field in f.fields():
-                lyr.addAttribute(field)
-            lyr.addFeature(f)
-            lyr.commitChanges()
-            self.mSensorLayers[sensor] = lyr
-        return self.mSensorLayers[sensor]
+    def dates(self):
+        raise NotImplementedError()
 
-    def createEditor(self, parent, option, index):
-        cname = self.getColumnName(index)
-        model = self.mTableView.model()
-        w = None
-        if index.isValid() and isinstance(model, PlotSettingsModel2D):
-            plotStyle = model.idx2plotStyle(index)
 
-            if isinstance(plotStyle, TemporalProfile2DPlotStyle):
-                if cname == model.cnExpression:
 
-                    w = QgsFieldExpressionWidget(parent=parent)
-                    w.setExpressionDialogTitle('Values')
-                    w.setToolTip('Set an expression to specify the image band or calculate a spectral index.')
-                    w.fieldChanged[str, bool].connect(lambda n, b: self.checkData(index, w, w.expression()))
-                    w.setExpression(plotStyle.expression())
-                    plotStyle.sigSensorChanged.connect(lambda s: w.setLayer(self.exampleLyr(s)))
-                    if isinstance(plotStyle.sensor(), SensorInstrument):
-                        w.setLayer(self.exampleLyr(plotStyle.sensor()))
 
+class PlotSettingsModel3D(QAbstractTableModel):
 
+    #sigSensorAdded = pyqtSignal(SensorPlotSettings)
+    sigVisibilityChanged = pyqtSignal(TemporalProfile2DPlotStyle)
+    sigPlotStylesAdded = pyqtSignal(list)
+    sigPlotStylesRemoved = pyqtSignal(list)
 
+    def __init__(self, parent=None, *args):
 
-                elif cname == model.cnStyle:
-                    w = PlotStyleButton(parent=parent)
-                    w.setPlotStyle(plotStyle)
-                    w.setToolTip('Set style.')
-                    w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle()))
+        #assert isinstance(tableView, QTableView)
 
-                elif cname == model.cnSensor:
-                    w = QComboBox(parent=parent)
-                    m = SensorListModel(self.mTimeSeries)
-                    w.setModel(m)
+        super(PlotSettingsModel3D, self).__init__(parent=parent)
+        self.mTimeSeries = None
+        self.cnID = 'ID'
+        self.cnExpression = LABEL_EXPRESSION_3D
+        self.cnTemporalProfile = 'Coordinate'
+        self.cnStyle = 'Style'
+        self.cnSensor = 'Sensor'
+        self.columnNames = [self.cnTemporalProfile, self.cnSensor, self.cnStyle, self.cnExpression]
+        self.mPlotSettings = []
+        #assert isinstance(plotWidget, DateTimePlotWidget)
 
-                elif cname == model.cnTemporalProfile:
-                    w = QgsFeatureListComboBox(parent=parent)
-                    w.setSourceLayer(self.mTemporalProfileLayer)
-                    w.setIdentifierField('id')
-                    w.setDisplayExpression('to_string("id")+\'  \'+"name"')
-                    w.setAllowNull(False)
-                else:
-                    raise NotImplementedError()
-        return w
+        self.sortColumnIndex = 0
+        self.sortOrder = Qt.AscendingOrder
 
-    def checkData(self, index, w, value):
-        assert isinstance(index, QModelIndex)
-        model = self.mTableView.model()
-        if index.isValid() and isinstance(model, PlotSettingsModel2D):
-            plotStyle = model.idx2plotStyle(index)
-            assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
-            if isinstance(w, QgsFieldExpressionWidget):
-                assert value == w.expression()
-                assert w.isExpressionValid(value) == w.isValidExpression()
 
-                if w.isValidExpression():
-                    self.commitData.emit(w)
-                else:
-                    s = ""
-                    #print(('Delegate commit failed',w.asExpression()))
-            if isinstance(w, PlotStyleButton):
+        self.sort(0, Qt.AscendingOrder)
 
-                self.commitData.emit(w)
+    def hasStyleForSensor(self, sensor):
+        assert isinstance(sensor, SensorInstrument)
+        for plotStyle in self.mPlotSettings:
+            assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
+            if plotStyle.sensor() == sensor:
+                return True
+        return False
 
-    def setEditorData(self, editor, index):
-        cname = self.getColumnName(index)
-        model = self.mTableView.model()
 
-        w = None
-        if index.isValid() and isinstance(model, PlotSettingsModel2D):
-            cname = self.getColumnName(index)
-            if cname == model.cnExpression:
-                lastExpr = index.model().data(index, Qt.DisplayRole)
-                assert isinstance(editor, QgsFieldExpressionWidget)
-                editor.setProperty('lastexpr', lastExpr)
-                editor.setField(lastExpr)
 
-            elif cname == model.cnStyle:
-                style = index.data()
-                assert isinstance(editor, PlotStyleButton)
-                editor.setPlotStyle(style)
+    def onSensorRemoved(self, sensor):
+        assert isinstance(sensor, SensorInstrument)
+        self.removePlotStyles([s for s in self.mPlotSettings if s.sensor() == sensor])
 
-            elif cname == model.cnSensor:
-                assert isinstance(editor, QComboBox)
-                m = editor.model()
-                assert isinstance(m, SensorListModel)
-                sensor = index.data(role=Qt.UserRole)
-                if isinstance(sensor, SensorInstrument):
-                    idx = m.sensor2idx(sensor)
-                    editor.setCurrentIndex(idx.row())
-            elif cname == model.cnTemporalProfile:
-                assert isinstance(editor, QgsFeatureListComboBox)
-                value = editor.identifierValue()
-                if value != QVariant():
-                    plotStyle = index.data(role=Qt.UserRole)
-                    TP = plotStyle.temporalProfile()
-                    editor.setIdentifierValue(TP.id())
-                else:
-                    s  = ""
-            else:
-                raise NotImplementedError()
 
-    def setModelData(self, w, model, index):
-        cname = self.getColumnName(index)
-        model = self.mTableView.model()
 
-        if index.isValid() and isinstance(model, PlotSettingsModel2D):
-            if cname == model.cnExpression:
-                assert isinstance(w, QgsFieldExpressionWidget)
-                expr = w.asExpression()
-                exprLast = model.data(index, Qt.DisplayRole)
+    def __len__(self):
+        return len(self.mPlotSettings)
 
-                if w.isValidExpression() and expr != exprLast:
-                    model.setData(index, w.asExpression(), Qt.EditRole)
+    def __iter__(self):
+        return iter(self.mPlotSettings)
 
-            elif cname == model.cnStyle:
-                if isinstance(w, PlotStyleButton):
-                    model.setData(index, w.plotStyle(), Qt.EditRole)
+    def __getitem__(self, slice):
+        return self.mPlotSettings[slice]
 
-            elif cname == model.cnSensor:
-                assert isinstance(w, QComboBox)
-                sensor = w.itemData(w.currentIndex(), role=Qt.UserRole)
-                if isinstance(sensor, SensorInstrument):
-                    model.setData(index, sensor, Qt.EditRole)
+    def __contains__(self, item):
+        return item in self.mPlotSettings
 
-            elif cname == model.cnTemporalProfile:
-                assert isinstance(w, QgsFeatureListComboBox)
-                fid = w.identifierValue()
-                if isinstance(fid, int):
-                    TP = self.mTemporalProfileLayer.mProfiles.get(fid)
-                    model.setData(index, TP, Qt.EditRole)
 
-            else:
-                raise NotImplementedError()
+    def columnIndex(self, name):
+        return self.columnNames.index(name)
 
 
+    def insertPlotStyles(self, plotStyles, i=None):
+        """
+        Inserts PlotStyle
+        :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle]
+        :param i: index to insert, defaults to the last list position
+        """
+        if isinstance(plotStyles, TemporalProfile3DPlotStyle):
+            plotStyles = [plotStyles]
+        assert isinstance(plotStyles, list)
+        for plotStyle in plotStyles:
+            assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
 
-class PlotSettingsModel3DWidgetDelegate(QStyledItemDelegate):
-    """
+        if i is None:
+            i = len(self.mPlotSettings)
 
-    """
-    def __init__(self, tableView, temporalProfileLayer, parent=None):
-        assert isinstance(tableView, QTableView)
-        assert isinstance(temporalProfileLayer, TemporalProfileLayer)
-        super(PlotSettingsModel3DWidgetDelegate, self).__init__(parent=parent)
-        self._preferedSize = QgsFieldExpressionWidget().sizeHint()
-        self.mTableView = tableView
-        self.mTimeSeries = temporalProfileLayer.timeSeries()
-        self.mTemporalProfileLayer = temporalProfileLayer
-        self.mSensorLayers = {}
+        if len(plotStyles) > 0:
+            self.beginInsertRows(QModelIndex(), i, i + len(plotStyles)-1)
+            for j, plotStyle in enumerate(plotStyles):
+                assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
+                self.mPlotSettings.insert(i+j, plotStyle)
+            self.endInsertRows()
+            self.sigPlotStylesAdded.emit(plotStyles)
 
+    def removePlotStyles(self, plotStyles):
+        """
+        Removes PlotStyle instances
+        :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle]
+        """
+        if isinstance(plotStyles, TemporalProfile3DPlotStyle):
+            plotStyles = [plotStyles]
+        assert isinstance(plotStyles, list)
 
+        if len(plotStyles) > 0:
+            for plotStyle in plotStyles:
+                assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
+                if plotStyle in self.mPlotSettings:
+                    idx = self.plotStyle2idx(plotStyle)
+                    self.beginRemoveRows(QModelIndex(), idx.row(),idx.row())
+                    self.mPlotSettings.remove(plotStyle)
+                    self.endRemoveRows()
+            self.sigPlotStylesRemoved.emit(plotStyles)
 
+    def sort(self, col, order):
+        if self.rowCount() == 0:
+            return
 
 
+        colName = self.columnames[col]
+        r = order != Qt.AscendingOrder
 
+        #self.beginMoveRows(idxSrc,
 
-    def setItemDelegates(self, tableView):
-        assert isinstance(tableView, QTableView)
-        model = tableView.model()
-        assert isinstance(model, PlotSettingsModel3D)
-        for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]:
-            i = model.columnNames.index(c)
-            tableView.setItemDelegateForColumn(i, self)
+        if colName == self.cnSensor:
+            self.mPlotSettings.sort(key = lambda sv:sv.sensor().name(), reverse=r)
 
-    def getColumnName(self, index):
-        assert index.isValid()
-        model = index.model()
-        assert isinstance(model, PlotSettingsModel3D)
-        return model.columnNames[index.column()]
-    """
-    def sizeHint(self, options, index):
-        s = super(ExpressionDelegate, self).sizeHint(options, index)
-        exprString = self.tableView.model().data(index)
-        l = QLabel()
-        l.setText(exprString)
-        x = l.sizeHint().width() + 100
-        s = QSize(x, s.height())
-        return self._preferedSize
-    """
-    def createEditor(self, parent, option, index):
-        cname = self.getColumnName(index)
-        model = self.mTableView.model()
-        w = None
-        if index.isValid() and isinstance(model, PlotSettingsModel3D):
-            plotStyle = model.idx2plotStyle(index)
-            if isinstance(plotStyle, TemporalProfile3DPlotStyle):
-                if cname == model.cnExpression:
-                    w = QgsFieldExpressionWidget(parent=parent)
-                    w.setExpression(plotStyle.expression())
-                    w.setLayer(self.exampleLyr(plotStyle.sensor()))
-                    def onSensorAdded(s):
-                        w.setLayer(self.exampleLyr(s))
-                    #plotStyle.sigSensorChanged.connect(lambda s : w.setLayer(self.exampleLyr(s)))
-                    plotStyle.sigSensorChanged.connect(onSensorAdded)
-                    w.setExpressionDialogTitle('Values')
-                    w.setToolTip('Set an expression to specify the image band or calculate a spectral index.')
-                    w.fieldChanged[str,bool].connect(lambda n, b : self.checkData(index, w, w.expression()))
+    def rowCount(self, parent = QModelIndex()):
+        return len(self.mPlotSettings)
 
-                elif cname == model.cnStyle:
-                    w = TemporalProfile3DPlotStyleButton(parent=parent)
-                    w.setPlotStyle(plotStyle)
-                    w.setToolTip('Set plot style')
-                    w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle()))
 
-                elif cname == model.cnSensor:
-                    w = QComboBox(parent=parent)
-                    m = SensorListModel(self.mTimeSeries)
-                    w.setModel(m)
+    def removeRows(self, row, count , parent = QModelIndex()):
 
-                elif cname == model.cnTemporalProfile:
-                    w = QgsFeatureListComboBox(parent=parent)
-                    w.setSourceLayer(self.mTemporalProfileLayer)
-                    w.setIdentifierField('id')
-                    w.setDisplayExpression('to_string("id")+\'  \'+"name"')
-                    w.setAllowNull(False)
-                else:
-                    raise NotImplementedError()
-        return w
+        self.beginRemoveRows(parent, row, row + count-1)
 
-    def exampleLyr(self, sensor):
+        toRemove = self.mPlotSettings[row:row + count]
 
-        if sensor not in self.mSensorLayers.keys():
-            crs = QgsCoordinateReferenceSystem('EPSG:4862')
-            uri = 'Point?crs={}'.format(crs.authid())
-            lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory')
-            assert sensor is None or isinstance(sensor, SensorInstrument)
+        for tsd in toRemove:
+            self.mPlotSettings.remove(tsd)
 
-            f = sensorExampleQgsFeature(sensor, singleBandOnly=True)
-            assert isinstance(f, QgsFeature)
-            assert lyr.startEditing()
-            for field in f.fields():
-                lyr.addAttribute(field)
-            lyr.addFeature(f)
-            lyr.commitChanges()
-            self.mSensorLayers[sensor] = lyr
-        return self.mSensorLayers[sensor]
+        self.endRemoveRows()
 
-    def checkData(self, index, w, value):
-        assert isinstance(index, QModelIndex)
-        model = self.mTableView.model()
-        if index.isValid() and isinstance(model, PlotSettingsModel3D):
-            plotStyle = model.idx2plotStyle(index)
-            assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
-            if isinstance(w, QgsFieldExpressionWidget):
-                assert value == w.expression()
-                assert w.isExpressionValid(value) == w.isValidExpression()
+    def plotStyle2idx(self, plotStyle):
 
-                if w.isValidExpression():
-                    self.commitData.emit(w)
-                else:
-                    s = ""
-                    #print(('Delegate commit failed',w.asExpression()))
-            if isinstance(w, TemporalProfile3DPlotStyleButton):
+        assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
 
-                self.commitData.emit(w)
+        if plotStyle in self.mPlotSettings:
+            i = self.mPlotSettings.index(plotStyle)
+            return self.createIndex(i, 0)
+        else:
+            return QModelIndex()
 
+    def idx2plotStyle(self, index):
 
-    def setEditorData(self, editor, index):
-        cname = self.getColumnName(index)
-        model = self.mTableView.model()
+        if index.isValid() and index.row() < self.rowCount():
+            return self.mPlotSettings[index.row()]
 
-        w = None
-        if index.isValid() and isinstance(model, PlotSettingsModel3D):
-            cname = self.getColumnName(index)
-            style = model.idx2plotStyle(index)
+        return None
 
-            if cname == model.cnExpression:
-                lastExpr = index.model().data(index, Qt.DisplayRole)
-                assert isinstance(editor, QgsFieldExpressionWidget)
-                editor.setProperty('lastexpr', lastExpr)
-                editor.setField(lastExpr)
+    def columnCount(self, parent = QModelIndex()):
+        return len(self.columnNames)
 
-            elif cname == model.cnStyle:
-                assert isinstance(editor, TemporalProfile3DPlotStyleButton)
-                editor.setPlotStyle(style)
+    def data(self, index, role = Qt.DisplayRole):
+        if role is None or not index.isValid():
+            return None
 
-            elif cname == model.cnSensor:
-                assert isinstance(editor, QComboBox)
-                m = editor.model()
-                assert isinstance(m, SensorListModel)
-                sensor = index.data(role=Qt.UserRole)
-                if isinstance(sensor, SensorInstrument):
-                    idx = m.sensor2idx(sensor)
-                    editor.setCurrentIndex(idx.row())
-            elif cname == model.cnTemporalProfile:
-                assert isinstance(editor, QgsFeatureListComboBox)
-                value = editor.identifierValue()
-                if value != QVariant():
-                    plotStyle = index.data(role=Qt.UserRole)
-                    TP = plotStyle.temporalProfile()
-                    editor.setIdentifierValue(TP.id())
-                else:
-                    s  = ""
+        value = None
+        columnName = self.columnNames[index.column()]
+        plotStyle = self.idx2plotStyle(index)
+        if isinstance(plotStyle, TemporalProfile3DPlotStyle):
+            sensor = plotStyle.sensor()
+            #print(('data', columnName, role))
+            if role == Qt.DisplayRole:
+                if columnName == self.cnSensor:
+                    if isinstance(sensor, SensorInstrument):
+                        value = sensor.name()
+                    else:
+                        value = '<Select Sensor>'
+                elif columnName == self.cnExpression:
+                    value = plotStyle.expression()
+                elif columnName == self.cnTemporalProfile:
+                    tp = plotStyle.temporalProfile()
+                    if isinstance(tp, TemporalProfile):
+                        value = tp.name()
+                    else:
+                        value = 'undefined'
 
-            else:
-                raise NotImplementedError()
+            elif role == Qt.EditRole:
+                if columnName == self.cnExpression:
+                    value = plotStyle.expression()
 
-    def setModelData(self, w, model, index):
-        cname = self.getColumnName(index)
-        model = self.mTableView.model()
+            elif role == Qt.CheckStateRole:
+                if columnName == self.cnTemporalProfile:
+                    value = Qt.Checked if plotStyle.isVisible() else Qt.Unchecked
 
-        if index.isValid() and isinstance(model, PlotSettingsModel3D):
-            if cname == model.cnExpression:
-                assert isinstance(w, QgsFieldExpressionWidget)
-                expr = w.asExpression()
-                exprLast = model.data(index, Qt.DisplayRole)
+            elif role == Qt.UserRole:
+                value = plotStyle
+                if columnName == self.cnSensor:
+                    value = plotStyle.sensor()
+                elif columnName == self.cnStyle:
+                    value = plotStyle
+                else:
+                    value = plotStyle
+        #print(('get data',value))
+        return value
 
-                if w.isValidExpression() and expr != exprLast:
-                    model.setData(index, w.asExpression(), Qt.EditRole)
+    def setData(self, index, value, role=None):
+        if role is None or not index.isValid():
+            return False
+        #print(('Set data', index.row(), index.column(), value, role))
+        columnName = self.columnNames[index.column()]
 
-            elif cname == model.cnStyle:
-                assert isinstance(w, TemporalProfile3DPlotStyleButton)
-                model.setData(index, w.plotStyle(), Qt.EditRole)
+        if value is None:
+            return False
 
-            elif cname == model.cnSensor:
-                assert isinstance(w, QComboBox)
-                sensor = w.itemData(w.currentIndex(), role=Qt.UserRole)
-                assert isinstance(sensor, SensorInstrument)
-                model.setData(index, sensor, Qt.EditRole)
+        result = False
+        plotStyle = self.idx2plotStyle(index)
+        if isinstance(plotStyle, TemporalProfile3DPlotStyle):
+            """
+            if role in [Qt.DisplayRole]:
+                if columnName == self.cnExpression and isinstance(value, str):
+                    plotStyle.setExpression(value)
+                    result = True
+                elif columnName == self.cnStyle:
+                    if isinstance(value, PlotStyle):
+                        plotStyle.copyFrom(value)
+                        result = True
+            """
 
-                s = ""
-
-            elif cname == model.cnTemporalProfile:
-                assert isinstance(w, QgsFeatureListComboBox)
-                fid = w.identifierValue()
-                if isinstance(fid, int):
-                    TP = self.mTemporalProfileLayer.mProfiles.get(fid)
-                    model.setData(index, TP, Qt.EditRole)
-
-            else:
-                raise NotImplementedError()
-
-
-
-class SensorPixelDataMemoryLayer(QgsVectorLayer):
-
-    def __init__(self, sensor, crs=None):
-        assert isinstance(sensor, SensorInstrument)
-        if crs is None:
-            crs = QgsCoordinateReferenceSystem('EPSG:4862')
-        uri = 'Point?crs={}'.format(crs.authid())
-        super(SensorPixelDataMemoryLayer, self).__init__(uri, 'Pixels_sensor_' + sensor.name(), 'memory', False)
-        self.mSensor = sensor
+            if role == Qt.CheckStateRole:
+                if columnName == self.cnTemporalProfile:
+                    plotStyle.setVisibility(value == Qt.Checked)
+                    result = True
 
-        #initialize fields
-        assert self.startEditing()
-        # standard field names, types, etc.
-        fieldDefs = [('pxid', QVariant.String, 'integer'),
-                     ('date', QVariant.String, 'char'),
-                     ('doy', QVariant.Int, 'integer'),
-                     ('geo_x', QVariant.Double, 'decimal'),
-                     ('geo_y', QVariant.Double, 'decimal'),
-                     ('px_x', QVariant.Int, 'integer'),
-                     ('px_y', QVariant.Int, 'integer'),
-                     ]
-        # one field for each band
-        for b in range(sensor.nb):
-            fName = 'b{}'.format(b + 1)
-            fieldDefs.append((fName, QVariant.Double, 'decimal'))
+            if role == Qt.EditRole:
+                if columnName == self.cnSensor:
+                    plotStyle.setSensor(value)
+                    result = True
+                elif columnName == self.cnExpression:
+                    plotStyle.setExpression(value)
+                    result = True
+                elif columnName == self.cnTemporalProfile:
+                    plotStyle.setTemporalProfile(value)
+                    result = True
+                elif columnName == self.cnStyle:
+                    #set the style and trigger an update
+                    lastItemType = plotStyle.itemType()
+                    lastExpression = plotStyle.expression()
+                    plotStyle.copyFrom(value)
 
-        # initialize fields
-        for fieldDef in fieldDefs:
-            field = QgsField(fieldDef[0], fieldDef[1], fieldDef[2])
-            self.addAttribute(field)
-        self.commitChanges()
+                    if lastItemType != plotStyle.itemType() or \
+                        lastExpression != plotStyle.expression():
+                        plotStyle.updateDataProperties()
+                    else:
+                        plotStyle.updateStyleProperties()
 
-    def sensor(self):
-        return self.mSensor
+                    result = True
 
-    def nPixels(self):
-        raise NotImplementedError()
+        return result
 
-    def dates(self):
-        raise NotImplementedError()
 
+    def flags(self, index):
+        if index.isValid():
+            stype = self.idx2plotStyle(index)
+            columnName = self.columnNames[index.column()]
+            flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
+            if columnName in [self.cnTemporalProfile]:
+                flags = flags | Qt.ItemIsUserCheckable
+            if columnName in [self.cnTemporalProfile, self.cnSensor, self.cnExpression, self.cnStyle]: #allow check state
+                flags = flags | Qt.ItemIsEditable
+            return flags
+            #return item.qt_flags(index.column())
+        return Qt.NoItemFlags
 
+    def headerData(self, col, orientation, role):
+        if Qt is None:
+            return None
+        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
+            return self.columnNames[col]
+        elif orientation == Qt.Vertical and role == Qt.DisplayRole:
+            return col
+        return None
 
 
-class PlotSettingsModel3D(QAbstractTableModel):
+class PlotSettingsModel2D(QAbstractTableModel):
 
-    #sigSensorAdded = pyqtSignal(SensorPlotSettings)
+    # sigSensorAdded = pyqtSignal(SensorPlotSettings)
     sigVisibilityChanged = pyqtSignal(TemporalProfile2DPlotStyle)
+    sigDataChanged = pyqtSignal(TemporalProfile2DPlotStyle)
     sigPlotStylesAdded = pyqtSignal(list)
     sigPlotStylesRemoved = pyqtSignal(list)
 
+
     def __init__(self, parent=None, *args):
 
-        #assert isinstance(tableView, QTableView)
 
-        super(PlotSettingsModel3D, self).__init__(parent=parent)
-        self.mTimeSeries = None
+        super(PlotSettingsModel2D, self).__init__(parent=parent)
+
         self.cnID = 'ID'
-        self.cnExpression = LABEL_EXPRESSION_3D
-        self.cnTemporalProfile = 'Coordinate'
-        self.cnStyle = 'Style'
         self.cnSensor = 'Sensor'
+        self.cnExpression = LABEL_EXPRESSION_2D
+        self.cnStyle = 'Style'
+        self.cnTemporalProfile = 'Coordinate'
         self.columnNames = [self.cnTemporalProfile, self.cnSensor, self.cnStyle, self.cnExpression]
-        self.mPlotSettings = []
-        #assert isinstance(plotWidget, DateTimePlotWidget)
-
-        self.sortColumnIndex = 0
-        self.sortOrder = Qt.AscendingOrder
-
-
-        self.sort(0, Qt.AscendingOrder)
-
-    def hasStyleForSensor(self, sensor):
-        assert isinstance(sensor, SensorInstrument)
-        for plotStyle in self.mPlotSettings:
-            assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
-            if plotStyle.sensor() == sensor:
-                return True
-        return False
-
-
-
-    def onSensorRemoved(self, sensor):
-        assert isinstance(sensor, SensorInstrument)
-        self.removePlotStyles([s for s in self.mPlotSettings if s.sensor() == sensor])
 
+        self.mPlotSettings = []
 
+        self.dataChanged.connect(self.signaler)
 
     def __len__(self):
         return len(self.mPlotSettings)
@@ -634,9 +499,44 @@ class PlotSettingsModel3D(QAbstractTableModel):
         return item in self.mPlotSettings
 
 
+    def testSlot(self, *args):
+        print(('TESTSLOT', args))
+
     def columnIndex(self, name):
         return self.columnNames.index(name)
 
+    def signaler(self, idxUL, idxLR):
+        if idxUL.isValid():
+
+            plotStyle = self.idx2plotStyle(idxUL)
+            cname = self.columnNames[idxUL.column()]
+            if cname in [self.cnSensor,self.cnStyle]:
+                self.sigVisibilityChanged.emit(plotStyle)
+            if cname in [self.cnExpression]:
+                self.sigDataChanged.emit(plotStyle)
+
+
+
+
+    def requiredBandsIndices(self, sensor):
+        """
+        Returns the band indices required to calculate the values for
+        the different PlotStyle expressions making use of sensor
+        :param sensor: SensorInstrument for which the band indices are to be returned.
+        :return: [list-of-band-indices]
+        """
+        bandIndices = set()
+        assert isinstance(sensor, SensorInstrument)
+        for p in [p for p in self.mPlotSettings if p.sensor() == sensor]:
+            assert isinstance(p, TemporalProfile2DPlotStyle)
+            expression = p.expression()
+            #remove leading & tailing "
+            bandKeys = regBandKey.findall(expression)
+            for bandIndex in [bandKey2bandIndex(key) for key in bandKeys]:
+                bandIndices.add(bandIndex)
+
+        return bandIndices
+
 
     def insertPlotStyles(self, plotStyles, i=None):
         """
@@ -644,11 +544,11 @@ class PlotSettingsModel3D(QAbstractTableModel):
         :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle]
         :param i: index to insert, defaults to the last list position
         """
-        if isinstance(plotStyles, TemporalProfile3DPlotStyle):
+        if isinstance(plotStyles, TemporalProfile2DPlotStyle):
             plotStyles = [plotStyles]
         assert isinstance(plotStyles, list)
         for plotStyle in plotStyles:
-            assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
+            assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
 
         if i is None:
             i = len(self.mPlotSettings)
@@ -656,23 +556,33 @@ class PlotSettingsModel3D(QAbstractTableModel):
         if len(plotStyles) > 0:
             self.beginInsertRows(QModelIndex(), i, i + len(plotStyles)-1)
             for j, plotStyle in enumerate(plotStyles):
-                assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
+                assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
+                plotStyle.sigExpressionUpdated.connect(lambda s = plotStyle: self.onStyleUpdated(s))
                 self.mPlotSettings.insert(i+j, plotStyle)
             self.endInsertRows()
             self.sigPlotStylesAdded.emit(plotStyles)
 
+    def onStyleUpdated(self, style):
+
+        idx = self.plotStyle2idx(style)
+        r = idx.row()
+        self.dataChanged.emit(self.createIndex(r, 0), self.createIndex(r, self.columnCount()))
+
+        s = ""
+
+
     def removePlotStyles(self, plotStyles):
         """
         Removes PlotStyle instances
         :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle]
         """
-        if isinstance(plotStyles, TemporalProfile3DPlotStyle):
+        if isinstance(plotStyles, PlotStyle):
             plotStyles = [plotStyles]
         assert isinstance(plotStyles, list)
 
         if len(plotStyles) > 0:
             for plotStyle in plotStyles:
-                assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
+                assert isinstance(plotStyle, PlotStyle)
                 if plotStyle in self.mPlotSettings:
                     idx = self.plotStyle2idx(plotStyle)
                     self.beginRemoveRows(QModelIndex(), idx.row(),idx.row())
@@ -680,19 +590,6 @@ class PlotSettingsModel3D(QAbstractTableModel):
                     self.endRemoveRows()
             self.sigPlotStylesRemoved.emit(plotStyles)
 
-    def sort(self, col, order):
-        if self.rowCount() == 0:
-            return
-
-
-        colName = self.columnames[col]
-        r = order != Qt.AscendingOrder
-
-        #self.beginMoveRows(idxSrc,
-
-        if colName == self.cnSensor:
-            self.mPlotSettings.sort(key = lambda sv:sv.sensor().name(), reverse=r)
-
     def rowCount(self, parent = QModelIndex()):
         return len(self.mPlotSettings)
 
@@ -710,7 +607,7 @@ class PlotSettingsModel3D(QAbstractTableModel):
 
     def plotStyle2idx(self, plotStyle):
 
-        assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
+        assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
 
         if plotStyle in self.mPlotSettings:
             i = self.mPlotSettings.index(plotStyle)
@@ -735,7 +632,7 @@ class PlotSettingsModel3D(QAbstractTableModel):
         value = None
         columnName = self.columnNames[index.column()]
         plotStyle = self.idx2plotStyle(index)
-        if isinstance(plotStyle, TemporalProfile3DPlotStyle):
+        if isinstance(plotStyle, TemporalProfile2DPlotStyle):
             sensor = plotStyle.sensor()
             #print(('data', columnName, role))
             if role == Qt.DisplayRole:
@@ -753,20 +650,26 @@ class PlotSettingsModel3D(QAbstractTableModel):
                     else:
                         value = 'undefined'
 
-            elif role == Qt.EditRole:
-                if columnName == self.cnExpression:
-                    value = plotStyle.expression()
+            if role == Qt.DecorationRole:
+                if columnName == self.cnStyle:
+                    value = plotStyle.createIcon(QSize(25,25))
 
             elif role == Qt.CheckStateRole:
                 if columnName == self.cnTemporalProfile:
                     value = Qt.Checked if plotStyle.isVisible() else Qt.Unchecked
 
+
+
             elif role == Qt.UserRole:
                 value = plotStyle
                 if columnName == self.cnSensor:
                     value = plotStyle.sensor()
+                elif columnName == self.cnExpression:
+                    value = plotStyle.expression()
                 elif columnName == self.cnStyle:
                     value = plotStyle
+                elif columnName == self.cnTemporalProfile:
+                    value == plotStyle.temporalProfile()
                 else:
                     value = plotStyle
         #print(('get data',value))
@@ -778,22 +681,20 @@ class PlotSettingsModel3D(QAbstractTableModel):
         #print(('Set data', index.row(), index.column(), value, role))
         columnName = self.columnNames[index.column()]
 
-        if value is None:
-            return False
 
         result = False
         plotStyle = self.idx2plotStyle(index)
-        if isinstance(plotStyle, TemporalProfile3DPlotStyle):
-            """
+        if isinstance(plotStyle, TemporalProfile2DPlotStyle):
             if role in [Qt.DisplayRole]:
-                if columnName == self.cnExpression and isinstance(value, str):
+                if columnName == self.cnExpression:
                     plotStyle.setExpression(value)
+                    plotStyle.updateDataProperties()
                     result = True
                 elif columnName == self.cnStyle:
                     if isinstance(value, PlotStyle):
                         plotStyle.copyFrom(value)
+                        plotStyle.updateStyleProperties()
                         result = True
-            """
 
             if role == Qt.CheckStateRole:
                 if columnName == self.cnTemporalProfile:
@@ -801,35 +702,36 @@ class PlotSettingsModel3D(QAbstractTableModel):
                     result = True
 
             if role == Qt.EditRole:
-                if columnName == self.cnSensor:
-                    plotStyle.setSensor(value)
-                    result = True
-                elif columnName == self.cnExpression:
+                if columnName == self.cnExpression:
                     plotStyle.setExpression(value)
+
+                    result = True
+                elif columnName == self.cnStyle:
+                    plotStyle.copyFrom(value)
+                    result = True
+                elif columnName == self.cnSensor:
+                    plotStyle.setSensor(value)
                     result = True
                 elif columnName == self.cnTemporalProfile:
                     plotStyle.setTemporalProfile(value)
                     result = True
-                elif columnName == self.cnStyle:
-                    #set the style and trigger an update
-                    lastItemType = plotStyle.itemType()
-                    lastExpression = plotStyle.expression()
-                    plotStyle.copyFrom(value)
-
-                    if lastItemType != plotStyle.itemType() or \
-                        lastExpression != plotStyle.expression():
-                        plotStyle.updateDataProperties()
-                    else:
-                        plotStyle.updateStyleProperties()
 
-                    result = True
+        if result:
+            #save plot-style
+            self.savePlotSettings(plotStyle, index='DEFAULT')
+            self.dataChanged.emit(index, index)
 
         return result
 
+    def savePlotSettings(self, sensorPlotSettings, index='DEFAULT'):
+        return
+
+    def restorePlotSettings(self, sensor, index='DEFAULT'):
+        return None
+
 
     def flags(self, index):
         if index.isValid():
-            stype = self.idx2plotStyle(index)
             columnName = self.columnNames[index.column()]
             flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
             if columnName in [self.cnTemporalProfile]:
@@ -850,1092 +752,1179 @@ class PlotSettingsModel3D(QAbstractTableModel):
         return None
 
 
-class PlotSettingsModel2D(QAbstractTableModel):
 
-    # sigSensorAdded = pyqtSignal(SensorPlotSettings)
-    sigVisibilityChanged = pyqtSignal(TemporalProfile2DPlotStyle)
-    sigDataChanged = pyqtSignal(TemporalProfile2DPlotStyle)
-    sigPlotStylesAdded = pyqtSignal(list)
-    sigPlotStylesRemoved = pyqtSignal(list)
+class ProfileViewDockUI(QgsDockWidget, loadUI('profileviewdock.ui')):
 
 
-    def __init__(self, parent=None, *args):
+    def __init__(self, parent=None):
+        super(ProfileViewDockUI, self).__init__(parent)
+        self.setupUi(self)
 
+        self.addActions(self.findChildren(QAction))
 
-        super(PlotSettingsModel2D, self).__init__(parent=parent)
+        self.mActions2D = [self.actionAddStyle2D, self.actionRemoveStyle2D, self.actionRefresh2D, self.actionReset2DPlot]
+        self.mActions3D = [self.actionAddStyle3D, self.actionRemoveStyle3D, self.actionRefresh3D,
+                           self.actionReset3DCamera]
+        self.mActionsTP = [self.actionLoadTPFromOgr, self.actionSaveTemporalProfiles, self.actionToggleEditing,
+                           self.actionRemoveTemporalProfile, self.actionLoadMissingValues]
+        #TBD.
+        #self.line.setVisible(False)
+        #self.listWidget.setVisible(False)
+        self.baseTitle = self.windowTitle()
+        self.stackedWidget.currentChanged.connect(self.onStackPageChanged)
 
-        self.cnID = 'ID'
-        self.cnSensor = 'Sensor'
-        self.cnExpression = LABEL_EXPRESSION_2D
-        self.cnStyle = 'Style'
-        self.cnTemporalProfile = 'Coordinate'
-        self.columnNames = [self.cnTemporalProfile, self.cnSensor, self.cnStyle, self.cnExpression]
 
-        self.mPlotSettings = []
-        #assert isinstance(plotWidget, DateTimePlotWidget)
-        #self.mPlotWidget = plotWidget
-        self.sortColumnIndex = 0
-        self.sortOrder = Qt.AscendingOrder
-        #assert isinstance(self.tpCollection.TS, TimeSeries)
-        #self.tpCollection.TS.sigSensorAdded.connect(self.addPlotItem)
-        #self.tpCollection.TS.sigSensorRemoved.connect(self.removeSensor)
+        self.plotWidget3D = None
+        self.plotWidget3DMPL = None
 
-        self.sort(0, Qt.AscendingOrder)
-        self.dataChanged.connect(self.signaler)
+        self.init3DWidgets('gl')
 
-    def __len__(self):
-        return len(self.mPlotSettings)
 
-    def __iter__(self):
-        return iter(self.mPlotSettings)
+        #pi = self.plotWidget2D.plotItem
+        #ax = DateAxis(orientation='bottom', showValues=True)
+        #pi.layout.addItem(ax, 3,2)
 
-    def __getitem__(self, slice):
-        return self.mPlotSettings[slice]
 
-    def __contains__(self, item):
-        return item in self.mPlotSettings
+        self.TS = None
 
+        self.progressBar.setMinimum(0)
+        self.progressBar.setMaximum(100)
+        self.progressBar.setValue(0)
+        self.progressInfo.setText('')
+        self.pxViewModel2D = None
+        self.pxViewModel3D = None
 
-    def testSlot(self, *args):
-        print(('TESTSLOT', args))
+        self.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
 
-    def columnIndex(self, name):
-        return self.columnNames.index(name)
+    def init3DWidgets(self, mode='gl'):
+        assert mode in ['gl']
+        l = self.frame3DPlot.layout()
 
-    def signaler(self, idxUL, idxLR):
-        if idxUL.isValid():
+        if ENABLE_OPENGL and OPENGL_AVAILABLE and mode == 'gl':
 
-            plotStyle = self.idx2plotStyle(idxUL)
-            cname = self.columnNames[idxUL.column()]
-            if cname in [self.cnSensor,self.cnStyle]:
-                self.sigVisibilityChanged.emit(plotStyle)
-            if cname in [self.cnExpression]:
-                self.sigDataChanged.emit(plotStyle)
+            from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D
+            self.plotWidget3D = ViewWidget3D(parent=self.labelDummy3D.parent())
+            self.plotWidget3D.setObjectName('plotWidget3D')
 
+            size = self.labelDummy3D.size()
+            l.addWidget(self.plotWidget3D)
+            self.plotWidget3D.setSizePolicy(self.labelDummy3D.sizePolicy())
+            self.labelDummy3D.setVisible(False)
+            l.removeWidget(self.labelDummy3D)
+            #self.plotWidget3D.setBaseSize(size)
+            self.splitter3D.setSizes([100, 100])
+            self.frameSettings3D.setEnabled(True)
+        else:
+            self.frameSettings3D.setEnabled(False)
 
+    def onStackPageChanged(self, i):
+        w = self.stackedWidget.currentWidget()
+        title = self.baseTitle
+        if w == self.page2D:
+            title = '{} | 2D'.format(title)
+            for a in self.mActions2D:
+                a.setVisible(True)
+            for a in self.mActions3D + self.mActionsTP:
+                a.setVisible(False)
+        elif w == self.page3D:
+            title = '{} | 3D (experimental!)'.format(title)
+            for a in self.mActions2D + self.mActionsTP:
+                a.setVisible(False)
+            for a in self.mActions3D:
+                a.setVisible(True)
+        elif w == self.pagePixel:
+            title = '{} | Coordinates'.format(title)
+            for a in self.mActions2D + self.mActions3D:
+                a.setVisible(False)
+            for a in self.mActionsTP:
+                a.setVisible(True)
 
+        w.update()
+        self.setWindowTitle(title)
 
-    def requiredBandsIndices(self, sensor):
-        """
-        Returns the band indices required to calculate the values for
-        the different PlotStyle expressions making use of sensor
-        :param sensor: SensorInstrument for which the band indices are to be returned.
-        :return: [list-of-band-indices]
-        """
-        bandIndices = set()
-        assert isinstance(sensor, SensorInstrument)
-        for p in [p for p in self.mPlotSettings if p.sensor() == sensor]:
-            assert isinstance(p, TemporalProfile2DPlotStyle)
-            expression = p.expression()
-            #remove leading & tailing "
-            bandKeys = regBandKey.findall(expression)
-            for bandIndex in [bandKey2bandIndex(key) for key in bandKeys]:
-                bandIndices.add(bandIndex)
 
-        return bandIndices
+class SpectralTemporalVisualization(QObject):
 
+    sigShowPixel = pyqtSignal(TimeSeriesDatum, QgsPoint, QgsCoordinateReferenceSystem)
 
-    def insertPlotStyles(self, plotStyles, i=None):
-        """
-        Inserts PlotStyle
-        :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle]
-        :param i: index to insert, defaults to the last list position
-        """
-        if isinstance(plotStyles, TemporalProfile2DPlotStyle):
-            plotStyles = [plotStyles]
-        assert isinstance(plotStyles, list)
-        for plotStyle in plotStyles:
-            assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
+    """
+    Signalizes to move to specific date of interest
+    """
+    sigMoveToDate = pyqtSignal(np.datetime64)
 
-        if i is None:
-            i = len(self.mPlotSettings)
 
-        if len(plotStyles) > 0:
-            self.beginInsertRows(QModelIndex(), i, i + len(plotStyles)-1)
-            for j, plotStyle in enumerate(plotStyles):
-                assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
-                plotStyle.sigExpressionUpdated.connect(lambda s = plotStyle: self.onStyleUpdated(s))
-                self.mPlotSettings.insert(i+j, plotStyle)
-            self.endInsertRows()
-            self.sigPlotStylesAdded.emit(plotStyles)
+    def __init__(self, timeSeries, profileDock):
+        super(SpectralTemporalVisualization, self).__init__()
 
-    def onStyleUpdated(self, style):
+        assert isinstance(profileDock, ProfileViewDockUI)
+        self.ui = profileDock
 
-        idx = self.plotStyle2idx(style)
-        r = idx.row()
-        self.dataChanged.emit(self.createIndex(r, 0), self.createIndex(r, self.columnCount()))
+        import eotimeseriesviewer.pixelloader
+        if DEBUG:
+            eotimeseriesviewer.pixelloader.DEBUG = True
 
-        s = ""
+        #the timeseries. will be set later
+        assert isinstance(timeSeries, TimeSeries)
+        self.TS = timeSeries
+        self.plot_initialized = False
 
+        self.plot2D = self.ui.plotWidget2D
+        self.plot2D.getViewBox().sigMoveToDate.connect(self.sigMoveToDate)
+        self.plot3D = self.ui.plotWidget3D
 
-    def removePlotStyles(self, plotStyles):
-        """
-        Removes PlotStyle instances
-        :param plotStyles: TemporalProfilePlotStyle | [list-of-TemporalProfilePlotStyle]
-        """
-        if isinstance(plotStyles, PlotStyle):
-            plotStyles = [plotStyles]
-        assert isinstance(plotStyles, list)
+        # temporal profile collection to store loaded values
+        self.mTemporalProfileLayer = TemporalProfileLayer(self.TS)
+        self.mTemporalProfileLayer.sigTemporalProfilesAdded.connect(self.onTemporalProfilesAdded)
+        #self.mTemporalProfileLayer.startEditing()
+        self.mTemporalProfileLayer.selectionChanged.connect(self.onTemporalProfileSelectionChanged)
 
-        if len(plotStyles) > 0:
-            for plotStyle in plotStyles:
-                assert isinstance(plotStyle, PlotStyle)
-                if plotStyle in self.mPlotSettings:
-                    idx = self.plotStyle2idx(plotStyle)
-                    self.beginRemoveRows(QModelIndex(), idx.row(),idx.row())
-                    self.mPlotSettings.remove(plotStyle)
-                    self.endRemoveRows()
-            self.sigPlotStylesRemoved.emit(plotStyles)
+        # fix to not loose C++ reference on temporal profile layer in case it is removed from QGIS mapcanvas
+        self.mMapCanvas = QgsMapCanvas()
+        self.mMapCanvas.setVisible(False)
+        self.mMapCanvas.setLayers([self.mTemporalProfileLayer])
+        # self.tpCollectionListModel = TemporalProfileCollectionListModel(self.tpCollection)
 
-    def sort(self, col, order):
-        if self.rowCount() == 0:
-            return
+        assert isinstance(self.ui.mDualView, QgsDualView)
+        self.ui.mDualView.init(self.mTemporalProfileLayer, self.mMapCanvas)
+        self.ui.mDualView.setView(QgsDualView.AttributeTable)
+        # pixel loader to load pixel values in parallel
+        config = QgsAttributeTableConfig()
+        config.update(self.mTemporalProfileLayer.fields())
+        config.setActionWidgetVisible(True)
 
+        hidden = [FN_ID]
+        for i, columnConfig in enumerate(config.columns()):
 
-        colName = self.columnames[col]
-        r = order != Qt.AscendingOrder
+            assert isinstance(columnConfig, QgsAttributeTableConfig.ColumnConfig)
+            config.setColumnHidden(i, columnConfig.name in hidden)
 
-        #self.beginMoveRows(idxSrc,
 
-        if colName == self.cnSensor:
-            self.mPlotSettings.sort(key = lambda sv:sv.sensor().name(), reverse=r)
-        elif colName == self.cnExpression:
-            self.mPlotSettings.sort(key=lambda sv: sv.expression(), reverse=r)
-        elif colName == self.cnStyle:
-            self.mPlotSettings.sort(key=lambda sv: sv.color, reverse=r)
+        self.mTemporalProfilesTableConfig = config
+        self.mTemporalProfileLayer.setAttributeTableConfig(self.mTemporalProfilesTableConfig)
 
-    def rowCount(self, parent = QModelIndex()):
-        return len(self.mPlotSettings)
+        self.pixelLoader = PixelLoader()
+        self.pixelLoader.sigPixelLoaded.connect(self.onPixelLoaded)
+        self.pixelLoader.sigLoadingStarted.connect(lambda: self.ui.progressInfo.setText('Start loading...'))
+        # self.pixelLoader.sigLoadingStarted.connect(self.tpCollection.prune)
+        self.pixelLoader.sigLoadingFinished.connect(lambda: self.plot2D.enableAutoRange('x', False))
 
+        # set the plot models for 2D
+        self.plotSettingsModel2D = PlotSettingsModel2D()
+        self.plotSettingsModel2DProxy = QSortFilterProxyModel()
+        self.plotSettingsModel2DProxy.setSourceModel(self.plotSettingsModel2D)
+        self.ui.tableView2DProfiles.setModel(self.plotSettingsModel2DProxy)
+        self.ui.tableView2DProfiles.setSortingEnabled(True)
+        self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot2DSelectionChanged)
+        self.ui.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
+        self.plotSettingsModel2D.sigDataChanged.connect(self.requestUpdate)
+        self.plotSettingsModel2D.rowsInserted.connect(self.onRowsInserted2D)
+        self.delegateTableView2D = PlotSettingsModel2DWidgetDelegate(self.ui.tableView2DProfiles, self.mTemporalProfileLayer)
+        self.delegateTableView2D.setItemDelegates(self.ui.tableView2DProfiles)
 
-    def removeRows(self, row, count , parent = QModelIndex()):
+        # set the plot models for 3D
+        self.plotSettingsModel3D = PlotSettingsModel3D()
+        self.ui.tableView3DProfiles.setModel(self.plotSettingsModel3D)
+        self.ui.tableView3DProfiles.setSortingEnabled(False)
+        self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot3DSelectionChanged)
+        self.ui.tableView3DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
+        self.plotSettingsModel3D.rowsInserted.connect(self.onRowsInserted3D)
+        self.delegateTableView3D = PlotSettingsModel3DWidgetDelegate(self.ui.tableView3DProfiles, self.mTemporalProfileLayer)
+        self.delegateTableView3D.setItemDelegates(self.ui.tableView3DProfiles)
 
-        self.beginRemoveRows(parent, row, row + count-1)
+        if not ENABLE_OPENGL:
+            self.ui.listWidget.item(1).setHidden(True)
+            self.ui.page3D.setHidden(True)
 
-        toRemove = self.mPlotSettings[row:row + count]
 
-        for tsd in toRemove:
-            self.mPlotSettings.remove(tsd)
 
-        self.endRemoveRows()
+        def onTemporalProfilesRemoved(removedProfiles):
+            #set to valid temporal profile
 
-    def plotStyle2idx(self, plotStyle):
+            affectedStyles2D = [p for p in self.plotSettingsModel2D if p.temporalProfile() in removedProfiles]
+            affectedStyles3D = [p for p in self.plotSettingsModel3D if p.temporalProfile() in removedProfiles]
+            alternativeProfile = self.mTemporalProfileLayer[-1] if len(self.mTemporalProfileLayer) > 0 else None
+            for s in affectedStyles2D:
+                assert isinstance(s, TemporalProfile2DPlotStyle)
+                m = self.plotSettingsModel2D
+                idx = m.plotStyle2idx(s)
+                idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile))
+                m.setData(idx, alternativeProfile, Qt.EditRole)
 
-        assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
+            for s in affectedStyles3D:
+                assert isinstance(s, TemporalProfile3DPlotStyle)
+                m = self.plotSettingsModel3D
+                idx = m.plotStyle2idx(s)
+                idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile))
+                m.setData(idx, alternativeProfile, Qt.EditRole)
 
-        if plotStyle in self.mPlotSettings:
-            i = self.mPlotSettings.index(plotStyle)
-            return self.createIndex(i, 0)
-        else:
-            return QModelIndex()
+        self.mTemporalProfileLayer.sigTemporalProfilesRemoved.connect(onTemporalProfilesRemoved)
 
-    def idx2plotStyle(self, index):
+        # remove / add plot style
+        def on2DPlotStyleRemoved(plotStyles):
+            for plotStyle in plotStyles:
+                assert isinstance(plotStyle, PlotStyle)
+                for pi in plotStyle.mPlotItems:
+                    self.plot2D.plotItem.removeItem(pi)
 
-        if index.isValid() and index.row() < self.rowCount():
-            return self.mPlotSettings[index.row()]
+        def on3DPlotStyleRemoved(plotStyles):
+            toRemove = []
+            for plotStyle in plotStyles:
+                assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
+                toRemove.append(plotStyle.mPlotItems)
+            self.plot3D.removeItems(toRemove)
 
-        return None
 
-    def columnCount(self, parent = QModelIndex()):
-        return len(self.columnNames)
+        self.plotSettingsModel2D.sigPlotStylesRemoved.connect(on2DPlotStyleRemoved)
+        self.plotSettingsModel3D.sigPlotStylesRemoved.connect(on3DPlotStyleRemoved)
 
-    def data(self, index, role = Qt.DisplayRole):
-        if role is None or not index.isValid():
-            return None
+        #initialize the update loop
+        self.updateRequested = True
+        self.updateTimer = QTimer(self)
+        self.updateTimer.timeout.connect(self.onDataUpdate)
+        self.updateTimer.start(2000)
 
-        value = None
-        columnName = self.columnNames[index.column()]
-        plotStyle = self.idx2plotStyle(index)
-        if isinstance(plotStyle, TemporalProfile2DPlotStyle):
-            sensor = plotStyle.sensor()
-            #print(('data', columnName, role))
-            if role == Qt.DisplayRole:
-                if columnName == self.cnSensor:
-                    if isinstance(sensor, SensorInstrument):
-                        value = sensor.name()
-                    else:
-                        value = '<Select Sensor>'
-                elif columnName == self.cnExpression:
-                    value = plotStyle.expression()
-                elif columnName == self.cnTemporalProfile:
-                    tp = plotStyle.temporalProfile()
-                    if isinstance(tp, TemporalProfile):
-                        value = tp.name()
-                    else:
-                        value = 'undefined'
+        self.sigMoveToDate.connect(self.onMoveToDate)
 
-            #elif role == Qt.DecorationRole:
-            #    if columnName == self.cnStyle:
-            #        value = plotStyle.createIcon(QSize(96,96))
+        self.initActions()
+        #self.ui.stackedWidget.setCurrentPage(self.ui.pagePixel)
+        self.ui.onStackPageChanged(self.ui.stackedWidget.currentIndex())
 
-            elif role == Qt.CheckStateRole:
-                if columnName == self.cnTemporalProfile:
-                    value = Qt.Checked if plotStyle.isVisible() else Qt.Unchecked
+    def temporalProfileLayer(self)->TemporalProfileLayer:
+        """
+        Returns a QgsVectorLayer that is used to store profile coordinates.
+        :return:
+        """
+        return self.mTemporalProfileLayer
 
+    def onTemporalProfilesContextMenu(self, event):
+        assert isinstance(event, QContextMenuEvent)
+        tableView = self.ui.tableViewTemporalProfiles
+        selectionModel = self.ui.tableViewTemporalProfiles.selectionModel()
+        assert isinstance(selectionModel, QItemSelectionModel)
 
+        model = self.ui.tableViewTemporalProfiles.model()
+        assert isinstance(model, TemporalProfileLayer)
 
-            elif role == Qt.UserRole:
-                value = plotStyle
-                if columnName == self.cnSensor:
-                    value = plotStyle.sensor()
-                elif columnName == self.cnExpression:
-                    value = plotStyle.expression()
-                elif columnName == self.cnStyle:
-                    value = plotStyle
-                elif columnName == self.cnTemporalProfile:
-                    value == plotStyle.temporalProfile()
-                else:
-                    value = plotStyle
-        #print(('get data',value))
-        return value
+        temporalProfiles = []
 
-    def setData(self, index, value, role=None):
-        if role is None or not index.isValid():
-            return False
-        #print(('Set data', index.row(), index.column(), value, role))
-        columnName = self.columnNames[index.column()]
+        if len(selectionModel.selectedIndexes()) > 0:
+            for idx in selectionModel.selectedIndexes():
+                tp = model.idx2tp(idx)
+                if isinstance(tp, TemporalProfile) and not tp in temporalProfiles:
+                    temporalProfiles.append(tp)
+        else:
+            temporalProfiles = model[:]
 
+        spatialPoints = [tp.coordinate() for tp in temporalProfiles]
 
-        result = False
-        plotStyle = self.idx2plotStyle(index)
-        if isinstance(plotStyle, TemporalProfile2DPlotStyle):
-            if role in [Qt.DisplayRole]:
-                if columnName == self.cnExpression:
-                    plotStyle.setExpression(value)
-                    plotStyle.updateDataProperties()
-                    result = True
-                elif columnName == self.cnStyle:
-                    if isinstance(value, PlotStyle):
-                        plotStyle.copyFrom(value)
-                        plotStyle.updateStyleProperties()
-                        result = True
 
-            if role == Qt.CheckStateRole:
-                if columnName == self.cnTemporalProfile:
-                    plotStyle.setVisibility(value == Qt.Checked)
-                    result = True
+        menu = QMenu()
 
-            if role == Qt.EditRole:
-                if columnName == self.cnExpression:
-                    plotStyle.setExpression(value)
+        a = menu.addAction('Load missing')
+        a.setToolTip('Loads missing band-pixels.')
+        a.triggered.connect(lambda : self.loadCoordinate(spatialPoints=spatialPoints, mode='all'))
+        s = ""
 
-                    result = True
-                elif columnName == self.cnStyle:
-                    plotStyle.copyFrom(value)
-                    result = True
-                elif columnName == self.cnSensor:
-                    plotStyle.setSensor(value)
-                    result = True
-                elif columnName == self.cnTemporalProfile:
-                    plotStyle.setTemporalProfile(value)
-                    result = True
+        a = menu.addAction('Reload')
+        a.setToolTip('Reloads all band-pixels.')
+        a.triggered.connect(lambda: self.loadCoordinate(spatialPoints=spatialPoints, mode='reload'))
 
-        if result:
-            #save plot-style
-            self.savePlotSettings(plotStyle, index='DEFAULT')
-            self.dataChanged.emit(index, index)
+        menu.popup(tableView.viewport().mapToGlobal(event.pos()))
+        self.menu = menu
 
-        return result
 
-    def savePlotSettings(self, sensorPlotSettings, index='DEFAULT'):
-        return
+    def selected2DPlotStyles(self):
+        result = []
 
-    def restorePlotSettings(self, sensor, index='DEFAULT'):
-        return None
+        m = self.ui.tableView2DProfiles.model()
+        for idx in selectedModelIndices(self.ui.tableView2DProfiles):
+            result.append(m.idx2plotStyle(idx))
+        return result
 
 
-    def flags(self, index):
-        if index.isValid():
-            columnName = self.columnNames[index.column()]
-            flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
-            if columnName in [self.cnTemporalProfile]:
-                flags = flags | Qt.ItemIsUserCheckable
-            if columnName in [self.cnTemporalProfile, self.cnSensor, self.cnExpression, self.cnStyle]: #allow check state
-                flags = flags | Qt.ItemIsEditable
-            return flags
-            #return item.qt_flags(index.column())
-        return Qt.NoItemFlags
 
-    def headerData(self, col, orientation, role):
-        if Qt is None:
-            return None
-        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
-            return self.columnNames[col]
-        elif orientation == Qt.Vertical and role == Qt.DisplayRole:
-            return col
-        return None
+    def removePlotStyles2D(self, plotStyles):
+        m = self.ui.tableView2DProfiles.model()
+        if isinstance(m, PlotSettingsModel2D):
+            m.removePlotStyles(plotStyles)
 
+    def removeTemporalProfiles(self, fids):
 
+        self.mTemporalProfileLayer.selectByIds(fids)
+        b = self.mTemporalProfileLayer.isEditable()
+        self.mTemporalProfileLayer.startEditing()
+        self.mTemporalProfileLayer.deleteSelectedFeatures()
+        self.mTemporalProfileLayer.saveEdits(leaveEditable=b)
 
-class ProfileViewDockUI(QgsDockWidget, loadUI('profileviewdock.ui')):
+    def createNewPlotStyle2D(self):
+        l = len(self.mTemporalProfileLayer)
 
 
-    def __init__(self, parent=None):
-        super(ProfileViewDockUI, self).__init__(parent)
-        self.setupUi(self)
+        plotStyle = TemporalProfile2DPlotStyle()
+        plotStyle.sigExpressionUpdated.connect(self.updatePlot2D)
 
-        self.addActions(self.findChildren(QAction))
+        sensors = self.TS.sensors()
+        if len(sensors) > 0:
+            plotStyle.setSensor(sensors[0])
 
-        self.mActions2D = [self.actionAddStyle2D, self.actionRemoveStyle2D, self.actionRefresh2D, self.actionReset2DPlot]
-        self.mActions3D = [self.actionAddStyle3D, self.actionRemoveStyle3D, self.actionRefresh3D,
-                           self.actionReset3DCamera]
-        self.mActionsTP = [self.actionLoadTPFromOgr, self.actionSaveTemporalProfiles, self.actionToggleEditing,
-                           self.actionRemoveTemporalProfile, self.actionLoadMissingValues]
-        #TBD.
-        #self.line.setVisible(False)
-        #self.listWidget.setVisible(False)
-        self.baseTitle = self.windowTitle()
-        self.stackedWidget.currentChanged.connect(self.onStackPageChanged)
+        if len(self.mTemporalProfileLayer) > 0:
+            temporalProfile = self.mTemporalProfileLayer[0]
+            plotStyle.setTemporalProfile(temporalProfile)
 
 
-        self.plotWidget3D = None
-        self.plotWidget3DMPL = None
+        if len(self.plotSettingsModel2D) > 0:
+            lastStyle = self.plotSettingsModel2D[0] #top style in list is the most-recent
+            assert isinstance(lastStyle, TemporalProfile2DPlotStyle)
+            markerColor = nextColor(lastStyle.markerBrush.color())
+            plotStyle.markerBrush.setColor(markerColor)
+        self.plotSettingsModel2D.insertPlotStyles([plotStyle], i=0)
+        pdi = plotStyle.createPlotItem(self.plot2D)
 
-        self.init3DWidgets('gl')
+        assert isinstance(pdi, TemporalProfilePlotDataItem)
+        pdi.sigClicked.connect(self.onProfileClicked2D)
+        pdi.sigPointsClicked.connect(self.onPointsClicked2D)
+        self.plot2D.plotItem.addItem(pdi)
+        #self.plot2D.getPlotItem().addItem(pg.PlotDataItem(x=[1, 2, 3], y=[1, 2, 3]))
+        #plotItem.addDataItem(pdi)
+        #plotItem.plot().sigPlotChanged.emit(plotItem)
+        self.updatePlot2D()
+        return plotStyle
 
 
-        #pi = self.plotWidget2D.plotItem
-        #ax = DateAxis(orientation='bottom', showValues=True)
-        #pi.layout.addItem(ax, 3,2)
+    def createNewPlotStyle3D(self):
+        if not (ENABLE_OPENGL and OPENGL_AVAILABLE):
+            return
 
 
-        self.TS = None
+        plotStyle = TemporalProfile3DPlotStyle()
+        plotStyle.sigExpressionUpdated.connect(self.updatePlot3D)
 
-        self.progressBar.setMinimum(0)
-        self.progressBar.setMaximum(100)
-        self.progressBar.setValue(0)
-        self.progressInfo.setText('')
-        self.pxViewModel2D = None
-        self.pxViewModel3D = None
+        if len(self.mTemporalProfileLayer) > 0:
+            temporalProfile = self.mTemporalProfileLayer[0]
+            plotStyle.setTemporalProfile(temporalProfile)
+            if len(self.plotSettingsModel3D) > 0:
+                color = self.plotSettingsModel3D[-1].color()
+                plotStyle.setColor(nextColor(color))
 
-        self.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
+        sensors = list(self.TS.mSensors2TSDs.keys())
+        if len(sensors) > 0:
+            plotStyle.setSensor(sensors[0])
 
-    def init3DWidgets(self, mode='gl'):
-        assert mode in ['gl']
-        l = self.frame3DPlot.layout()
 
-        if ENABLE_OPENGL and OPENGL_AVAILABLE and mode == 'gl':
+        self.plotSettingsModel3D.insertPlotStyles([plotStyle], i=0) # latest to the top
+        plotItems = plotStyle.createPlotItem()
+        self.plot3D.addItems(plotItems)
+        #self.updatePlot3D()
 
-            from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D
-            self.plotWidget3D = ViewWidget3D(parent=self.labelDummy3D.parent())
-            self.plotWidget3D.setObjectName('plotWidget3D')
+    def onProfileClicked2D(self, pdi):
+        if isinstance(pdi, TemporalProfilePlotDataItem):
+            sensor = pdi.mPlotStyle.sensor()
+            tp = pdi.mPlotStyle.temporalProfile()
+            if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument):
+                c = tp.coordinate()
+                info = ['Sensor:{}'.format(sensor.name()),
+                        'Coordinate:{}, {}'.format(c.x(), c.y())]
+                self.ui.tbInfo2D.setPlainText('\n'.join(info))
 
-            size = self.labelDummy3D.size()
-            l.addWidget(self.plotWidget3D)
-            self.plotWidget3D.setSizePolicy(self.labelDummy3D.sizePolicy())
-            self.labelDummy3D.setVisible(False)
-            l.removeWidget(self.labelDummy3D)
-            #self.plotWidget3D.setBaseSize(size)
-            self.splitter3D.setSizes([100, 100])
-            self.frameSettings3D.setEnabled(True)
-        else:
-            self.frameSettings3D.setEnabled(False)
 
-    def onStackPageChanged(self, i):
-        w = self.stackedWidget.currentWidget()
-        title = self.baseTitle
-        if w == self.page2D:
-            title = '{} | 2D'.format(title)
-            for a in self.mActions2D:
-                a.setVisible(True)
-            for a in self.mActions3D + self.mActionsTP:
-                a.setVisible(False)
-        elif w == self.page3D:
-            title = '{} | 3D (experimental!)'.format(title)
-            for a in self.mActions2D + self.mActionsTP:
-                a.setVisible(False)
-            for a in self.mActions3D:
-                a.setVisible(True)
-        elif w == self.pagePixel:
-            title = '{} | Coordinates'.format(title)
-            for a in self.mActions2D + self.mActions3D:
-                a.setVisible(False)
-            for a in self.mActionsTP:
-                a.setVisible(True)
+    def onPointsClicked2D(self, pdi, spottedItems):
+        if isinstance(pdi, TemporalProfilePlotDataItem) and isinstance(spottedItems, list):
+            sensor = pdi.mPlotStyle.sensor()
+            tp = pdi.mPlotStyle.temporalProfile()
+            if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument):
+                c = tp.coordinate()
+                info = ['Sensor: {}'.format(sensor.name()),
+                        'Coordinate: {}, {}'.format(c.x(), c.y())]
 
-        w.update()
-        self.setWindowTitle(title)
+                for item in spottedItems:
+                    pos = item.pos()
+                    x = pos.x()
+                    y = pos.y()
+                    date = num2date(x)
+                    info.append('Date: {}\nValue: {}'.format(date, y))
 
+                self.ui.tbInfo2D.setPlainText('\n'.join(info))
 
-class SpectralTemporalVisualization(QObject):
+    def onTemporalProfilesAdded(self, profiles):
+        # self.mTemporalProfileLayer.prune()
+        for plotStyle in self.plotSettingsModel3D:
+            assert isinstance(plotStyle, TemporalProfilePlotStyleBase)
+            if not isinstance(plotStyle.temporalProfile(), TemporalProfile):
 
-    sigShowPixel = pyqtSignal(TimeSeriesDatum, QgsPoint, QgsCoordinateReferenceSystem)
+                r = self.plotSettingsModel3D.plotStyle2idx(plotStyle).row()
+                c = self.plotSettingsModel3D.columnIndex(self.plotSettingsModel3D.cnTemporalProfile)
+                idx = self.plotSettingsModel3D.createIndex(r, c)
+                self.plotSettingsModel3D.setData(idx, self.mTemporalProfileLayer[0])
 
-    """
-    Signalizes to move to specific date of interest
-    """
-    sigMoveToDate = pyqtSignal(np.datetime64)
+    def onTemporalProfileSelectionChanged(self, selectedFIDs, deselectedFIDs):
+        nSelected = len(selectedFIDs)
 
+        self.ui.actionRemoveTemporalProfile.setEnabled(nSelected > 0)
 
-    def __init__(self, timeSeries, profileDock):
-        super(SpectralTemporalVisualization, self).__init__()
 
-        assert isinstance(profileDock, ProfileViewDockUI)
-        self.ui = profileDock
+    def onPlot2DSelectionChanged(self, selected, deselected):
 
-        import eotimeseriesviewer.pixelloader
-        if DEBUG:
-            eotimeseriesviewer.pixelloader.DEBUG = True
+        self.ui.actionRemoveStyle2D.setEnabled(len(selected) > 0)
 
-        #the timeseries. will be set later
-        assert isinstance(timeSeries, TimeSeries)
-        self.TS = timeSeries
-        self.plot_initialized = False
+    def onPlot3DSelectionChanged(self, selected, deselected):
 
-        self.plot2D = self.ui.plotWidget2D
-        self.plot2D.getViewBox().sigMoveToDate.connect(self.sigMoveToDate)
-        self.plot3D = self.ui.plotWidget3D
+        self.ui.actionRemoveStyle3D.setEnabled(len(selected) > 0)
 
-        # temporal profile collection to store loaded values
-        self.mTemporalProfileLayer = TemporalProfileLayer(self.TS)
-        self.mTemporalProfileLayer.sigTemporalProfilesAdded.connect(self.onTemporalProfilesAdded)
-        self.mTemporalProfileLayer.startEditing()
-        self.mTemporalProfileLayer.selectionChanged.connect(self.onTemporalProfileSelectionChanged)
+    def initActions(self):
 
-        # fix to not loose C++ reference on temporal profile layer in case it is removed from QGIS mapcanvas
-        self.mMapCanvas = QgsMapCanvas()
-        self.mMapCanvas.setVisible(False)
-        self.mMapCanvas.setLayers([self.mTemporalProfileLayer])
-        # self.tpCollectionListModel = TemporalProfileCollectionListModel(self.tpCollection)
+        self.ui.actionRemoveStyle2D.setEnabled(False)
+        self.ui.actionRemoveTemporalProfile.setEnabled(False)
+        self.ui.actionAddStyle2D.triggered.connect(self.createNewPlotStyle2D)
+        self.ui.actionAddStyle3D.triggered.connect(self.createNewPlotStyle3D)
+        self.ui.actionRefresh2D.triggered.connect(self.updatePlot2D)
+        self.ui.actionRefresh3D.triggered.connect(self.updatePlot3D)
+        self.ui.actionRemoveStyle2D.triggered.connect(lambda:self.removePlotStyles2D(self.selected2DPlotStyles()))
+        self.ui.actionRemoveTemporalProfile.triggered.connect(lambda :self.removeTemporalProfiles(self.mTemporalProfileLayer.selectedFeatureIds()))
+        self.ui.actionToggleEditing.triggered.connect(self.onToggleEditing)
+        self.ui.actionReset2DPlot.triggered.connect(self.plot2D.resetViewBox)
+        self.plot2D.resetTransform()
+        self.ui.actionReset3DCamera.triggered.connect(self.reset3DCamera)
+        self.ui.actionLoadTPFromOgr.triggered.connect(lambda : self.mTemporalProfileLayer.loadCoordinatesFromOgr(None))
+        self.ui.actionLoadMissingValues.triggered.connect(lambda: self.loadMissingData())
+        self.ui.actionSaveTemporalProfiles.triggered.connect(self.mTemporalProfileLayer.saveTemporalProfiles)
+        #set actions to be shown in the TemporalProfileTableView context menu
+        ma = [self.ui.actionSaveTemporalProfiles, self.ui.actionLoadMissingValues]
 
-        assert isinstance(self.ui.mDualView, QgsDualView)
-        self.ui.mDualView.init(self.mTemporalProfileLayer, self.mMapCanvas)
-        self.ui.mDualView.setView(QgsDualView.AttributeTable)
-        # pixel loader to load pixel values in parallel
-        config = QgsAttributeTableConfig()
-        config.update(self.mTemporalProfileLayer.fields())
-        config.setActionWidgetVisible(True)
 
-        hidden = [FN_ID]
-        for i, columnConfig in enumerate(config.columns()):
+        self.onEditingToggled()
 
-            assert isinstance(columnConfig, QgsAttributeTableConfig.ColumnConfig)
-            config.setColumnHidden(i, columnConfig.name in hidden)
+    def onSaveTemporalProfiles(self):
 
+        s = ""
 
-        self.mTemporalProfilesTableConfig = config
-        self.mTemporalProfileLayer.setAttributeTableConfig(self.mTemporalProfilesTableConfig)
+    def onToggleEditing(self, b):
 
-        self.pixelLoader = PixelLoader()
-        self.pixelLoader.sigPixelLoaded.connect(self.onPixelLoaded)
-        self.pixelLoader.sigLoadingStarted.connect(lambda: self.ui.progressInfo.setText('Start loading...'))
-        # self.pixelLoader.sigLoadingStarted.connect(self.tpCollection.prune)
-        self.pixelLoader.sigLoadingFinished.connect(lambda: self.plot2D.enableAutoRange('x', False))
+        if self.mTemporalProfileLayer.isEditable():
+            self.mTemporalProfileLayer.saveEdits(leaveEditable=False)
+        else:
+            self.mTemporalProfileLayer.startEditing()
+        self.onEditingToggled()
 
-        # set the plot models for 2D
-        self.plotSettingsModel2D = PlotSettingsModel2D()
-        self.ui.tableView2DProfiles.setModel(self.plotSettingsModel2D)
-        self.ui.tableView2DProfiles.setSortingEnabled(False)
-        self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot2DSelectionChanged)
-        self.ui.tableView2DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
-        self.plotSettingsModel2D.sigDataChanged.connect(self.requestUpdate)
-        self.plotSettingsModel2D.rowsInserted.connect(self.onRowsInserted2D)
-        self.delegateTableView2D = PlotSettingsModel2DWidgetDelegate(self.ui.tableView2DProfiles, self.mTemporalProfileLayer)
-        self.delegateTableView2D.setItemDelegates(self.ui.tableView2DProfiles)
+    def onEditingToggled(self):
+        lyr = self.mTemporalProfileLayer
 
-        # set the plot models for 3D
-        self.plotSettingsModel3D = PlotSettingsModel3D()
-        self.ui.tableView3DProfiles.setModel(self.plotSettingsModel3D)
-        self.ui.tableView3DProfiles.setSortingEnabled(False)
-        self.ui.tableView2DProfiles.selectionModel().selectionChanged.connect(self.onPlot3DSelectionChanged)
-        self.ui.tableView3DProfiles.horizontalHeader().setResizeMode(QHeaderView.Interactive)
-        self.plotSettingsModel3D.rowsInserted.connect(self.onRowsInserted3D)
-        self.delegateTableView3D = PlotSettingsModel3DWidgetDelegate(self.ui.tableView3DProfiles, self.mTemporalProfileLayer)
-        self.delegateTableView3D.setItemDelegates(self.ui.tableView3DProfiles)
+        hasSelectedFeatures = lyr.selectedFeatureCount() > 0
+        isEditable = lyr.isEditable()
+        self.ui.actionToggleEditing.blockSignals(True)
+        self.ui.actionToggleEditing.setChecked(isEditable)
+        #self.actionSaveTemporalProfiles.setEnabled(isEditable)
+        #self.actionReload.setEnabled(not isEditable)
+        self.ui.actionToggleEditing.blockSignals(False)
 
-        if not ENABLE_OPENGL:
-            self.ui.listWidget.item(1).setHidden(True)
-            self.ui.page3D.setHidden(True)
+        #self.actionAddAttribute.setEnabled(isEditable)
+        #self.actionRemoveAttribute.setEnabled(isEditable)
+        self.ui.actionRemoveTemporalProfile.setEnabled(isEditable and hasSelectedFeatures)
+        #self.actionPasteFeatures.setEnabled(isEditable)
+        self.ui.actionToggleEditing.setEnabled(not lyr.readOnly())
 
 
 
-        def onTemporalProfilesRemoved(removedProfiles):
-            #set to valid temporal profile
 
-            affectedStyles2D = [p for p in self.plotSettingsModel2D if p.temporalProfile() in removedProfiles]
-            affectedStyles3D = [p for p in self.plotSettingsModel3D if p.temporalProfile() in removedProfiles]
-            alternativeProfile = self.mTemporalProfileLayer[-1] if len(self.mTemporalProfileLayer) > 0 else None
-            for s in affectedStyles2D:
-                assert isinstance(s, TemporalProfile2DPlotStyle)
-                m = self.plotSettingsModel2D
-                idx = m.plotStyle2idx(s)
-                idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile))
-                m.setData(idx, alternativeProfile, Qt.EditRole)
+    def reset3DCamera(self, *args):
 
-            for s in affectedStyles3D:
-                assert isinstance(s, TemporalProfile3DPlotStyle)
-                m = self.plotSettingsModel3D
-                idx = m.plotStyle2idx(s)
-                idx = m.createIndex(idx.row(), m.columnNames.index(m.cnTemporalProfile))
-                m.setData(idx, alternativeProfile, Qt.EditRole)
+        if ENABLE_OPENGL and OPENGL_AVAILABLE:
+            self.ui.actionReset3DCamera.trigger()
 
-        self.mTemporalProfileLayer.sigTemporalProfilesRemoved.connect(onTemporalProfilesRemoved)
 
-        # remove / add plot style
-        def on2DPlotStyleRemoved(plotStyles):
-            for plotStyle in plotStyles:
-                assert isinstance(plotStyle, PlotStyle)
-                for pi in plotStyle.mPlotItems:
-                    self.plot2D.plotItem.removeItem(pi)
 
-        def on3DPlotStyleRemoved(plotStyles):
-            toRemove = []
-            for plotStyle in plotStyles:
-                assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
-                toRemove.append(plotStyle.mPlotItems)
-            self.plot3D.removeItems(toRemove)
+    sigMoveToTSD = pyqtSignal(TimeSeriesDatum)
 
+    def onMoveToDate(self, date):
+        dt = np.asarray([np.abs(tsd.date() - date) for tsd in self.TS])
+        i = np.argmin(dt)
+        self.sigMoveToTSD.emit(self.TS[i])
 
-        self.plotSettingsModel2D.sigPlotStylesRemoved.connect(on2DPlotStyleRemoved)
-        self.plotSettingsModel3D.sigPlotStylesRemoved.connect(on3DPlotStyleRemoved)
 
-        #initialize the update loop
-        self.updateRequested = True
-        self.updateTimer = QTimer(self)
-        self.updateTimer.timeout.connect(self.onDataUpdate)
-        self.updateTimer.start(2000)
+    def onPixelLoaded(self, d):
 
-        self.sigMoveToDate.connect(self.onMoveToDate)
+        if isinstance(d, PixelLoaderTask):
 
-        self.initActions()
-        #self.ui.stackedWidget.setCurrentPage(self.ui.pagePixel)
-        self.ui.onStackPageChanged(self.ui.stackedWidget.currentIndex())
+            bn = os.path.basename(d.sourcePath)
+            if d.success():
 
-    def temporalProfileLayer(self)->TemporalProfileLayer:
-        """
-        Returns a QgsVectorLayer that is used to store profile coordinates.
-        :return:
-        """
-        return self.mTemporalProfileLayer
+                t = 'Loaded {} pixel from {}.'.format(len(d.resProfiles), bn)
+                self.mTemporalProfileLayer.addPixelLoaderResult(d)
+                self.updateRequested = True
+            else:
+                t = 'Failed loading from {}.'.format(bn)
+                if d.info and d.info != '':
+                    t += '({})'.format(d.info)
 
-    def onTemporalProfilesContextMenu(self, event):
-        assert isinstance(event, QContextMenuEvent)
-        tableView = self.ui.tableViewTemporalProfiles
-        selectionModel = self.ui.tableViewTemporalProfiles.selectionModel()
-        assert isinstance(selectionModel, QItemSelectionModel)
+            # QgsApplication.processEvents()
+            self.ui.progressInfo.setText(t)
 
-        model = self.ui.tableViewTemporalProfiles.model()
-        assert isinstance(model, TemporalProfileLayer)
+    def requestUpdate(self, *args):
+        self.updateRequested = True
+        #next time
 
-        temporalProfiles = []
+    def onRowsInserted2D(self, parent, start, end):
+        model = self.ui.tableView2DProfiles.model().sourceModel()
+        if isinstance(model, PlotSettingsModel2D):
+            colExpression = model.columnIndex(model.cnExpression)
+            colStyle = model.columnIndex(model.cnStyle)
+            while start <= end:
+                idxExpr = model.createIndex(start, colExpression)
+                idxStyle = model.createIndex(start, colStyle)
+                self.ui.tableView2DProfiles.openPersistentEditor(idxExpr)
+                self.ui.tableView2DProfiles.openPersistentEditor(idxStyle)
+                start += 1
 
-        if len(selectionModel.selectedIndexes()) > 0:
-            for idx in selectionModel.selectedIndexes():
-                tp = model.idx2tp(idx)
-                if isinstance(tp, TemporalProfile) and not tp in temporalProfiles:
-                    temporalProfiles.append(tp)
-        else:
-            temporalProfiles = model[:]
+    def onRowsInserted3D(self, parent, start, end):
+        model = self.ui.tableView3DProfiles.model()
+        if isinstance(model, PlotSettingsModel3D):
+            colExpression = model.columnIndex(model.cnExpression)
+            colStyle = model.columnIndex(model.cnStyle)
+            while start <= end:
+                idxStyle = model.createIndex(start, colStyle)
+                idxExpr = model.createIndex(start, colExpression)
+                self.ui.tableView3DProfiles.openPersistentEditor(idxStyle)
+                self.ui.tableView3DProfiles.openPersistentEditor(idxExpr)
+                start += 1
 
-        spatialPoints = [tp.coordinate() for tp in temporalProfiles]
+    def onObservationClicked(self, plotDataItem, points):
+        for p in points:
+            tsd = p.data()
+            #print(tsd)
 
 
-        menu = QMenu()
+    def loadMissingData(self, backgroundProcess=False):
+        """
+        Loads all band values of collected locations that have not been loaded until now
+        """
 
-        a = menu.addAction('Load missing')
-        a.setToolTip('Loads missing band-pixels.')
-        a.triggered.connect(lambda : self.loadCoordinate(spatialPoints=spatialPoints, mode='all'))
-        s = ""
+        fids = self.mTemporalProfileLayer.selectedFeatureIds()
+        if len(fids) == 0:
+            fids = [f.id() for f in self.mTemporalProfileLayer.getFeatures()]
 
-        a = menu.addAction('Reload')
-        a.setToolTip('Reloads all band-pixels.')
-        a.triggered.connect(lambda: self.loadCoordinate(spatialPoints=spatialPoints, mode='reload'))
+        tps = [self.mTemporalProfileLayer.mProfiles.get(fid) for fid in fids]
+        spatialPoints = [tp.coordinate() for tp in tps if isinstance(tp, TemporalProfile)]
+        self.loadCoordinate(spatialPoints=spatialPoints, mode='all', backgroundProcess=backgroundProcess)
 
-        menu.popup(tableView.viewport().mapToGlobal(event.pos()))
-        self.menu = menu
+    LOADING_MODES = ['missing', 'reload', 'all']
+    def loadCoordinate(self, spatialPoints=None, LUT_bandIndices=None, mode='missing', backgroundProcess = True):
+        """
+        :param spatialPoints: [list-of-geometries] to load pixel values from
+        :param LUT_bandIndices: dictionary {sensor:[indices]} with band indices to be loaded per sensor
+        :param mode:
+        :return:
+        """
+        """
+        Loads a temporal profile for a single or multiple geometries.
+        :param spatialPoints: SpatialPoint | [list-of-SpatialPoints]
+        """
+        assert mode in SpectralTemporalVisualization.LOADING_MODES
 
+        if not isinstance(self.plotSettingsModel2D, PlotSettingsModel2D):
+            return False
 
-    def selected2DPlotStyles(self):
-        result = []
+        # if not self.pixelLoader.isReadyToLoad():
+        #    return False
 
-        m = self.ui.tableView2DProfiles.model()
-        for idx in selectedModelIndices(self.ui.tableView2DProfiles):
-            result.append(m.idx2plotStyle(idx))
-        return result
+        assert isinstance(self.TS, TimeSeries)
 
+        # Get or create the TimeSeriesProfiles which will store the loaded values
 
+        tasks = []
+        TPs = []
+        theGeometries = []
 
-    def removePlotStyles2D(self, plotStyles):
-        m = self.ui.tableView2DProfiles.model()
-        if isinstance(m, PlotSettingsModel2D):
-            m.removePlotStyles(plotStyles)
+        # Define which (new) bands need to be loaded for each sensor
+        if LUT_bandIndices is None:
+            LUT_bandIndices = dict()
+            for sensor in self.TS.sensors():
+                if mode in ['all','reload']:
+                    LUT_bandIndices[sensor] = list(range(sensor.nb))
+                else:
+                    LUT_bandIndices[sensor] = self.plotSettingsModel2D.requiredBandsIndices(sensor)
 
-    def removeTemporalProfiles(self, fids):
+        assert isinstance(LUT_bandIndices, dict)
+        for sensor in self.TS.sensors():
+            assert sensor in LUT_bandIndices.keys()
 
-        self.mTemporalProfileLayer.selectByIds(fids)
-        b = self.mTemporalProfileLayer.isEditable()
-        self.mTemporalProfileLayer.startEditing()
-        self.mTemporalProfileLayer.deleteSelectedFeatures()
-        self.mTemporalProfileLayer.saveEdits(leaveEditable=b)
+        #update new / existing points
+        if isinstance(spatialPoints, SpatialPoint):
+            spatialPoints = [spatialPoints]
 
-    def createNewPlotStyle2D(self):
-        l = len(self.mTemporalProfileLayer)
+        for spatialPoint in spatialPoints:
+            assert isinstance(spatialPoint, SpatialPoint)
+            TP = self.mTemporalProfileLayer.fromSpatialPoint(spatialPoint)
 
+            # if not TP exists for this point, create an empty one
+            if not isinstance(TP, TemporalProfile):
+                TP = self.mTemporalProfileLayer.createTemporalProfiles(spatialPoint)[0]
 
-        plotStyle = TemporalProfile2DPlotStyle()
-        plotStyle.sigExpressionUpdated.connect(self.updatePlot2D)
+                if len(self.mTemporalProfileLayer) == 1:
+                    if len(self.plotSettingsModel2D) == 0:
+                        self.createNewPlotStyle2D()
 
-        sensors = self.TS.sensors()
-        if len(sensors) > 0:
-            plotStyle.setSensor(sensors[0])
+                    if len(self.plotSettingsModel3D) == 0:
+                        self.createNewPlotStyle3D()
 
-        if len(self.mTemporalProfileLayer) > 0:
-            temporalProfile = self.mTemporalProfileLayer[0]
-            plotStyle.setTemporalProfile(temporalProfile)
+            TPs.append(TP)
+            theGeometries.append(TP.coordinate())
 
 
-        if len(self.plotSettingsModel2D) > 0:
-            lastStyle = self.plotSettingsModel2D[0] #top style in list is the most-recent
-            assert isinstance(lastStyle, TemporalProfile2DPlotStyle)
-            markerColor = nextColor(lastStyle.markerBrush.color())
-            plotStyle.markerBrush.setColor(markerColor)
-        self.plotSettingsModel2D.insertPlotStyles([plotStyle], i=0)
-        pdi = plotStyle.createPlotItem(self.plot2D)
+        TP_ids = [TP.id() for TP in TPs]
+        # each TSD is a Task
+        s = ""
+        # a Task defines which bands are to be loaded
+        for tsd in self.TS:
+            assert isinstance(tsd, TimeSeriesDatum)
 
-        assert isinstance(pdi, TemporalProfilePlotDataItem)
-        pdi.sigClicked.connect(self.onProfileClicked2D)
-        pdi.sigPointsClicked.connect(self.onPointsClicked2D)
-        self.plot2D.plotItem.addItem(pdi)
-        #self.plot2D.getPlotItem().addItem(pg.PlotDataItem(x=[1, 2, 3], y=[1, 2, 3]))
-        #plotItem.addDataItem(pdi)
-        #plotItem.plot().sigPlotChanged.emit(plotItem)
-        self.updatePlot2D()
-        return plotStyle
+            # do not load from invisible TSDs
+            if not tsd.isVisible():
+                continue
 
+            # which bands do we need to load?
+            requiredIndices = set(LUT_bandIndices[tsd.sensor()])
+            if len(requiredIndices) == 0:
+                continue
 
-    def createNewPlotStyle3D(self):
-        if not (ENABLE_OPENGL and OPENGL_AVAILABLE):
-            return
+            if mode == 'missing':
+                missingIndices = set()
 
+                for TP in TPs:
+                    assert isinstance(TP, TemporalProfile)
+                    need2load = TP.missingBandIndices(tsd, requiredIndices=requiredIndices)
+                    missingIndices = missingIndices.union(need2load)
 
-        plotStyle = TemporalProfile3DPlotStyle()
-        plotStyle.sigExpressionUpdated.connect(self.updatePlot3D)
+                missingIndices = sorted(list(missingIndices))
+            else:
+                missingIndices = requiredIndices
 
-        if len(self.mTemporalProfileLayer) > 0:
-            temporalProfile = self.mTemporalProfileLayer[0]
-            plotStyle.setTemporalProfile(temporalProfile)
-            if len(self.plotSettingsModel3D) > 0:
-                color = self.plotSettingsModel3D[-1].color()
-                plotStyle.setColor(nextColor(color))
+            if len(missingIndices) > 0:
+                for pathImg in tsd.sourceUris():
+                    task = PixelLoaderTask(pathImg, theGeometries,
+                                       bandIndices=missingIndices,
+                                       temporalProfileIDs=TP_ids)
+                tasks.append(task)
 
-        sensors = list(self.TS.mSensors2TSDs.keys())
-        if len(sensors) > 0:
-            plotStyle.setSensor(sensors[0])
+        if len(tasks) > 0:
+            aGoodDefault = 2 if len(self.TS) > 25 else 1
 
 
-        self.plotSettingsModel3D.insertPlotStyles([plotStyle], i=0) # latest to the top
-        plotItems = plotStyle.createPlotItem()
-        self.plot3D.addItems(plotItems)
-        #self.updatePlot3D()
+            if DEBUG:
+                print('Start loading for {} geometries from {} sources...'.format(
+                    len(theGeometries), len(tasks)
+                ))
+            if backgroundProcess:
+                self.pixelLoader.startLoading(tasks)
+            else:
+                import eotimeseriesviewer.pixelloader
+                tasks = [PixelLoaderTask.fromDump(eotimeseriesviewer.pixelloader.doLoaderTask(None, task.toDump())) for task in tasks]
+                l = len(tasks)
+                for i, task in enumerate(tasks):
+                    self.pixelLoader.sigPixelLoaded.emit(task)
 
-    def onProfileClicked2D(self, pdi):
-        if isinstance(pdi, TemporalProfilePlotDataItem):
-            sensor = pdi.mPlotStyle.sensor()
-            tp = pdi.mPlotStyle.temporalProfile()
-            if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument):
-                c = tp.coordinate()
-                info = ['Sensor:{}'.format(sensor.name()),
-                        'Coordinate:{}, {}'.format(c.x(), c.y())]
-                self.ui.tbInfo2D.setPlainText('\n'.join(info))
+        else:
+            if DEBUG:
+                print('Data for geometries already loaded')
 
 
-    def onPointsClicked2D(self, pdi, spottedItems):
-        if isinstance(pdi, TemporalProfilePlotDataItem) and isinstance(spottedItems, list):
-            sensor = pdi.mPlotStyle.sensor()
-            tp = pdi.mPlotStyle.temporalProfile()
-            if isinstance(tp, TemporalProfile) and isinstance(sensor, SensorInstrument):
-                c = tp.coordinate()
-                info = ['Sensor: {}'.format(sensor.name()),
-                        'Coordinate: {}, {}'.format(c.x(), c.y())]
-
-                for item in spottedItems:
-                    pos = item.pos()
-                    x = pos.x()
-                    y = pos.y()
-                    date = num2date(x)
-                    info.append('Date: {}\nValue: {}'.format(date, y))
 
-                self.ui.tbInfo2D.setPlainText('\n'.join(info))
+    @QtCore.pyqtSlot()
+    def onDataUpdate(self):
 
-    def onTemporalProfilesAdded(self, profiles):
         # self.mTemporalProfileLayer.prune()
-        for plotStyle in self.plotSettingsModel3D:
-            assert isinstance(plotStyle, TemporalProfilePlotStyleBase)
-            if not isinstance(plotStyle.temporalProfile(), TemporalProfile):
 
-                r = self.plotSettingsModel3D.plotStyle2idx(plotStyle).row()
-                c = self.plotSettingsModel3D.columnIndex(self.plotSettingsModel3D.cnTemporalProfile)
-                idx = self.plotSettingsModel3D.createIndex(r, c)
-                self.plotSettingsModel3D.setData(idx, self.mTemporalProfileLayer[0])
-
-    def onTemporalProfileSelectionChanged(self, selectedFIDs, deselectedFIDs):
-        nSelected = len(selectedFIDs)
+        for plotSetting in self.plotSettingsModel2D:
+            assert isinstance(plotSetting, TemporalProfile2DPlotStyle)
+            tp = plotSetting.temporalProfile()
+            for pdi in plotSetting.mPlotItems:
+                assert isinstance(pdi, TemporalProfilePlotDataItem)
+                pdi.updateDataAndStyle()
+            if isinstance(tp, TemporalProfile) and plotSetting.temporalProfile().updated():
+                plotSetting.temporalProfile().resetUpdatedFlag()
 
-        self.ui.actionRemoveTemporalProfile.setEnabled(nSelected > 0)
 
+        for i in self.plot2D.plotItem.dataItems:
+            i.updateItems()
 
-    def onPlot2DSelectionChanged(self, selected, deselected):
+        notInit = [0, 1] == self.plot2D.plotItem.getAxis('bottom').range
+        if notInit:
+            x0 = x1 = None
+            for plotSetting in self.plotSettingsModel2D:
+                assert isinstance(plotSetting, TemporalProfile2DPlotStyle)
+                for pdi in plotSetting.mPlotItems:
+                    assert isinstance(pdi, TemporalProfilePlotDataItem)
+                    if pdi.xData.ndim == 0 or pdi.xData.shape[0] == 0:
+                        continue
+                    if x0 is None:
+                        x0 = pdi.xData.min()
+                        x1 = pdi.xData.max()
+                    else:
+                        x0 = min(pdi.xData.min(), x0)
+                        x1 = max(pdi.xData.max(), x1)
 
-        self.ui.actionRemoveStyle2D.setEnabled(len(selected) > 0)
+            if x0 is not None:
+                self.plot2D.plotItem.setXRange(x0, x1)
 
-    def onPlot3DSelectionChanged(self, selected, deselected):
+                # self.plot2D.xAxisInitialized = True
 
-        self.ui.actionRemoveStyle3D.setEnabled(len(selected) > 0)
+    @QtCore.pyqtSlot()
+    def updatePlot3D(self):
+        if ENABLE_OPENGL and OPENGL_AVAILABLE:
 
-    def initActions(self):
+            from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D
+            assert isinstance(self.plot3D, ViewWidget3D)
 
-        self.ui.actionRemoveStyle2D.setEnabled(False)
-        self.ui.actionRemoveTemporalProfile.setEnabled(False)
-        self.ui.actionAddStyle2D.triggered.connect(self.createNewPlotStyle2D)
-        self.ui.actionAddStyle3D.triggered.connect(self.createNewPlotStyle3D)
-        self.ui.actionRefresh2D.triggered.connect(self.updatePlot2D)
-        self.ui.actionRefresh3D.triggered.connect(self.updatePlot3D)
-        self.ui.actionRemoveStyle2D.triggered.connect(lambda:self.removePlotStyles2D(self.selected2DPlotStyles()))
-        self.ui.actionRemoveTemporalProfile.triggered.connect(lambda :self.removeTemporalProfiles(self.mTemporalProfileLayer.selectedFeatureIds()))
-        self.ui.actionToggleEditing.triggered.connect(self.onToggleEditing)
-        self.ui.actionReset2DPlot.triggered.connect(self.plot2D.resetViewBox)
-        self.plot2D.resetTransform()
-        self.ui.actionReset3DCamera.triggered.connect(self.reset3DCamera)
-        self.ui.actionLoadTPFromOgr.triggered.connect(lambda : self.mTemporalProfileLayer.loadCoordinatesFromOgr(None))
-        self.ui.actionLoadMissingValues.triggered.connect(lambda: self.loadMissingData())
-        self.ui.actionSaveTemporalProfiles.triggered.connect(self.mTemporalProfileLayer.saveTemporalProfiles)
-        #set actions to be shown in the TemporalProfileTableView context menu
-        ma = [self.ui.actionSaveTemporalProfiles, self.ui.actionLoadMissingValues]
+            # 1. ensure that data from all bands will be loaded
+            #    new loaded values will be shown in the next updatePlot3D call
+            coordinates = []
+            allPlotItems = []
+            for plotStyle3D in self.plotSettingsModel3D:
+                assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle)
+                if plotStyle3D.isPlotable():
+                    coordinates.append(plotStyle3D.temporalProfile().coordinate())
 
+            if len(coordinates) > 0:
+                self.loadCoordinate(coordinates, mode='all')
 
-        self.onEditingToggled()
+            toRemove = [item for item in self.plot3D.items if item not in allPlotItems]
+            self.plot3D.removeItems(toRemove)
+            toAdd = [item for item in allPlotItems if item not in self.plot3D.items]
+            self.plot3D.addItems(toAdd)
 
-    def onSaveTemporalProfiles(self):
 
-        s = ""
+            """
+            # 3. add new plot items
+            plotItems = []
+            for plotStyle3D in self.plotSettingsModel3D:
+                assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle)
+                if plotStyle3D.isPlotable():
+                    items = plotStyle3D.createPlotItem(None)
+                    plotItems.extend(items)
 
-    def onToggleEditing(self, b):
+            self.plot3D.addItems(plotItems)
+            self.plot3D.updateDataRanges()
+            self.plot3D.resetScaling()
+            """
 
-        if self.mTemporalProfileLayer.isEditable():
-            self.mTemporalProfileLayer.saveEdits(leaveEditable=False)
-        else:
-            self.mTemporalProfileLayer.startEditing()
-        self.onEditingToggled()
+    @QtCore.pyqtSlot()
+    def updatePlot2D(self):
+        if isinstance(self.plotSettingsModel2D, PlotSettingsModel2D):
 
-    def onEditingToggled(self):
-        lyr = self.mTemporalProfileLayer
+            locations = set()
+            for plotStyle in self.plotSettingsModel2D:
+                assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
+                if plotStyle.isPlotable():
+                    locations.add(plotStyle.temporalProfile().coordinate())
 
-        hasSelectedFeatures = lyr.selectedFeatureCount() > 0
-        isEditable = lyr.isEditable()
-        self.ui.actionToggleEditing.blockSignals(True)
-        self.ui.actionToggleEditing.setChecked(isEditable)
-        #self.actionSaveTemporalProfiles.setEnabled(isEditable)
-        #self.actionReload.setEnabled(not isEditable)
-        self.ui.actionToggleEditing.blockSignals(False)
+                    for pdi in plotStyle.mPlotItems:
+                        assert isinstance(pdi, TemporalProfilePlotDataItem)
+                        pdi.updateDataAndStyle()
 
-        #self.actionAddAttribute.setEnabled(isEditable)
-        #self.actionRemoveAttribute.setEnabled(isEditable)
-        self.ui.actionRemoveTemporalProfile.setEnabled(isEditable and hasSelectedFeatures)
-        #self.actionPasteFeatures.setEnabled(isEditable)
-        self.ui.actionToggleEditing.setEnabled(not lyr.readOnly())
+            #2. load pixel data
+            self.loadCoordinate(list(locations))
 
+            # https://github.com/pyqtgraph/pyqtgraph/blob/5195d9dd6308caee87e043e859e7e553b9887453/examples/customPlot.py
+            return
 
 
 
-    def reset3DCamera(self, *args):
+class PlotSettingsModel2DWidgetDelegate(QStyledItemDelegate):
+    """
 
-        if ENABLE_OPENGL and OPENGL_AVAILABLE:
-            self.ui.actionReset3DCamera.trigger()
+    """
+    def __init__(self, tableView, temporalProfileLayer, parent=None):
+        assert isinstance(tableView, QTableView)
+        assert isinstance(temporalProfileLayer, TemporalProfileLayer)
+        super(PlotSettingsModel2DWidgetDelegate, self).__init__(parent=parent)
+        self._preferedSize = QgsFieldExpressionWidget().sizeHint()
+        self.mTableView = tableView
+        self.mTemporalProfileLayer = temporalProfileLayer
+        self.mTimeSeries = temporalProfileLayer.timeSeries()
 
+        self.mSensorLayers = {}
 
 
-    sigMoveToTSD = pyqtSignal(TimeSeriesDatum)
+    def sortFilterProxyModel(self)->QSortFilterProxyModel:
+        return self.mTableView.model()
 
-    def onMoveToDate(self, date):
-        dt = np.asarray([np.abs(tsd.date - date) for tsd in self.TS])
-        i = np.argmin(dt)
-        self.sigMoveToTSD.emit(self.TS[i])
+    def plotSettingsModel(self)->PlotSettingsModel2D:
+        return self.sortFilterProxyModel().sourceModel()
 
+    def setItemDelegates(self, tableView):
+        assert isinstance(tableView, QTableView)
+        model = self.plotSettingsModel()
 
-    def onPixelLoaded(self, d):
+        assert isinstance(model, PlotSettingsModel2D)
+        for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]:
+            i = model.columnNames.index(c)
+            tableView.setItemDelegateForColumn(i, self)
 
-        if isinstance(d, PixelLoaderTask):
+    def getColumnName(self, index):
+        assert index.isValid()
+        model = self.plotSettingsModel()
+        assert isinstance(model, PlotSettingsModel2D)
+        return model.columnNames[index.column()]
+    """
+    def sizeHint(self, options, index):
+        s = super(ExpressionDelegate, self).sizeHint(options, index)
+        exprString = self.tableView.model().data(index)
+        l = QLabel()
+        l.setText(exprString)
+        x = l.sizeHint().width() + 100
+        s = QSize(x, s.height())
+        return self._preferedSize
+    """
+    def exampleLyr(self, sensor):
+        # if isinstance(sensor, SensorInstrument):
+        if sensor not in self.mSensorLayers.keys():
 
-            bn = os.path.basename(d.sourcePath)
-            if d.success():
+            crs = QgsCoordinateReferenceSystem('EPSG:4862')
+            uri = 'Point?crs={}'.format(crs.authid())
+            lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory')
+            f = sensorExampleQgsFeature(sensor)
+            assert isinstance(f, QgsFeature)
+            assert lyr.startEditing()
+            for field in f.fields():
+                lyr.addAttribute(field)
+            lyr.addFeature(f)
+            lyr.commitChanges()
+            self.mSensorLayers[sensor] = lyr
+        return self.mSensorLayers[sensor]
 
-                t = 'Loaded {} pixel from {}.'.format(len(d.resProfiles), bn)
-                self.mTemporalProfileLayer.addPixelLoaderResult(d)
-                self.updateRequested = True
-            else:
-                t = 'Failed loading from {}.'.format(bn)
-                if d.info and d.info != '':
-                    t += '({})'.format(d.info)
+    def createEditor(self, parent, option, index):
+        cname = self.getColumnName(index)
+        model = self.plotSettingsModel()
+        pmodel = self.sortFilterProxyModel()
 
-            # QgsApplication.processEvents()
-            self.ui.progressInfo.setText(t)
+        w = None
+        if index.isValid() and isinstance(model, PlotSettingsModel2D):
 
-    def requestUpdate(self, *args):
-        self.updateRequested = True
-        #next time
+            plotStyle = model.idx2plotStyle(pmodel.mapToSource(index))
 
-    def onRowsInserted2D(self, parent, start, end):
-        model = self.ui.tableView2DProfiles.model()
-        if isinstance(model, PlotSettingsModel2D):
-            colExpression = model.columnIndex(model.cnExpression)
-            colStyle = model.columnIndex(model.cnStyle)
-            while start <= end:
-                idxExpr = model.createIndex(start, colExpression)
-                idxStyle = model.createIndex(start, colStyle)
-                self.ui.tableView2DProfiles.openPersistentEditor(idxExpr)
-                self.ui.tableView2DProfiles.openPersistentEditor(idxStyle)
-                start += 1
+            if isinstance(plotStyle, TemporalProfile2DPlotStyle):
+                if cname == model.cnExpression:
 
-    def onRowsInserted3D(self, parent, start, end):
-        model = self.ui.tableView3DProfiles.model()
-        if isinstance(model, PlotSettingsModel3D):
-            colExpression = model.columnIndex(model.cnExpression)
-            colStyle = model.columnIndex(model.cnStyle)
-            while start <= end:
-                idxStyle = model.createIndex(start, colStyle)
-                idxExpr = model.createIndex(start, colExpression)
-                self.ui.tableView3DProfiles.openPersistentEditor(idxStyle)
-                self.ui.tableView3DProfiles.openPersistentEditor(idxExpr)
-                start += 1
+                    w = QgsFieldExpressionWidget(parent=parent)
+                    w.setExpressionDialogTitle('Values')
+                    w.setToolTip('Set an expression to specify the image band or calculate a spectral index.')
+                    w.fieldChanged[str, bool].connect(lambda n, b: self.checkData(index, w, w.expression()))
+                    w.setExpression(plotStyle.expression())
+                    plotStyle.sigSensorChanged.connect(lambda s: w.setLayer(self.exampleLyr(s)))
+                    if isinstance(plotStyle.sensor(), SensorInstrument):
+                        w.setLayer(self.exampleLyr(plotStyle.sensor()))
 
-    def onObservationClicked(self, plotDataItem, points):
-        for p in points:
-            tsd = p.data()
-            #print(tsd)
 
 
-    def loadMissingData(self, backgroundProcess=False):
-        """
-        Loads all band values of collected locations that have not been loaded until now
-        """
 
-        fids = self.mTemporalProfileLayer.selectedFeatureIds()
-        if len(fids) == 0:
-            fids = [f.id() for f in self.mTemporalProfileLayer.getFeatures()]
+                elif cname == model.cnStyle:
+                    w = PlotStyleButton(parent=parent)
+                    w.setPlotStyle(plotStyle)
+                    w.setToolTip('Set style.')
+                    w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle()))
 
-        tps = [self.mTemporalProfileLayer.mProfiles.get(fid) for fid in fids]
-        spatialPoints = [tp.coordinate() for tp in tps if isinstance(tp, TemporalProfile)]
-        self.loadCoordinate(spatialPoints=spatialPoints, mode='all', backgroundProcess=backgroundProcess)
+                elif cname == model.cnSensor:
+                    w = QComboBox(parent=parent)
+                    m = SensorListModel(self.mTimeSeries)
+                    w.setModel(m)
 
-    LOADING_MODES = ['missing', 'reload', 'all']
-    def loadCoordinate(self, spatialPoints=None, LUT_bandIndices=None, mode='missing', backgroundProcess = True):
-        """
-        :param spatialPoints: [list-of-geometries] to load pixel values from
-        :param LUT_bandIndices: dictionary {sensor:[indices]} with band indices to be loaded per sensor
-        :param mode:
-        :return:
-        """
-        """
-        Loads a temporal profile for a single or multiple geometries.
-        :param spatialPoints: SpatialPoint | [list-of-SpatialPoints]
-        """
-        assert mode in SpectralTemporalVisualization.LOADING_MODES
+                elif cname == model.cnTemporalProfile:
+                    w = QgsFeatureListComboBox(parent=parent)
+                    w.setSourceLayer(self.mTemporalProfileLayer)
+                    w.setIdentifierField('id')
+                    w.setDisplayExpression('to_string("id")+\'  \'+"name"')
+                    w.setAllowNull(False)
+                else:
+                    raise NotImplementedError()
+        return w
 
-        if not isinstance(self.plotSettingsModel2D, PlotSettingsModel2D):
-            return False
+    def checkData(self, index, w, value):
+        assert isinstance(index, QModelIndex)
+        model = self.mTableView.model()
+        if index.isValid() and isinstance(model, PlotSettingsModel2D):
+            plotStyle = model.idx2plotStyle(index)
+            assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
+            if isinstance(w, QgsFieldExpressionWidget):
+                assert value == w.expression()
+                assert w.isExpressionValid(value) == w.isValidExpression()
 
-        # if not self.pixelLoader.isReadyToLoad():
-        #    return False
+                if w.isValidExpression():
+                    self.commitData.emit(w)
+                else:
+                    s = ""
+                    #print(('Delegate commit failed',w.asExpression()))
+            if isinstance(w, PlotStyleButton):
 
-        assert isinstance(self.TS, TimeSeries)
+                self.commitData.emit(w)
 
-        # Get or create the TimeSeriesProfiles which will store the loaded values
+    def setEditorData(self, editor, proxyIndex):
 
-        tasks = []
-        TPs = []
-        theGeometries = []
+        model = self.plotSettingsModel()
+        index = self.sortFilterProxyModel().mapToSource(proxyIndex)
+        w = None
+        if index.isValid() and isinstance(model, PlotSettingsModel2D):
+            cname = self.getColumnName(proxyIndex)
+            if cname == model.cnExpression:
+                lastExpr = model.data(index, Qt.DisplayRole)
+                assert isinstance(editor, QgsFieldExpressionWidget)
+                editor.setProperty('lastexpr', lastExpr)
+                editor.setField(lastExpr)
 
-        # Define which (new) bands need to be loaded for each sensor
-        if LUT_bandIndices is None:
-            LUT_bandIndices = dict()
-            for sensor in self.TS.sensors():
-                if mode in ['all','reload']:
-                    LUT_bandIndices[sensor] = list(range(sensor.nb))
+            elif cname == model.cnStyle:
+                style = index.data()
+                assert isinstance(editor, PlotStyleButton)
+                editor.setPlotStyle(style)
+
+            elif cname == model.cnSensor:
+                assert isinstance(editor, QComboBox)
+                m = editor.model()
+                assert isinstance(m, SensorListModel)
+                sensor = index.data(role=Qt.UserRole)
+                if isinstance(sensor, SensorInstrument):
+                    idx = m.sensor2idx(sensor)
+                    editor.setCurrentIndex(idx.row())
+            elif cname == model.cnTemporalProfile:
+                assert isinstance(editor, QgsFeatureListComboBox)
+                value = editor.identifierValue()
+                if value != QVariant():
+                    plotStyle = index.data(role=Qt.UserRole)
+                    TP = plotStyle.temporalProfile()
+                    editor.setIdentifierValue(TP.id())
                 else:
-                    LUT_bandIndices[sensor] = self.plotSettingsModel2D.requiredBandsIndices(sensor)
+                    s  = ""
+            else:
+                raise NotImplementedError()
 
-        assert isinstance(LUT_bandIndices, dict)
-        for sensor in self.TS.sensors():
-            assert sensor in LUT_bandIndices.keys()
+    def setModelData(self, w, model, proxyIndex):
+        index = self.sortFilterProxyModel().mapToSource(proxyIndex)
+        cname = self.getColumnName(proxyIndex)
+        model = self.plotSettingsModel()
 
-        #update new / existing points
-        if isinstance(spatialPoints, SpatialPoint):
-            spatialPoints = [spatialPoints]
+        if index.isValid() and isinstance(model, PlotSettingsModel2D):
+            if cname == model.cnExpression:
+                assert isinstance(w, QgsFieldExpressionWidget)
+                expr = w.asExpression()
+                exprLast = model.data(index, Qt.DisplayRole)
 
-        for spatialPoint in spatialPoints:
-            assert isinstance(spatialPoint, SpatialPoint)
-            TP = self.mTemporalProfileLayer.fromSpatialPoint(spatialPoint)
+                if w.isValidExpression() and expr != exprLast:
+                    model.setData(index, w.asExpression(), Qt.EditRole)
 
-            # if not TP exists for this point, create an empty one
-            if not isinstance(TP, TemporalProfile):
-                TP = self.mTemporalProfileLayer.createTemporalProfiles(spatialPoint)[0]
+            elif cname == model.cnStyle:
+                if isinstance(w, PlotStyleButton):
+                    model.setData(index, w.plotStyle(), Qt.EditRole)
 
-                if len(self.mTemporalProfileLayer) == 1:
-                    if len(self.plotSettingsModel2D) == 0:
-                        self.createNewPlotStyle2D()
+            elif cname == model.cnSensor:
+                assert isinstance(w, QComboBox)
+                sensor = w.itemData(w.currentIndex(), role=Qt.UserRole)
+                if isinstance(sensor, SensorInstrument):
+                    model.setData(index, sensor, Qt.EditRole)
 
-                    if len(self.plotSettingsModel3D) == 0:
-                        self.createNewPlotStyle3D()
+            elif cname == model.cnTemporalProfile:
+                assert isinstance(w, QgsFeatureListComboBox)
+                fid = w.identifierValue()
+                if isinstance(fid, int):
+                    TP = self.mTemporalProfileLayer.mProfiles.get(fid)
+                    model.setData(index, TP, Qt.EditRole)
 
-            TPs.append(TP)
-            theGeometries.append(TP.coordinate())
+            else:
+                raise NotImplementedError()
 
 
-        TP_ids = [TP.id() for TP in TPs]
-        # each TSD is a Task
-        s = ""
-        # a Task defines which bands are to be loaded
-        for tsd in self.TS:
-            assert isinstance(tsd, TimeSeriesDatum)
 
-            # do not load from invisible TSDs
-            if not tsd.isVisible():
-                continue
+class PlotSettingsModel3DWidgetDelegate(QStyledItemDelegate):
+    """
 
-            # which bands do we need to load?
-            requiredIndices = set(LUT_bandIndices[tsd.sensor()])
-            if len(requiredIndices) == 0:
-                continue
+    """
+    def __init__(self, tableView, temporalProfileLayer, parent=None):
+        assert isinstance(tableView, QTableView)
+        assert isinstance(temporalProfileLayer, TemporalProfileLayer)
+        super(PlotSettingsModel3DWidgetDelegate, self).__init__(parent=parent)
+        self._preferedSize = QgsFieldExpressionWidget().sizeHint()
+        self.mTableView = tableView
+        self.mTimeSeries = temporalProfileLayer.timeSeries()
+        self.mTemporalProfileLayer = temporalProfileLayer
+        self.mSensorLayers = {}
 
-            if mode == 'missing':
-                missingIndices = set()
 
-                for TP in TPs:
-                    assert isinstance(TP, TemporalProfile)
-                    need2load = TP.missingBandIndices(tsd, requiredIndices=requiredIndices)
-                    missingIndices = missingIndices.union(need2load)
 
-                missingIndices = sorted(list(missingIndices))
-            else:
-                missingIndices = requiredIndices
 
-            if len(missingIndices) > 0:
-                for pathImg in tsd.sourceUris():
-                    task = PixelLoaderTask(pathImg, theGeometries,
-                                       bandIndices=missingIndices,
-                                       temporalProfileIDs=TP_ids)
-                tasks.append(task)
 
-        if len(tasks) > 0:
-            aGoodDefault = 2 if len(self.TS) > 25 else 1
 
 
-            if DEBUG:
-                print('Start loading for {} geometries from {} sources...'.format(
-                    len(theGeometries), len(tasks)
-                ))
-            if backgroundProcess:
-                self.pixelLoader.startLoading(tasks)
-            else:
-                import eotimeseriesviewer.pixelloader
-                tasks = [PixelLoaderTask.fromDump(eotimeseriesviewer.pixelloader.doLoaderTask(None, task.toDump())) for task in tasks]
-                l = len(tasks)
-                for i, task in enumerate(tasks):
-                    self.pixelLoader.sigPixelLoaded.emit(task)
+    def setItemDelegates(self, tableView):
+        assert isinstance(tableView, QTableView)
+        model = tableView.model()
+        assert isinstance(model, PlotSettingsModel3D)
+        for c in [model.cnSensor, model.cnExpression, model.cnStyle, model.cnTemporalProfile]:
+            i = model.columnNames.index(c)
+            tableView.setItemDelegateForColumn(i, self)
 
-        else:
-            if DEBUG:
-                print('Data for geometries already loaded')
+    def getColumnName(self, index):
+        assert index.isValid()
+        model = index.model()
+        assert isinstance(model, PlotSettingsModel3D)
+        return model.columnNames[index.column()]
+    """
+    def sizeHint(self, options, index):
+        s = super(ExpressionDelegate, self).sizeHint(options, index)
+        exprString = self.tableView.model().data(index)
+        l = QLabel()
+        l.setText(exprString)
+        x = l.sizeHint().width() + 100
+        s = QSize(x, s.height())
+        return self._preferedSize
+    """
+    def createEditor(self, parent, option, index):
+        cname = self.getColumnName(index)
+        model = self.mTableView.model()
+        w = None
+        if index.isValid() and isinstance(model, PlotSettingsModel3D):
+            plotStyle = model.idx2plotStyle(index)
+            if isinstance(plotStyle, TemporalProfile3DPlotStyle):
+                if cname == model.cnExpression:
+                    w = QgsFieldExpressionWidget(parent=parent)
+                    w.setExpression(plotStyle.expression())
+                    w.setLayer(self.exampleLyr(plotStyle.sensor()))
+                    def onSensorAdded(s):
+                        w.setLayer(self.exampleLyr(s))
+                    #plotStyle.sigSensorChanged.connect(lambda s : w.setLayer(self.exampleLyr(s)))
+                    plotStyle.sigSensorChanged.connect(onSensorAdded)
+                    w.setExpressionDialogTitle('Values')
+                    w.setToolTip('Set an expression to specify the image band or calculate a spectral index.')
+                    w.fieldChanged[str,bool].connect(lambda n, b : self.checkData(index, w, w.expression()))
 
+                elif cname == model.cnStyle:
+                    w = TemporalProfile3DPlotStyleButton(parent=parent)
+                    w.setPlotStyle(plotStyle)
+                    w.setToolTip('Set plot style')
+                    w.sigPlotStyleChanged.connect(lambda: self.checkData(index, w, w.plotStyle()))
 
+                elif cname == model.cnSensor:
+                    w = QComboBox(parent=parent)
+                    m = SensorListModel(self.mTimeSeries)
+                    w.setModel(m)
 
-    @QtCore.pyqtSlot()
-    def onDataUpdate(self):
+                elif cname == model.cnTemporalProfile:
+                    w = QgsFeatureListComboBox(parent=parent)
+                    w.setSourceLayer(self.mTemporalProfileLayer)
+                    w.setIdentifierField('id')
+                    w.setDisplayExpression('to_string("id")+\'  \'+"name"')
+                    w.setAllowNull(False)
+                else:
+                    raise NotImplementedError()
+        return w
 
-        # self.mTemporalProfileLayer.prune()
+    def exampleLyr(self, sensor):
 
-        for plotSetting in self.plotSettingsModel2D:
-            assert isinstance(plotSetting, TemporalProfile2DPlotStyle)
-            tp = plotSetting.temporalProfile()
-            for pdi in plotSetting.mPlotItems:
-                assert isinstance(pdi, TemporalProfilePlotDataItem)
-                pdi.updateDataAndStyle()
-            if isinstance(tp, TemporalProfile) and plotSetting.temporalProfile().updated():
-                plotSetting.temporalProfile().resetUpdatedFlag()
+        if sensor not in self.mSensorLayers.keys():
+            crs = QgsCoordinateReferenceSystem('EPSG:4862')
+            uri = 'Point?crs={}'.format(crs.authid())
+            lyr = QgsVectorLayer(uri, 'LOCATIONS', 'memory')
+            assert sensor is None or isinstance(sensor, SensorInstrument)
 
+            f = sensorExampleQgsFeature(sensor, singleBandOnly=True)
+            assert isinstance(f, QgsFeature)
+            assert lyr.startEditing()
+            for field in f.fields():
+                lyr.addAttribute(field)
+            lyr.addFeature(f)
+            lyr.commitChanges()
+            self.mSensorLayers[sensor] = lyr
+        return self.mSensorLayers[sensor]
 
-        for i in self.plot2D.plotItem.dataItems:
-            i.updateItems()
+    def checkData(self, index, w, value):
+        assert isinstance(index, QModelIndex)
+        model = self.mTableView.model()
+        if index.isValid() and isinstance(model, PlotSettingsModel3D):
+            plotStyle = model.idx2plotStyle(index)
+            assert isinstance(plotStyle, TemporalProfile3DPlotStyle)
+            if isinstance(w, QgsFieldExpressionWidget):
+                assert value == w.expression()
+                assert w.isExpressionValid(value) == w.isValidExpression()
 
-        notInit = [0, 1] == self.plot2D.plotItem.getAxis('bottom').range
-        if notInit:
-            x0 = x1 = None
-            for plotSetting in self.plotSettingsModel2D:
-                assert isinstance(plotSetting, TemporalProfile2DPlotStyle)
-                for pdi in plotSetting.mPlotItems:
-                    assert isinstance(pdi, TemporalProfilePlotDataItem)
-                    if pdi.xData.ndim == 0 or pdi.xData.shape[0] == 0:
-                        continue
-                    if x0 is None:
-                        x0 = pdi.xData.min()
-                        x1 = pdi.xData.max()
-                    else:
-                        x0 = min(pdi.xData.min(), x0)
-                        x1 = max(pdi.xData.max(), x1)
+                if w.isValidExpression():
+                    self.commitData.emit(w)
+                else:
+                    s = ""
+                    #print(('Delegate commit failed',w.asExpression()))
+            if isinstance(w, TemporalProfile3DPlotStyleButton):
 
-            if x0 is not None:
-                self.plot2D.plotItem.setXRange(x0, x1)
+                self.commitData.emit(w)
 
-                # self.plot2D.xAxisInitialized = True
 
-    @QtCore.pyqtSlot()
-    def updatePlot3D(self):
-        if ENABLE_OPENGL and OPENGL_AVAILABLE:
+    def setEditorData(self, editor, index):
+        cname = self.getColumnName(index)
+        model = self.mTableView.model()
 
-            from eotimeseriesviewer.temporalprofiles3dGL import ViewWidget3D
-            assert isinstance(self.plot3D, ViewWidget3D)
+        w = None
+        if index.isValid() and isinstance(model, PlotSettingsModel3D):
+            cname = self.getColumnName(index)
+            style = model.idx2plotStyle(index)
 
-            # 1. ensure that data from all bands will be loaded
-            #    new loaded values will be shown in the next updatePlot3D call
-            coordinates = []
-            allPlotItems = []
-            for plotStyle3D in self.plotSettingsModel3D:
-                assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle)
-                if plotStyle3D.isPlotable():
-                    coordinates.append(plotStyle3D.temporalProfile().coordinate())
+            if cname == model.cnExpression:
+                lastExpr = index.model().data(index, Qt.DisplayRole)
+                assert isinstance(editor, QgsFieldExpressionWidget)
+                editor.setProperty('lastexpr', lastExpr)
+                editor.setField(lastExpr)
 
-            if len(coordinates) > 0:
-                self.loadCoordinate(coordinates, mode='all')
+            elif cname == model.cnStyle:
+                assert isinstance(editor, TemporalProfile3DPlotStyleButton)
+                editor.setPlotStyle(style)
 
-            toRemove = [item for item in self.plot3D.items if item not in allPlotItems]
-            self.plot3D.removeItems(toRemove)
-            toAdd = [item for item in allPlotItems if item not in self.plot3D.items]
-            self.plot3D.addItems(toAdd)
+            elif cname == model.cnSensor:
+                assert isinstance(editor, QComboBox)
+                m = editor.model()
+                assert isinstance(m, SensorListModel)
+                sensor = index.data(role=Qt.UserRole)
+                if isinstance(sensor, SensorInstrument):
+                    idx = m.sensor2idx(sensor)
+                    editor.setCurrentIndex(idx.row())
+            elif cname == model.cnTemporalProfile:
+                assert isinstance(editor, QgsFeatureListComboBox)
+                value = editor.identifierValue()
+                if value != QVariant():
+                    plotStyle = index.data(role=Qt.UserRole)
+                    TP = plotStyle.temporalProfile()
+                    editor.setIdentifierValue(TP.id())
+                else:
+                    s  = ""
 
+            else:
+                raise NotImplementedError()
 
-            """
-            # 3. add new plot items
-            plotItems = []
-            for plotStyle3D in self.plotSettingsModel3D:
-                assert isinstance(plotStyle3D, TemporalProfile3DPlotStyle)
-                if plotStyle3D.isPlotable():
-                    items = plotStyle3D.createPlotItem(None)
-                    plotItems.extend(items)
+    def setModelData(self, w, model, index):
+        cname = self.getColumnName(index)
+        model = self.mTableView.model()
 
-            self.plot3D.addItems(plotItems)
-            self.plot3D.updateDataRanges()
-            self.plot3D.resetScaling()
-            """
+        if index.isValid() and isinstance(model, PlotSettingsModel3D):
+            if cname == model.cnExpression:
+                assert isinstance(w, QgsFieldExpressionWidget)
+                expr = w.asExpression()
+                exprLast = model.data(index, Qt.DisplayRole)
 
-    @QtCore.pyqtSlot()
-    def updatePlot2D(self):
-        if isinstance(self.plotSettingsModel2D, PlotSettingsModel2D):
+                if w.isValidExpression() and expr != exprLast:
+                    model.setData(index, w.asExpression(), Qt.EditRole)
 
-            locations = set()
-            for plotStyle in self.plotSettingsModel2D:
-                assert isinstance(plotStyle, TemporalProfile2DPlotStyle)
-                if plotStyle.isPlotable():
-                    locations.add(plotStyle.temporalProfile().coordinate())
+            elif cname == model.cnStyle:
+                assert isinstance(w, TemporalProfile3DPlotStyleButton)
+                model.setData(index, w.plotStyle(), Qt.EditRole)
 
-                    for pdi in plotStyle.mPlotItems:
-                        assert isinstance(pdi, TemporalProfilePlotDataItem)
-                        pdi.updateDataAndStyle()
+            elif cname == model.cnSensor:
+                assert isinstance(w, QComboBox)
+                sensor = w.itemData(w.currentIndex(), role=Qt.UserRole)
+                assert isinstance(sensor, SensorInstrument)
+                model.setData(index, sensor, Qt.EditRole)
 
-            #2. load pixel data
-            self.loadCoordinate(list(locations))
+                s = ""
+
+            elif cname == model.cnTemporalProfile:
+                assert isinstance(w, QgsFeatureListComboBox)
+                fid = w.identifierValue()
+                if isinstance(fid, int):
+                    TP = self.mTemporalProfileLayer.mProfiles.get(fid)
+                    model.setData(index, TP, Qt.EditRole)
+
+            else:
+                raise NotImplementedError()
 
-            # https://github.com/pyqtgraph/pyqtgraph/blob/5195d9dd6308caee87e043e859e7e553b9887453/examples/customPlot.py
-            return