Skip to content

Commit

Permalink
implemented View to code function (view.toAltair and view.toVegaLite)
Browse files Browse the repository at this point in the history
  • Loading branch information
dorisjlee committed Jun 4, 2020
1 parent 8fcf54d commit 01e9e56
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 69 deletions.
22 changes: 6 additions & 16 deletions lux/luxDataFrame/LuxDataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,18 +323,19 @@ def showMore(self):
def getWidget(self):
return self.widget

def getExported(self):
def getExported(self) -> typing.Union[typing.Dict[str,ViewCollection], ViewCollection]:
"""
Convert the _exportedVisIdxs dictionary into a programmable ViewCollection
Example _exportedVisIdxs :
{'Correlation': [0, 2], 'Category': [1]}
indicating the 0th and 2nd vis from the `Correlation` tab is selected, and the 1st vis from the `Category` tab is selected.
Returns
-------
When there are no exported vis, return empty list -> []
When all the exported vis is from the same tab, return a ViewCollection of selected views. -> ViewCollection(v1, v2...)
When the exported vis is from the different tabs, return a dictionary with the action name as key and selected views in the ViewCollection. -> {"Enhance": ViewCollection(v1, v2...), "Filter": ViewCollection(v5, v7...), ..}
typing.Union[typing.Dict[str,ViewCollection], ViewCollection]
When there are no exported vis, return empty list -> []
When all the exported vis is from the same tab, return a ViewCollection of selected views. -> ViewCollection(v1, v2...)
When the exported vis is from the different tabs, return a dictionary with the action name as key and selected views in the ViewCollection. -> {"Enhance": ViewCollection(v1, v2...), "Filter": ViewCollection(v5, v7...), ..}
"""
exportedVisLst =self.widget._exportedVisIdxs
exportedViews = []
Expand Down Expand Up @@ -444,17 +445,6 @@ def currentViewToJSON(vc, inputCurrentView=""):
currentViewSpec = vc[0].renderVSpec()
elif (numVC>1):
pass
# This behavior is jarring to user, so comment out for now
# # if the compiled object is a collection, see if we can remove the elements with "?" and generate a Current View
# specifiedDobj = currentViewDobj.getVariableFieldsRemoved()
# if (specifiedDobj.spec!=[]): specifiedDobj.compile(enumerateCollection=False)
# if (currentView!=""):
# currentViewSpec = currentView.compiled.renderVSpec()
# elif (specifiedDobj.isEmpty()):
# currentViewSpec = {}
# else:
# specifiedDobj.compile(enumerateCollection=False)
# currentViewSpec = specifiedDobj.compiled.renderVSpec()
return currentViewSpec
@staticmethod
def recToJSON(recs):
Expand Down
33 changes: 27 additions & 6 deletions lux/view/View.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,34 @@ def removeColumnFromSpecNew(self, attribute):
else:
newSpec.append(self.specLst[i])
self.specLst = newSpec
def toAltair(self) -> str:
"""
Generate minimal Altair code to visualize the view
def renderVSpec(self, renderer="altair"):
Returns
-------
str
String version of the Altair code. Need to print out the string to apply formatting.
"""
from lux.vizLib.altair.AltairRenderer import AltairRenderer
if (renderer == "altair"):
renderer = AltairRenderer()
renderer = AltairRenderer(outputType="Altair")
self.vis= renderer.createVis(self)
return self.vis
'''
Possibly add more helper functions for retrieving information fro specified SpecLst
'''

def toVegaLite(self) -> dict:
"""
Generate minimal VegaLite code to visualize the view
Returns
-------
dict
Dictionary of the VegaLite JSON specification
"""
from lux.vizLib.altair.AltairRenderer import AltairRenderer
renderer = AltairRenderer(outputType="VegaLite")
self.vis= renderer.createVis(self)
return self.vis

