diff --git a/examples/theater/PlotExample.py b/examples/theater/PlotExample.py index 45cc630b..277a0162 100644 --- a/examples/theater/PlotExample.py +++ b/examples/theater/PlotExample.py @@ -12,36 +12,49 @@ vf0.insertView( 0, pycinema.theater.views.NodeEditorView() ) vf1 = vf0.insertFrame(1) vf1.setVerticalOrientation() -PlotLineView_0 = vf1.insertView( 0, pycinema.theater.views.PlotLineView() ) -PlotBarView_0 = vf1.insertView( 1, pycinema.theater.views.PlotBarView() ) -vf1.setSizes([425, 424]) -vf0.setSizes([848, 847]) +PlotBarView_0 = vf1.insertView( 0, pycinema.theater.views.PlotBarView() ) +PlotLineView_0 = vf1.insertView( 1, pycinema.theater.views.PlotLineView() ) +PlotScatterView_0 = vf1.insertView( 2, pycinema.theater.views.PlotScatterView() ) +vf1.setSizes([292, 288, 295]) +vf0.setSizes([706, 705]) # filters CinemaDatabaseReader_0 = pycinema.filters.CinemaDatabaseReader() -PlotLineItem_0 = pycinema.filters.PlotLineItem() PlotBarItem_0 = pycinema.filters.PlotBarItem() +PlotLineItem_0 = pycinema.filters.PlotLineItem() +PlotScatterItem_0 = pycinema.filters.PlotScatterItem() # properties -PlotLineView_0.inputs.title.set("Sample Line Chart", False) -PlotLineView_0.inputs.background.set("white", False) -PlotLineView_0.inputs.plotitem.set(PlotLineItem_0.outputs.plotitem, False) PlotBarView_0.inputs.title.set("Plot Title", False) PlotBarView_0.inputs.background.set("white", False) PlotBarView_0.inputs.plotitem.set(PlotBarItem_0.outputs.plotitem, False) +PlotLineView_0.inputs.title.set("Plot Title", False) +PlotLineView_0.inputs.background.set("white", False) +PlotLineView_0.inputs.plotitem.set(PlotLineItem_0.outputs.plotitem, False) +PlotScatterView_0.inputs.title.set("Plot Title", False) +PlotScatterView_0.inputs.background.set("white", False) +PlotScatterView_0.inputs.plotitem.set(PlotScatterItem_0.outputs.plotitem, False) CinemaDatabaseReader_0.inputs.path.set("data/plot-line.cdb", False) CinemaDatabaseReader_0.inputs.file_column.set("FILE", False) -PlotLineItem_0.inputs.table.set(CinemaDatabaseReader_0.outputs.table, False) -PlotLineItem_0.inputs.x.set("b", False) -PlotLineItem_0.inputs.y.set("c", False) -PlotLineItem_0.inputs.linetype.set("default", False) -PlotLineItem_0.inputs.linecolor.set("red", False) -PlotLineItem_0.inputs.linewidth.set(1.0, False) PlotBarItem_0.inputs.table.set(CinemaDatabaseReader_0.outputs.table, False) PlotBarItem_0.inputs.x.set("a", False) PlotBarItem_0.inputs.y.set("b", False) -PlotBarItem_0.inputs.barcolor.set("blue", False) -PlotBarItem_0.inputs.barwidth.set(0.5, False) +PlotBarItem_0.inputs.brushcolor.set("blue", False) +PlotBarItem_0.inputs.width.set(0.5, False) +PlotLineItem_0.inputs.table.set(CinemaDatabaseReader_0.outputs.table, False) +PlotLineItem_0.inputs.x.set("a", False) +PlotLineItem_0.inputs.y.set("b", False) +PlotLineItem_0.inputs.penstyle.set("dash", False) +PlotLineItem_0.inputs.pencolor.set("red", False) +PlotLineItem_0.inputs.penwidth.set(2.0, False) +PlotScatterItem_0.inputs.table.set(CinemaDatabaseReader_0.outputs.table, False) +PlotScatterItem_0.inputs.x.set("a", False) +PlotScatterItem_0.inputs.y.set("b", False) +PlotScatterItem_0.inputs.pencolor.set("black", False) +PlotScatterItem_0.inputs.penwidth.set(1.0, False) +PlotScatterItem_0.inputs.brushcolor.set("gray", False) +PlotScatterItem_0.inputs.symbol.set("o", False) +PlotScatterItem_0.inputs.size.set(5.0, False) # execute pipeline -PlotLineView_0.update() +PlotBarView_0.update() diff --git a/examples/theater/URLCarScatterplot.py b/examples/theater/URLCarScatterplot.py new file mode 100644 index 00000000..a8853bf6 --- /dev/null +++ b/examples/theater/URLCarScatterplot.py @@ -0,0 +1,40 @@ +import pycinema +import pycinema.filters +import pycinema.theater +import pycinema.theater.views + +# pycinema settings +PYCINEMA = { 'VERSION' : '2.0.0'} + +# layout +vf0 = pycinema.theater.Theater.instance.centralWidget() +vf0.setHorizontalOrientation() +vf1 = vf0.insertFrame(0) +vf1.setVerticalOrientation() +vf1.insertView( 0, pycinema.theater.views.NodeEditorView() ) +TableView_1 = vf1.insertView( 1, pycinema.theater.views.TableView() ) +vf1.setSizes([610, 440]) +PlotScatterView_0 = vf0.insertView( 1, pycinema.theater.views.PlotScatterView() ) +vf0.setSizes([1111, 1110]) + +# filters +PlotScatterItem_0 = pycinema.filters.PlotScatterItem() +CSVReader_0 = pycinema.filters.CSVReader() + +# properties +PlotScatterView_0.inputs.title.set("Electric Range vs. Model Year for Vehicles in Washington State (2023)", False) +PlotScatterView_0.inputs.background.set("white", False) +PlotScatterView_0.inputs.plotitem.set(PlotScatterItem_0.outputs.plotitem, False) +PlotScatterItem_0.inputs.table.set(CSVReader_0.outputs.table, False) +PlotScatterItem_0.inputs.x.set("Model Year", False) +PlotScatterItem_0.inputs.y.set("Electric Range", False) +PlotScatterItem_0.inputs.pencolor.set("default", False) +PlotScatterItem_0.inputs.penwidth.set(1.0, False) +PlotScatterItem_0.inputs.brushcolor.set("light blue", False) +PlotScatterItem_0.inputs.symbol.set("o", False) +PlotScatterItem_0.inputs.size.set(10.0, False) +TableView_1.inputs.table.set(CSVReader_0.outputs.table, False) +CSVReader_0.inputs.path.set("https://data.wa.gov/api/views/f6w7-q2d2/rows.csv?accessType=DOWNLOAD", False) + +# execute pipeline +PlotScatterView_0.update() diff --git a/pycinema/Core.py b/pycinema/Core.py index c355966e..2c958c7e 100644 --- a/pycinema/Core.py +++ b/pycinema/Core.py @@ -2,6 +2,7 @@ import traceback import pprint import re +import numpy as np class Image(): def __init__(self, channels=None, meta=None): @@ -55,7 +56,8 @@ def isNumber(s): t = type(s) if t == int or t == float: return True - if t == str: + else: + # assume it is a string try: sf = float(s) return True @@ -63,6 +65,35 @@ def isNumber(s): return False return False +# +# table helper functions +# + +# +# get the column index from a table, return -1 on failure +# +def getColumnIndexFromTable(table, colname): + ID = -1 + + colnames = table[0] + if colname in colnames: + ID = colnames.index(colname) + + return ID + +# +# get a column of values from a table +# +def getColumnFromTable(table, colname): + colID = getColumnIndexFromTable(table, colname) + + if colID == -1: + print("ERROR: no column named \'" + colname + "\'") + return None + + else: + return [row [colID] for row in table[1:]] + def getTableExtent(table): try: nRows = len(table) diff --git a/pycinema/filters/CSVReader.py b/pycinema/filters/CSVReader.py new file mode 100644 index 00000000..f83983d4 --- /dev/null +++ b/pycinema/filters/CSVReader.py @@ -0,0 +1,58 @@ +from pycinema import Filter, isURL +import csv +import requests +from os.path import exists + +class CSVReader(Filter): + + def __init__(self): + super().__init__( + inputs={ + 'path': '' + }, + outputs={ + 'table': [[]] + } + ) + + def _update(self): + + table = [] + csvPath = self.inputs.path.get() + + if isURL(csvPath): + with requests.Session() as s: + print("requesting " + csvPath) + download = s.get(csvPath) + decoded = download.content.decode('utf-8') + csvdecoded = csv.reader(decoded.splitlines(), delimiter=',') + rows = list(csvdecoded) + for row in rows: + table.append(row) + + else: + if not csvPath: + self.outputs.table.set([[]]) + return 0 + + if not exists(csvPath): + print('[ERROR] file not found:', csvPath) + self.outputs.table.set([[]]) + return 0 + + try: + with open(csvPath, 'r+') as csvfile: + rows = csv.reader(csvfile, delimiter=',') + for row in rows: + table.append(row) + except: + print('[ERROR] Unable to open file:', csvPath) + self.outputs.table.set([[]]) + return 0 + + # remove empty lines + table = list(filter(lambda row: len(row)>0, table)) + + self.outputs.table.set(table) + + return 1 diff --git a/pycinema/filters/ImageMetadataToScatterItem.py b/pycinema/filters/ImageMetadataToScatterItem.py new file mode 100644 index 00000000..81a677bf --- /dev/null +++ b/pycinema/filters/ImageMetadataToScatterItem.py @@ -0,0 +1,58 @@ +from .PlotItem import * + +import numpy as np + +# +# ImageMetadataToScatterItem +# +class ImageMetadataToScatterItem(PlotItem): + + def __init__(self): + super().__init__( + inputs={ + 'images' : [], + 'x' : 'none', + 'y' : 'none', + 'pencolor' : 'default', + 'penwidth' : 1.0, + 'brushcolor': 'default', + 'symbol' : 'x', + 'size' : 1.0 + }, + outputs={ + 'plotitem' : {} + } + ) + + def _update(self): + + xdata = [] + ydata = [] + xlabel = self.inputs.x.get() + ylabel = self.inputs.y.get() + for image in self.inputs.images.get(): + xdata.append(image.meta[xlabel]) + ydata.append(image.meta[ylabel]) + + out = { 'x' : { + 'label' : self.inputs.x.get(), + 'data' : xdata + }, + 'y' : { + 'label' : self.inputs.y.get(), + 'data' : ydata + }, + 'pen' : { + 'color' : self.inputs.pencolor.get(), + 'width' : self.inputs.penwidth.get(), + }, + 'brush' : { + 'color' : self.inputs.brushcolor.get() + }, + 'symbol': self.inputs.symbol.get(), + 'size' : self.inputs.size.get() + } + self.outputs.plotitem.set({}) + self.outputs.plotitem.set(out) + + return 1 diff --git a/pycinema/filters/PlotBarItem.py b/pycinema/filters/PlotBarItem.py index b0b418c8..8b48871f 100644 --- a/pycinema/filters/PlotBarItem.py +++ b/pycinema/filters/PlotBarItem.py @@ -17,8 +17,8 @@ def __init__(self): 'table' : None, 'x' : 'none', 'y' : 'none', - 'barcolor' : 'default', - 'barwidth' : 1.0 + 'brushcolor': 'default', + 'width' : 1.0 }, outputs={ 'plotitem' : {} @@ -26,10 +26,8 @@ def __init__(self): ) def _update(self): - xID = self._getColumnIndex(self.inputs.x.get()) - xdata = self._getFloatArrayFromTable(xID) - yID = self._getColumnIndex(self.inputs.y.get()) - ydata = self._getFloatArrayFromTable(yID) + xdata = self._getColumnFromTable(self.inputs.x.get()) + ydata = self._getColumnFromTable(self.inputs.y.get()) out = { 'x' : { 'label' : self.inputs.x.get(), @@ -39,10 +37,10 @@ def _update(self): 'label' : self.inputs.y.get(), 'data' : ydata }, - 'bar' : { - 'color' : self.inputs.barcolor.get(), - 'width' : self.inputs.barwidth.get() - } + 'brush' : { + 'color' : self.inputs.brushcolor.get(), + }, + 'width' : self.inputs.width.get() } self.outputs.plotitem.set({}) self.outputs.plotitem.set(out) diff --git a/pycinema/filters/PlotItem.py b/pycinema/filters/PlotItem.py index 08ba6d6b..aecead20 100644 --- a/pycinema/filters/PlotItem.py +++ b/pycinema/filters/PlotItem.py @@ -1,4 +1,4 @@ -from pycinema import Filter +from pycinema import Filter, isNumber, getColumnFromTable import PIL import numpy as np @@ -15,17 +15,5 @@ class PlotItem(Filter): def __init__(self, inputs={}, outputs={}): super().__init__(inputs, outputs) - def _getColumnIndex(self, colname): - ID = 0 - - colnames = self.inputs.table.get()[0] - ID = colnames.index(colname) - - return ID - - def _getFloatArrayFromTable(self, colID): - data = self.inputs.table.get() - t = np.array(data) - row = t[:, colID] - - return row[1:].astype(float) + def _getColumnFromTable(self, colname): + return getColumnFromTable(self.inputs.table.get(), colname) diff --git a/pycinema/filters/PlotLineItem.py b/pycinema/filters/PlotLineItem.py index b17b983b..b392e878 100644 --- a/pycinema/filters/PlotLineItem.py +++ b/pycinema/filters/PlotLineItem.py @@ -15,9 +15,9 @@ def __init__(self): 'table' : None, 'x' : 'none', 'y' : 'none', - 'linetype' : 'default', - 'linecolor' : 'default', - 'linewidth' : 1.0 + 'penstyle' : 'default', + 'pencolor' : 'default', + 'penwidth' : 1.0 }, outputs={ 'plotitem' : 'none' @@ -25,10 +25,8 @@ def __init__(self): ) def _update(self): - xID = self._getColumnIndex(self.inputs.x.get()) - xdata = self._getFloatArrayFromTable(xID) - yID = self._getColumnIndex(self.inputs.y.get()) - ydata = self._getFloatArrayFromTable(yID) + xdata = self._getColumnFromTable(self.inputs.x.get()) + ydata = self._getColumnFromTable(self.inputs.y.get()) out = { 'x' : { 'label' : self.inputs.x.get(), @@ -38,10 +36,10 @@ def _update(self): 'label' : self.inputs.y.get(), 'data' : ydata }, - 'line' : { - 'type' : self.inputs.linetype.get(), - 'color' : self.inputs.linecolor.get(), - 'width' : self.inputs.linewidth.get() + 'pen' : { + 'style' : self.inputs.penstyle.get(), + 'color' : self.inputs.pencolor.get(), + 'width' : self.inputs.penwidth.get() } } self.outputs.plotitem.set({}) diff --git a/pycinema/filters/PlotScatterItem.py b/pycinema/filters/PlotScatterItem.py new file mode 100644 index 00000000..18bf5c68 --- /dev/null +++ b/pycinema/filters/PlotScatterItem.py @@ -0,0 +1,56 @@ +from .PlotItem import * + +import numpy as np + +# +# PlotScatterItem +# +# To be paired with a plot view +# Question: should this be a filter, or some new thing? +# Doesn't seem to fit the design of a view or filter +# +class PlotScatterItem(PlotItem): + + def __init__(self): + super().__init__( + inputs={ + 'table' : None, + 'x' : 'none', + 'y' : 'none', + 'pencolor' : 'default', + 'penwidth' : 1.0, + 'brushcolor': 'default', + 'symbol' : 'x', + 'size' : 1.0 + }, + outputs={ + 'plotitem' : {} + } + ) + + def _update(self): + xdata = self._getColumnFromTable(self.inputs.x.get()) + ydata = self._getColumnFromTable(self.inputs.y.get()) + + out = { 'x' : { + 'label' : self.inputs.x.get(), + 'data' : xdata + }, + 'y' : { + 'label' : self.inputs.y.get(), + 'data' : ydata + }, + 'pen' : { + 'color' : self.inputs.pencolor.get(), + 'width' : self.inputs.penwidth.get(), + }, + 'brush' : { + 'color' : self.inputs.brushcolor.get() + }, + 'symbol': self.inputs.symbol.get(), + 'size' : self.inputs.size.get() + } + self.outputs.plotitem.set({}) + self.outputs.plotitem.set(out) + + return 1 diff --git a/pycinema/filters/__init__.py b/pycinema/filters/__init__.py index d35df004..e6720e8b 100644 --- a/pycinema/filters/__init__.py +++ b/pycinema/filters/__init__.py @@ -2,6 +2,7 @@ from .CinemaDatabaseWriter import * from .ColorMapping import * from .ColorSource import * +from .CSVReader import * from .TableQuery import * from .TableEditor import * from .DepthCompositing import * @@ -11,7 +12,12 @@ from .ImageConvertGrayscale import * from .ImageFilterPIL import * from .ImageReader import * +from .ImageMetadataToScatterItem import * from .MaskCompositing import * +from .PlotItem import * +from .PlotBarItem import * +from .PlotLineItem import * +from .PlotScatterItem import * from .ShaderDemoScene import * from .ShaderFXAA import * from .ShaderIBS import * diff --git a/pycinema/theater/views/PlotBarView.py b/pycinema/theater/views/PlotBarView.py index 440877d6..78e26123 100644 --- a/pycinema/theater/views/PlotBarView.py +++ b/pycinema/theater/views/PlotBarView.py @@ -38,13 +38,13 @@ def _update(self): # for now, there is only one item, but there will be a list in the future item = self.inputs.plotitem.get() - # color - barcolor = item['bar']['color'] - if barcolor == 'default': - barcolor = 'black' + # brush + brushcolor = item['brush']['color'] + if brushcolor == 'default': + brushcolor = 'black' # graph item - bgItem = pg.BarGraphItem(x=item['x']['data'], height=item['y']['data'], width=item['bar']['width'], brush=barcolor) + bgItem = pg.BarGraphItem(x=item['x']['data'], height=item['y']['data'], width=item['width'], brush=brushcolor) # set up the plot self.plot.setBackground(self.inputs.background.get()) diff --git a/pycinema/theater/views/PlotLineView.py b/pycinema/theater/views/PlotLineView.py index 67e6b7e1..9353b358 100644 --- a/pycinema/theater/views/PlotLineView.py +++ b/pycinema/theater/views/PlotLineView.py @@ -47,16 +47,16 @@ def _update(self): item = self.inputs.plotitem.get() # pen - pencolor = item['line']['color'] + pencolor = item['pen']['color'] if pencolor == 'default': pencolor = 'black' - newpen = pg.mkPen(color = pencolor, style=self.PenStyles[item['line']['type']],width=item['line']['width']) + itempen = pg.mkPen(color = pencolor, style=self.PenStyles[item['pen']['style']],width=item['pen']['width']) # set up the plot self.plot.setBackground(self.inputs.background.get()) self.plot.setTitle(self.inputs.title.get()) self.plot.setLabel("left", item['y']['label']) self.plot.setLabel("bottom", item['x']['label']) - self.plot.plot(item['x']['data'], item['y']['data'], pen = newpen) + self.plot.plot(item['x']['data'], item['y']['data'], pen = itempen) return 1 diff --git a/pycinema/theater/views/PlotScatterView.py b/pycinema/theater/views/PlotScatterView.py new file mode 100644 index 00000000..131e7828 --- /dev/null +++ b/pycinema/theater/views/PlotScatterView.py @@ -0,0 +1,62 @@ +from pycinema import Filter + +from PySide6 import QtCore, QtWidgets, QtGui + +from .FilterView import FilterView + +import numpy as np +import pyqtgraph as pg + +class PlotScatterView(Filter, FilterView): + + def __init__(self): + FilterView.__init__( + self, + filter=self, + delete_filter_on_close = True + ) + + Filter.__init__( + self, + inputs={ + 'title' : 'Plot Title', + 'background': 'white', + 'plotitem' : 'none' + } + ) + + def generateWidgets(self): + self.plot = pg.PlotWidget() + self.content.layout().addWidget(self.plot) + + def _update(self): + # clear on update + self.plot.clear() + + # get plot items + # for now, there is only one item, but there will be a list in the future + item = self.inputs.plotitem.get() + + # pen + pencolor = item['pen']['color'] + if pencolor == 'default': + pencolor = 'black' + itempen = pg.mkPen(width=item['pen']['width'],color=pencolor) + + # brush + brushcolor = item['brush']['color'] + if brushcolor == 'default': + brushcolor = 'black' + itembrush = pg.mkBrush(color=brushcolor) + + # graph item + plotItem = pg.ScatterPlotItem(x=item['x']['data'], y=item['y']['data'], pen=itempen, brush=itembrush, symbol=item['symbol'], size=item['size']) + + # set up the plot + self.plot.setBackground(self.inputs.background.get()) + self.plot.setTitle(self.inputs.title.get()) + self.plot.setLabel("left", item['y']['label']) + self.plot.setLabel("bottom", item['x']['label']) + self.plot.addItem(plotItem) + + return 1 diff --git a/pycinema/theater/views/__init__.py b/pycinema/theater/views/__init__.py index b8a437c8..2fa95aed 100644 --- a/pycinema/theater/views/__init__.py +++ b/pycinema/theater/views/__init__.py @@ -9,3 +9,4 @@ from .InspectorView import InspectorView from .PlotBarView import PlotBarView from .PlotLineView import PlotLineView +from .PlotScatterView import PlotScatterView diff --git a/setup.py b/setup.py index a73f73ea..4c94718c 100644 --- a/setup.py +++ b/setup.py @@ -32,8 +32,8 @@ "ipywidgets==8.0.6", "PySide6>=6.0.0", "python-igraph>=0.10.5", - "pyqtgraph", - "requests" + "requests>=2.31.0", + "pyqtgraph>=0.13.3" ], classifiers=[ "Programming Language :: Python :: 3",