def renderVSpec(self, renderer="altair"):
if (renderer == "altair"):
return self.toVegaLite()
16 changes: 9 additions & 7 deletions lux/vizLib/altair/AltairChart.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ def __init__(self, view):
# self.data = data.cars.url
# self.data = "chartData"
self.data = view.data
# print(type(self.data),"test")
self.tooltip = True
# ----- START self.code modification -----
self.code = ""
self.chart = self.initializeChart()
self.code = self.getChartCode()
# self.addTooltip()
self.encodeColor()
self.addTitle()
self.code +="\nchart"
self.code = self.code.replace('\n\t\t','\n')
# ----- END self.code modification -----
def __repr__(self):
return f"AltairChart <{str(self.view)}>"
def addTooltip(self):
Expand All @@ -33,18 +36,17 @@ def encodeColor(self):
colorAttr = self.view.getAttrByChannel("color")
if (len(colorAttr)==1):
self.chart = self.chart.encode(color=alt.Color(colorAttr[0].attribute,type=colorAttr[0].dataType))
self.code+=f"chart = chart.encode(color=alt.Color('{colorAttr[0].attribute}',type='{colorAttr[0].dataType}'))"
elif (len(colorAttr)>1):
raise ValueError("There should not be more than one attribute specified in the same channel.")

def addTitle(self):
chartTitle = self.view.title
if chartTitle:
self.chart = self.chart.encode().properties(
title = chartTitle
)
if (self.code!=""):
self.code+=f"chart = chart.encode().properties(title = '{chartTitle}')"
def initializeChart(self):
return NotImplemented
def getChartCode(self):
return '''
import altair as alt
# Altair code placeholder
'''
22 changes: 12 additions & 10 deletions lux/vizLib/altair/AltairRenderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

class AltairRenderer:
"""
Renderer for Altair Charts
Renderer for Charts based on Altair (https://altair-viz.github.io/)
"""
def __init__(self):
pass
def __init__(self,outputType="VegaLite"):
self.outputType = outputType
def __repr__(self):
return f"AltairRenderer"
def createVis(self,view):
Expand Down Expand Up @@ -37,10 +37,12 @@ def createVis(self,view):
else:
chart = None
if (chart):
chartDict = chart.chart.to_dict()
# this is a bit of a work around because altair must take a pandas dataframe and we can only generate a luxDataFrame
# chart["data"] = { "values": view.data.to_dict(orient='records') }
chartDict["width"] = 160
chartDict["height"] = 150
chartDict["code"] = chart.code
return chartDict
if (self.outputType=="VegaLite"):
chartDict = chart.chart.to_dict()
# this is a bit of a work around because altair must take a pandas dataframe and we can only generate a luxDataFrame
# chart["data"] = { "values": view.data.to_dict(orient='records') }
chartDict["width"] = 160
chartDict["height"] = 150
return chartDict
elif (self.outputType=="Altair"):
return chart.code
39 changes: 28 additions & 11 deletions lux/vizLib/altair/BarChart.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,44 @@ def initializeChart(self):
xAttr = self.view.getAttrByChannel("x")[0]
yAttr = self.view.getAttrByChannel("y")[0]

self.code += "import altair as alt\n"
# self.code += f"viewData = pd.DataFrame({str(self.data.to_dict(orient='records'))})\n"
self.code += f"viewData = pd.DataFrame({str(self.data.to_dict())})\n"

if (xAttr.dataModel == "measure"):
yAttrField = alt.Y(yAttr.attribute, type = yAttr.dataType, axis=alt.Axis(labelOverlap=True))
xAttrField = alt.X(xAttr.attribute, type=xAttr.dataType,title=f"{xAttr.aggregation.capitalize()} of {xAttr.attribute}")
aggTitle = f'{xAttr.aggregation.capitalize()} of {xAttr.attribute}'
yAttrField = alt.Y(yAttr.attribute, type= yAttr.dataType, axis=alt.Axis(labelOverlap=True))
xAttrField = alt.X(xAttr.attribute, type= xAttr.dataType, title=aggTitle)
yAttrFieldCode = f"alt.Y('{yAttr.attribute}', type= '{yAttr.dataType}', axis=alt.Axis(labelOverlap=True))"
xAttrFieldCode = f"alt.X('{xAttr.attribute}', type= '{xAttr.dataType}', title='{aggTitle}')"

if (yAttr.sort=="ascending"):
yAttrField.sort="-x"
yAttrFieldCode = f"alt.Y('{yAttr.attribute}', type= '{yAttr.dataType}', axis=alt.Axis(labelOverlap=True), sort ='-x')"
else:
aggTitle = f"{yAttr.aggregation.capitalize()} of {yAttr.attribute}"
xAttrField = alt.X(xAttr.attribute, type = xAttr.dataType,axis=alt.Axis(labelOverlap=True))
yAttrField = alt.Y(yAttr.attribute,type=yAttr.dataType,title=aggTitle)
xAttrFieldCode = f"alt.X('{xAttr.attribute}', type= '{xAttr.dataType}', axis=alt.Axis(labelOverlap=True))"
yAttrFieldCode = f"alt.Y('{yAttr.attribute}', type= '{yAttr.dataType}', title='{aggTitle}')"
if (xAttr.sort=="ascending"):
xAttrField.sort="-y"
yAttrField = alt.Y(yAttr.attribute,type=yAttr.dataType,title=f"{yAttr.aggregation.capitalize()} of {yAttr.attribute}")
xAttrFieldCode = f"alt.X('{xAttr.attribute}', type= '{xAttr.dataType}', axis=alt.Axis(labelOverlap=True),sort='-y')"

chart = alt.Chart(self.data).mark_bar().encode(
y = yAttrField,
x = xAttrField
)
# TODO: tooltip messes up the count() bar charts
chart = chart.configure_mark(tooltip=alt.TooltipContent('encoding')) # Setting tooltip as non-null
# TODO: tooltip messes up the count() bar charts
# Can not do interactive whenever you have default count measure otherwise output strange error (Javascript Error: Cannot read property 'length' of undefined)
#chart = chart.interactive() # If you want to enable Zooming and Panning
return chart
def getChartCode(self):
return '''
import altair as alt
# Altair code placeholder
'''
chart = chart.configure_mark(tooltip=alt.TooltipContent('encoding')) # Setting tooltip as non-null

self.code += f'''
chart = alt.Chart(viewData).mark_bar(size=12).encode(
y = {yAttrFieldCode},
x = {xAttrFieldCode},
)
chart = chart.configure_mark(tooltip=alt.TooltipContent('encoding')) # Setting tooltip as non-null
'''
return chart
23 changes: 22 additions & 1 deletion lux/vizLib/altair/Histogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,30 @@ def initializeChart(self):
alt.X(msrAttr.attribute, title=f'{msrAttr.attribute} (binned)',bin=alt.Bin(binned=True), type=msrAttr.dataType, axis=alt.Axis(labelOverlap=True), scale=alt.Scale(domain=(xMin, xMax))),
alt.Y("Count of Records", type="quantitative")
)
else:
elif (measure.channel=="y"):
chart = alt.Chart(self.data).mark_bar(size=12).encode(
x = alt.X("Count of Records", type="quantitative"),
y = alt.Y(msrAttr.attribute, title=f'{msrAttr.attribute} (binned)', bin=alt.Bin(binned=True), axis=alt.Axis(labelOverlap=True), scale=alt.Scale(domain=(xMin, xMax)))
)
#####################################
## Constructing Altair Code String ##
#####################################

self.code += "import altair as alt\n"
# self.code += f"viewData = pd.DataFrame({str(self.data.to_dict(orient='records'))})\n"
self.code += f"viewData = pd.DataFrame({str(self.data.to_dict())})\n"
if (measure.channel=="x"):
self.code += f'''
chart = alt.Chart(viewData).mark_bar(size=12).encode(
alt.X('{msrAttr.attribute}', title=f'{msrAttr.attribute} (binned)',bin=alt.Bin(binned=True), type='{msrAttr.dataType}', axis=alt.Axis(labelOverlap=True), scale=alt.Scale(domain=({xMin}, {xMax}))),
alt.Y("Count of Records", type="quantitative")
)
'''
elif (measure.channel=="y"):
self.code += f'''
chart = alt.Chart(viewData).mark_bar(size=12).encode(
alt.Y('{msrAttr.attribute}', title=f'{msrAttr.attribute} (binned)',bin=alt.Bin(binned=True), type='{msrAttr.dataType}', axis=alt.Axis(labelOverlap=True), scale=alt.Scale(domain=({xMin}, {xMax}))),
alt.X("Count of Records", type="quantitative")
)
'''
return chart
29 changes: 23 additions & 6 deletions lux/vizLib/altair/LineChart.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,37 @@ def initializeChart(self):
self.tooltip = False # tooltip looks weird for line chart
xAttr = self.view.getAttrByChannel("x")[0]
yAttr = self.view.getAttrByChannel("y")[0]


self.code += "import altair as alt\n"
self.code += "import pandas._libs.tslibs.timestamps\n"
self.code += "from pandas._libs.tslibs.timestamps import Timestamp\n"
self.code += f"viewData = pd.DataFrame({str(self.data.to_dict())})\n"

if (yAttr.dataModel == "measure"):
aggTitle = f"{yAttr.aggregation.capitalize()} of {yAttr.attribute}"
xAttrSpec = alt.X(xAttr.attribute, type = xAttr.dataType)
yAttrSpec = alt.Y(yAttr.attribute,type= yAttr.dataType, title=f"{yAttr.aggregation.capitalize()} of {yAttr.attribute}")
yAttrSpec = alt.Y(yAttr.attribute, type= yAttr.dataType, title=aggTitle)
xAttrFieldCode = f"alt.X('{xAttr.attribute}', type = '{xAttr.dataType}')"
yAttrFieldCode = f"alt.Y('{yAttr.attribute}', type= '{yAttr.dataType}', title='{aggTitle}')"
else:
xAttrSpec = alt.X(xAttr.attribute,type= xAttr.dataType, title=f"{xAttr.aggregation.capitalize()} of {xAttr.attribute}")
aggTitle = f"{xAttr.aggregation.capitalize()} of {xAttr.attribute}"
xAttrSpec = alt.X(xAttr.attribute,type= xAttr.dataType, title=aggTitle)
yAttrSpec = alt.Y(yAttr.attribute, type = yAttr.dataType)
# if (yAttr.attribute=="count()"):
# yAttrSpec = alt.Y("Record",type="quantitative", aggregate="count")
xAttrFieldCode = f"alt.X('{xAttr.attribute}', type = '{xAttr.dataType}', title='{aggTitle}')"
yAttrFieldCode = f"alt.Y('{yAttr.attribute}', type= '{yAttr.dataType}')"

chart = alt.Chart(self.data).mark_line().encode(
x = xAttrSpec,
# TODO: need to change aggregate to non-default function, read aggFunc info in somewhere
y = yAttrSpec
)
chart = chart.interactive() # If you want to enable Zooming and Panning
chart = chart.interactive() # Enable Zooming and Panning
self.code += f'''
chart = alt.Chart(viewData).mark_line().encode(
y = {yAttrFieldCode},
x = {xAttrFieldCode},
)
chart = chart.interactive() # Enable Zooming and Panning
'''
return chart

22 changes: 10 additions & 12 deletions lux/vizLib/altair/ScatterChart.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,19 @@ def initializeChart(self):
)
chart = chart.configure_mark(tooltip=alt.TooltipContent('encoding')) # Setting tooltip as non-null
chart = chart.interactive() # Enable Zooming and Panning
return chart
def getChartCode(self):
chartCode = ""
chartCode += "import altair as alt\n"
dfname = "df" #Placeholder (need to read dynamically via locals())

xAttr = self.view.getAttrByChannel("x")[0]
yAttr = self.view.getAttrByChannel("y")[0]
chartCode += f'''
#####################################
## Constructing Altair Code String ##
#####################################

self.code += "import altair as alt\n"
dfname = "df" # TODO: Placeholder (need to read dynamically via locals())
self.code += f'''
chart = alt.Chart({dfname}).mark_circle().encode(
x=alt.X('{xAttr.attribute}',scale=alt.Scale(zero=False),type='{xAttr.dataType}'),
y=alt.Y('{yAttr.attribute}',scale=alt.Scale(zero=False),type='{yAttr.dataType}')
x=alt.X('{xAttr.attribute}',scale=alt.Scale(domain=({xMin}, {xMax})),type='{xAttr.dataType}'),
y=alt.Y('{yAttr.attribute}',scale=alt.Scale(domain=({yMin}, {yMax})),type='{yAttr.dataType}')
)
chart = chart.configure_mark(tooltip=alt.TooltipContent('encoding')) # Setting tooltip as non-null
chart = chart.interactive() # Enable Zooming and Panning
'''
chartCode = chartCode.replace('\n\t\t','\n')
return chartCode
return chart

0 comments on commit 01e9e56

Please sign in to comment.