diff --git a/examples/README.md b/examples/README.md index 28d5eaf..816fc9a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,4 +1,2 @@ -## Running the examples -Install subgrounds with your favorite Python dependency manager. E.g.: `pip install subgrounds`. - -Run the examples, e.g.: `python bar_chart.py` \ No newline at end of file +# Examples +> See [our docs](https://docs.playgrounds.network/examples/) for more examples! diff --git a/examples/aave_v2.py b/examples/aave_v2.py new file mode 100644 index 0000000..c94169c --- /dev/null +++ b/examples/aave_v2.py @@ -0,0 +1,20 @@ +from subgrounds import Subgrounds + +sg = Subgrounds() + +# Load +aave_v2 = sg.load_subgraph( + "https://api.thegraph.com/subgraphs/name/messari/aave-v2-ethereum" +) + +# Construct the query +latest = aave_v2.Query.markets( + orderBy=aave_v2.Market.totalValueLockedUSD, + orderDirection="desc", + first=5, +) + +# Return query to a dataframe +df = sg.query_df([latest.name, latest.totalValueLockedUSD]) + +print(df) diff --git a/examples/curve.py b/examples/curve.py new file mode 100644 index 0000000..7d1baa9 --- /dev/null +++ b/examples/curve.py @@ -0,0 +1,36 @@ +from subgrounds import Subgrounds + +sg = Subgrounds() + +curve = sg.load_subgraph( + "https://api.thegraph.com/subgraphs/name/messari/curve-finance-ethereum" +) + +# Partial FieldPath selecting the top 4 most traded pools on Curve +most_traded_pools = curve.Query.liquidityPools( + orderBy=curve.LiquidityPool.cumulativeVolumeUSD, + orderDirection="desc", + first=4, +) + +# Partial FieldPath selecting the top 2 pools by daily total revenue of +# the top 4 most traded tokens. +# Mote that reuse of `most_traded_pools` in the partial FieldPath +most_traded_snapshots = most_traded_pools.dailySnapshots( + orderBy=curve.LiquidityPoolDailySnapshot.dailyTotalRevenue, + orderDirection="desc", + first=3, +) + +# Querying: +# - the name of the top 4 most traded pools, their 2 most liquid +# pools' token symbols and their 2 most liquid pool's TVL in USD +df = sg.query_df( + [ + most_traded_pools.name, + most_traded_snapshots.dailyVolumeUSD, + most_traded_snapshots.dailyTotalRevenueUSD, + ] +) + +print(df) diff --git a/examples/dash_apps/README.md b/examples/dash_apps/README.md new file mode 100644 index 0000000..f41f6e7 --- /dev/null +++ b/examples/dash_apps/README.md @@ -0,0 +1,8 @@ +# Dash Examples + +## Instructions + +```bash +pip install "subgrounds[dash]" +python bar_chart.py +``` diff --git a/examples/bar_chart.py b/examples/dash_apps/bar_chart.py similarity index 90% rename from examples/bar_chart.py rename to examples/dash_apps/bar_chart.py index 5373115..ca07fdf 100644 --- a/examples/bar_chart.py +++ b/examples/dash_apps/bar_chart.py @@ -1,9 +1,9 @@ import dash from dash import html -from subgrounds.dash_wrappers import Graph -from subgrounds.plotly_wrappers import Bar, Figure -from subgrounds.subgrounds import Subgrounds +from subgrounds import Subgrounds +from subgrounds.contrib.dash import Graph +from subgrounds.contrib.plotly import Bar, Figure sg = Subgrounds() aaveV2 = sg.load_subgraph("https://api.thegraph.com/subgraphs/name/aave/protocol-v2") diff --git a/examples/dashboard.py b/examples/dash_apps/dashboard.py similarity index 95% rename from examples/dashboard.py rename to examples/dash_apps/dashboard.py index b0cd812..74279b4 100644 --- a/examples/dashboard.py +++ b/examples/dash_apps/dashboard.py @@ -1,11 +1,9 @@ import dash from dash import html -# from subgrounds.components import AutoUpdate, BarChart, IndicatorWithChange -# from subgrounds.subgraph import Subgraph -from subgrounds.dash_wrappers import AutoUpdate, Graph -from subgrounds.plotly_wrappers import Bar, Figure, Indicator -from subgrounds.subgrounds import Subgrounds +from subgrounds.contrib.dash import AutoUpdate, Graph +from subgrounds.contrib.plotly import Bar, Figure, Indicator +from subgrounds import Subgrounds sg = Subgrounds() uniswapV2 = sg.load_subgraph( diff --git a/examples/indicator.py b/examples/dash_apps/indicator.py similarity index 87% rename from examples/indicator.py rename to examples/dash_apps/indicator.py index 5165c9c..db42d90 100644 --- a/examples/indicator.py +++ b/examples/dash_apps/indicator.py @@ -1,9 +1,9 @@ import dash from dash import html -from subgrounds.dash_wrappers import Graph -from subgrounds.plotly_wrappers import Figure, Indicator -from subgrounds.subgrounds import Subgrounds +from subgrounds import Subgrounds +from subgrounds.contrib.dash import Graph +from subgrounds.contrib.plotly import Figure, Indicator sg = Subgrounds() uniswapV2 = sg.load_subgraph( diff --git a/examples/live_indicator.py b/examples/dash_apps/live_indicator.py similarity index 84% rename from examples/live_indicator.py rename to examples/dash_apps/live_indicator.py index 260c773..7407d12 100644 --- a/examples/live_indicator.py +++ b/examples/dash_apps/live_indicator.py @@ -1,9 +1,9 @@ import dash from dash import html -from subgrounds.dash_wrappers import AutoUpdate, Graph -from subgrounds.plotly_wrappers import Figure, Indicator -from subgrounds.subgrounds import Subgrounds +from subgrounds.contrib.dash import AutoUpdate, Graph +from subgrounds.contrib.plotly import Figure, Indicator +from subgrounds import Subgrounds sg = Subgrounds() uniswapV2 = sg.load_subgraph( diff --git a/examples/olympus_dashboard.py b/examples/dash_apps/olympus_dashboard.py similarity index 98% rename from examples/olympus_dashboard.py rename to examples/dash_apps/olympus_dashboard.py index 17641d2..3e3d28e 100644 --- a/examples/olympus_dashboard.py +++ b/examples/dash_apps/olympus_dashboard.py @@ -3,11 +3,9 @@ import dash from dash import html -from subgrounds.dash_wrappers import Graph -from subgrounds.plotly_wrappers import Figure, Indicator, Scatter -from subgrounds.schema import TypeRef -from subgrounds.subgraph import SyntheticField -from subgrounds.subgrounds import Subgrounds +from subgrounds import Subgrounds, SyntheticField +from subgrounds.contrib.dash import Graph +from subgrounds.contrib.plotly import Figure, Indicator, Scatter sg = Subgrounds() olympusDAO = sg.load_subgraph( diff --git a/examples/olympus_voting.py b/examples/dash_apps/olympus_voting.py similarity index 93% rename from examples/olympus_voting.py rename to examples/dash_apps/olympus_voting.py index bdd28bd..78ef71c 100644 --- a/examples/olympus_voting.py +++ b/examples/dash_apps/olympus_voting.py @@ -1,14 +1,11 @@ from datetime import datetime -from random import choice import dash from dash import html -from subgrounds.dash_wrappers import Graph -from subgrounds.plotly_wrappers import Figure, Indicator, Scatter -from subgrounds.schema import TypeRef -from subgrounds.subgraph import SyntheticField -from subgrounds.subgrounds import Subgrounds +from subgrounds import Subgrounds, SyntheticField +from subgrounds.contrib.dash import Graph +from subgrounds.contrib.plotly import Figure, Scatter sg = Subgrounds() olympusDAO = sg.load_subgraph( diff --git a/examples/synthetic_fields.py b/examples/dash_apps/synthetic_fields.py similarity index 86% rename from examples/synthetic_fields.py rename to examples/dash_apps/synthetic_fields.py index 74a828e..af2f08c 100644 --- a/examples/synthetic_fields.py +++ b/examples/dash_apps/synthetic_fields.py @@ -3,11 +3,10 @@ import dash from dash import html -from subgrounds.dash_wrappers import Graph -from subgrounds.plotly_wrappers import Figure, Scatter +from subgrounds import Subgrounds, SyntheticField +from subgrounds.contrib.dash import Graph +from subgrounds.contrib.plotly import Figure, Scatter from subgrounds.schema import TypeRef -from subgrounds.subgraph import SyntheticField -from subgrounds.subgrounds import Subgrounds sg = Subgrounds() uniswapV2 = sg.load_subgraph( diff --git a/examples/table.py b/examples/dash_apps/table.py similarity index 94% rename from examples/table.py rename to examples/dash_apps/table.py index 0a401ce..a7d50a7 100644 --- a/examples/table.py +++ b/examples/dash_apps/table.py @@ -1,9 +1,8 @@ import dash from dash import html -from subgrounds.dash_wrappers import DataTable -from subgrounds.plotly_wrappers import Bar, Figure -from subgrounds.subgrounds import Subgrounds +from subgrounds import Subgrounds +from subgrounds.contrib.dash import DataTable sg = Subgrounds() uniswapV2 = sg.load_subgraph( diff --git a/examples/uniswapv2_firehose.py b/examples/dash_apps/uniswapv2_firehose.py similarity index 94% rename from examples/uniswapv2_firehose.py rename to examples/dash_apps/uniswapv2_firehose.py index 3f1d223..e34e696 100644 --- a/examples/uniswapv2_firehose.py +++ b/examples/dash_apps/uniswapv2_firehose.py @@ -1,8 +1,8 @@ import dash from dash import html -from subgrounds.dash_wrappers import AutoUpdate, DataTable -from subgrounds.subgrounds import Subgrounds +from subgrounds import Subgrounds +from subgrounds.contrib.dash import AutoUpdate, DataTable sg = Subgrounds() uniswapV2 = sg.load_subgraph( diff --git a/examples/double_query.py b/examples/double_query.py deleted file mode 100644 index 7012615..0000000 --- a/examples/double_query.py +++ /dev/null @@ -1,78 +0,0 @@ -from datetime import datetime - -import dash -from dash import html - -from subgrounds.dash_wrappers import Graph -from subgrounds.plotly_wrappers import Figure, Scatter -from subgrounds.schema import TypeRef -from subgrounds.subgraph import SyntheticField -from subgrounds.subgrounds import Subgrounds - -sg = Subgrounds() -uniswapV2 = sg.load_subgraph( - "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2" -) - -# This is unecessary, but nice for brevity -Query = uniswapV2.Query -PairDayData = uniswapV2.PairDayData - -# This is a synthetic field - -PairDayData.exchange_rate = PairDayData.reserve0 / PairDayData.reserve1 - -# This is a synthetic field -PairDayData.datetime = SyntheticField( - lambda timestamp: str(datetime.fromtimestamp(timestamp)), - TypeRef.Named(name="String", kind="SCALAR"), - PairDayData.date, -) - -uni_eth = Query.pairDayDatas( - orderBy=PairDayData.date, - orderDirection="desc", - first=100, - where=[PairDayData.pairAddress == "0xd3d2e2692501a5c9ca623199d38826e513033a17"], -) - -toke_eth = Query.pairDayDatas( - orderBy=PairDayData.date, - orderDirection="desc", - first=100, - where=[PairDayData.pairAddress == "0x5fa464cefe8901d66c09b85d5fcdc55b3738c688"], -) - -# Dashboard -app = dash.Dash(__name__) - -app.layout = html.Div( - html.Div( - [ - html.Div( - [ - Graph( - Figure( - subgrounds=sg, - traces=[ - Scatter( - x=uni_eth.datetime, - y=uni_eth.exchange_rate, - mode="lines", - ), - Scatter( - x=toke_eth.datetime, - y=toke_eth.exchange_rate, - mode="lines", - ), - ], - ) - ) - ] - ) - ] - ) -) - -if __name__ == "__main__": - app.run_server(debug=True) diff --git a/examples/indicator2.py b/examples/indicator2.py deleted file mode 100644 index 4c6ef20..0000000 --- a/examples/indicator2.py +++ /dev/null @@ -1,39 +0,0 @@ -import dash -from dash import html - -from subgrounds.dash_wrappers import Graph -from subgrounds.plotly_wrappers import Figure, Indicator -from subgrounds.subgrounds import Subgrounds - -sg = Subgrounds() -uniswapV2 = sg.load_subgraph( - "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v2" -) - -# This is unecessary, but nice for brevity -pair = uniswapV2.Query.pair(id="0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc") - -# Dashboard -app = dash.Dash(__name__) - -app.layout = html.Div( - html.Div( - [ - html.Div( - [ - Graph( - Figure( - subgrounds=sg, - traces=[ - Indicator(value=pair.token0Price), - ], - ) - ) - ] - ) - ] - ) -) - -if __name__ == "__main__": - app.run_server(debug=True) diff --git a/pyproject.toml b/pyproject.toml index 7443c7e..871ddbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dash = ["dash"] # https://python-poetry.org/docs/managing-dependencies/#dependency-groups [tool.poetry.group.dev.dependencies] black = "^22.3.0" -deepdiff = "^6.2.1" # used for debugging data structures +deepdiff = "^6.2.1" # used for debugging data structures ipykernel = "^6.13.0" mypy = "^0.950" nose2 = "^0.11.0" @@ -36,8 +36,8 @@ python-semantic-release = "^7.33.1" ruff = "^0.0.253" [tool.poe.tasks] -format = { shell = "black subgrounds examples tests"} -check = { shell = "black subgrounds examples tests --check; ruff check subgrounds examples tests"} +format = { shell = "black subgrounds examples tests" } +check = { shell = "black subgrounds examples tests --check; ruff check subgrounds examples tests" } develop = { shell = "mudkip develop" } test = "pytest" generate-api-docs = { shell = "sphinx-apidoc --output docs/api subgrounds --separate --force" } @@ -50,6 +50,12 @@ version_toml = "pyproject.toml:tool.poetry.version" major_on_zero = false build_command = "poetry build" +[tool.ruff] + +[tool.ruff.per-file-ignores] +"subgrounds/plotly_wrappers" = ["F405", "F403"] +"subgrounds/dash_wrappers" = ["F405", "F403"] + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/subgrounds/contrib/README.md b/subgrounds/contrib/README.md new file mode 100644 index 0000000..9145395 --- /dev/null +++ b/subgrounds/contrib/README.md @@ -0,0 +1,17 @@ +# Subgrounds Contrib +> Extra parts of subgrounds that may not fit in the main package + +## What is this? +Contrib is a niche concept in some libraries that represent extra / contributed content to a library that may not fit in the main package. This might be due to the maintainer not willing to maintain said content, the content being deemed too experimental, or perhaps it's unknown whether it's a "good idea" or not. + +> Relevant [Stackoverflow](https://softwareengineering.stackexchange.com/questions/252053/whats-in-the-contrib-folder) post + +For us, `subgrounds.contrib` will represent extra features and ideas with `subgrounds` that generally builds upon the core part of `subgrounds`. It allows us to add extensions or features to other libraries (such as `plotly`) without *relying* on the library as a dependency. We might add new concepts to this subpackage in the future, so look out! + +## What's currently here? + +### Plotly +Originally located in `subgrounds.plotly_wrappers`, `subgrounds.contrib.plotly` contains helpful wrappers on `plotly` objects that allow you to use `FieldPaths` directly without creating a `pandas` `DataFrame`. + +### Dash +Originally located in `subgrounds.dash_wrappers`, `subgrounds.contrib.dash` contains helpful wrappers on `dash` objects that allow you to use other wrapped visualization objects (currently `subgrounds.contrib.plotly`) in `dash` apps without creating `pandas` `DataFrame`s. diff --git a/subgrounds/contrib/dash/__init__.py b/subgrounds/contrib/dash/__init__.py new file mode 100644 index 0000000..d479ad9 --- /dev/null +++ b/subgrounds/contrib/dash/__init__.py @@ -0,0 +1,15 @@ +"""Subgrounds Dash Components + +Extending dash components to be able to understand subgrounds logic. This includes other + extended components of other libraries such as `plotly`. +""" + +from .abcs import Refreshable +from .components import AutoUpdate, DataTable, Graph + +__all__ = [ + "Refreshable", + "Graph", + "DataTable", + "AutoUpdate", +] diff --git a/subgrounds/contrib/dash/abcs.py b/subgrounds/contrib/dash/abcs.py new file mode 100644 index 0000000..084b75e --- /dev/null +++ b/subgrounds/contrib/dash/abcs.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Any + +from dash.dependencies import Output + + +class Refreshable(ABC): + @property + @abstractmethod + def dash_dependencies(self) -> list[Output]: + raise NotImplementedError + + @property + @abstractmethod + def dash_dependencies_outputs(self) -> list[Any]: + raise NotImplementedError diff --git a/subgrounds/contrib/dash/components.py b/subgrounds/contrib/dash/components.py new file mode 100644 index 0000000..6a5ea9e --- /dev/null +++ b/subgrounds/contrib/dash/components.py @@ -0,0 +1,134 @@ +from __future__ import annotations + +from typing import Any, ClassVar + +import pandas as pd +from dash import dash_table, dcc, html +from dash.dependencies import Input, Output +from pipe import map, where + +from subgrounds import FieldPath, Subgrounds +from subgrounds.contrib.plotly import Figure + +from .abcs import Refreshable + + +class Graph(dcc.Graph, Refreshable): + counter: ClassVar[int] = 0 + wrapped_figure: Figure + + def __init__(self, fig: Figure, **kwargs) -> None: + super().__init__(id=f"graph-{Graph.counter}", figure=fig.figure, **kwargs) + Graph.counter += 1 + self.wrapped_figure = fig + + @property + def dash_dependencies(self) -> list[Output]: + return [Output(self.id, "figure")] + + @property + def dash_dependencies_outputs(self) -> list[Any]: + self.wrapped_figure.refresh() + return [self.wrapped_figure.figure] + + +class DataTable(dash_table.DataTable, Refreshable): + counter: ClassVar[int] = 0 + + subgrounds: Subgrounds + data: list[FieldPath] + columns: list[str] | None + concat: bool + append: bool + df: pd.DataFrame | list[pd.DataFrame] | None + + def __init__( + self, + subgrounds: Subgrounds, + data: FieldPath | list[FieldPath], + columns: list[str] | None = None, + concat: bool = False, + append: bool = False, + **kwargs, + ): + self.subgrounds = subgrounds + self.fpaths = data if type(data) == list else [data] + self.column_names = columns + self.concat = concat + self.append = append + self.df = None + + super().__init__(id=f"datatable-{DataTable.counter}", **kwargs) + DataTable.counter += 1 + + self.refresh() + + def refresh(self) -> None: + match (self.df, self.append): + case (None, _) | (_, False): + self.df = self.subgrounds.query_df( + self.fpaths, columns=self.column_names, concat=self.concat + ) + case (_, True): + self.df = pd.concat( + [ + self.df, + self.subgrounds.query_df( + self.fpaths, columns=self.column_names, concat=self.concat + ), + ], + ignore_index=True, + ) + self.df = self.df.drop_duplicates() + + self.columns = [{"name": i, "id": i} for i in self.df.columns] + self.data = self.df.to_dict("records") + + @property + def dash_dependencies(self) -> list[Output]: + return [Output(self.id, "data")] + + @property + def dash_dependencies_outputs(self) -> list[Output]: + self.refresh() + return [self.df.to_dict("records")] + + +class AutoUpdate(html.Div): + counter: ClassVar[int] = 0 + + def __init__(self, app, sec_interval: int = 1, children=[], **kwargs): + id = f"interval-{AutoUpdate.counter}" + + super().__init__( + children=[ + dcc.Interval(id=id, interval=sec_interval * 1000, n_intervals=0), + *children, + ], + **kwargs, + ) + + AutoUpdate.counter += 1 + + def flatten(input: list[Any]): + return [item for sublist in input for item in sublist] + + subgrounds_children = list( + children | where(lambda child: isinstance(child, Refreshable)) + ) + deps = flatten( + list(subgrounds_children | map(lambda child: child.dash_dependencies)) + ) + + def update(n): + outputs = flatten( + list( + subgrounds_children + | map(lambda child: child.dash_dependencies_outputs) + ) + ) + + return outputs[0] if len(outputs) == 1 else outputs + + # Register callback + app.callback(*deps, Input(id, "n_intervals"))(update) diff --git a/subgrounds/contrib/plotly/__init__.py b/subgrounds/contrib/plotly/__init__.py new file mode 100644 index 0000000..d724cc4 --- /dev/null +++ b/subgrounds/contrib/plotly/__init__.py @@ -0,0 +1,82 @@ +"""Subgrounds Plotly Components + +Extending plotly components to be able to understand subgrounds logic. +""" + +from .figure import Figure +from .traces import ( + Bar, + Barpolar, + Box, + Candlestick, + Carpet, + Choropleth, + Choroplethmapbox, + Contour, + Contourcarpet, + Densitymapbox, + Funnel, + Heatmap, + Histogram, + Histogram2d, + Histogram2dContour, + Icicle, + Indicator, + Ohlc, + Parcats, + Parcoords, + Pie, + Sankey, + Scatter, + Scatter3d, + Scattercarpet, + Scattergeo, + Scattermapbox, + Scatterpolar, + Sunburst, + Surface, + Table, + TraceWrapper, + Treemap, + Violin, + Waterfall, +) + +__all__ = [ + "Figure", + "TraceWrapper", + "Scatter", + "Pie", + "Bar", + "Heatmap", + "Contour", + "Table", + "Box", + "Violin", + "Histogram", + "Histogram2d", + "Histogram2dContour", + "Ohlc", + "Candlestick", + "Waterfall", + "Funnel", + "Indicator", + "Scatter3d", + "Surface", + "Scattergeo", + "Choropleth", + "Scattermapbox", + "Choroplethmapbox", + "Densitymapbox", + "Scatterpolar", + "Barpolar", + "Sunburst", + "Treemap", + "Icicle", + "Sankey", + "Parcoords", + "Parcats", + "Carpet", + "Scattercarpet", + "Contourcarpet", +] diff --git a/subgrounds/contrib/plotly/figure.py b/subgrounds/contrib/plotly/figure.py new file mode 100644 index 0000000..187dbda --- /dev/null +++ b/subgrounds/contrib/plotly/figure.py @@ -0,0 +1,52 @@ +from typing import Any + +import plotly.graph_objects as go +from pipe import map, traverse + +from subgrounds import Subgrounds +from subgrounds.query import DataRequest + +from .traces import TraceWrapper + + +class Figure: + subgrounds: Subgrounds + traces: list[TraceWrapper] + req: DataRequest | None + data: list[dict[str, Any]] | None + figure: go.Figure + + args: dict[str, Any] + + def __init__( + self, + subgrounds: Subgrounds, + traces: TraceWrapper | list[TraceWrapper], + **kwargs + ) -> None: + self.subgrounds = subgrounds + self.traces = list([traces] | traverse) + + traces = list(self.traces | map(lambda trace: trace.field_paths) | traverse) + + if len(traces) > 0: + self.req = self.subgrounds.mk_request(traces) + self.data = self.subgrounds.execute(self.req) + else: + self.req = None + self.data = None + + self.args = kwargs + self.refresh() + + def refresh(self) -> None: + # TODO: Modify this to support x/y in different documents + self.figure = go.Figure(**self.args) + + if self.req is None: + return + + self.data = self.subgrounds.execute(self.req) + + for trace in self.traces: + self.figure.add_trace(trace.mk_trace(self.data)) diff --git a/subgrounds/contrib/plotly/traces.py b/subgrounds/contrib/plotly/traces.py new file mode 100644 index 0000000..159359e --- /dev/null +++ b/subgrounds/contrib/plotly/traces.py @@ -0,0 +1,251 @@ +from abc import ABC +from typing import Any + +import plotly.graph_objects as go +from plotly.basedatatypes import BaseTraceType + +from subgrounds import FieldPath + + +class TraceWrapper(ABC): + graph_object: BaseTraceType + + fpaths: dict[str, FieldPath] + args: dict[str, Any] + + def __init__(self, **kwargs) -> None: + self.fpaths = {} + self.args = {} + + for key, arg in kwargs.items(): + match arg: + case FieldPath(): + self.fpaths[key] = arg + case _: + self.args[key] = arg + + def mk_trace(self, data: list[dict[str, Any]] | dict[str, Any]) -> BaseTraceType: + fpath_data = {} + for key, fpath in self.fpaths.items(): + item = fpath._extract_data(data) + + if type(item) == list and len(item) == 1: + fpath_data[key] = item[0] + else: + fpath_data[key] = item + + return self.graph_object(**(fpath_data | self.args)) # type: ignore + + @property + def field_paths(self) -> list[FieldPath]: + return [fpath for _, fpath in self.fpaths.items()] + + +# Simple +class Scatter(TraceWrapper): + """See https://plotly.com/python/line-and-scatter/""" + + graph_object = go.Scatter # type: ignore + + +class Pie(TraceWrapper): + """See https://plotly.com/python/pie-charts/""" + + graph_object = go.Pie # type: ignore + + +class Bar(TraceWrapper): + """See https://plotly.com/python/bar-charts/""" + + graph_object = go.Bar # type: ignore + + +class Heatmap(TraceWrapper): + """See https://plotly.com/python/heatmaps/""" + + graph_object = go.Heatmap # type: ignore + + +class Contour(TraceWrapper): + """See https://plotly.com/python/contour-plots/""" + + graph_object = go.Contour # type: ignore + + +class Table(TraceWrapper): + """See https://plotly.com/python/contour-plots/""" + + graph_object = go.Table # type: ignore + + +# Distributions +class Box(TraceWrapper): + """See https://plotly.com/python/box-plots/""" + + graph_object = go.Box # type: ignore + + +class Violin(TraceWrapper): + """See https://plotly.com/python/violin/""" + + graph_object = go.Violin # type: ignore + + +class Histogram(TraceWrapper): + """See https://plotly.com/python/histograms/""" + + graph_object = go.Histogram # type: ignore + + +class Histogram2d(TraceWrapper): + """See https://plotly.com/python/2D-Histogram/""" + + graph_object = go.Histogram2d # type: ignore + + +class Histogram2dContour(TraceWrapper): + """See https://plotly.com/python/2d-histogram-contour/""" + + graph_object = go.Histogram2dContour # type: ignore + + +# Finance +class Ohlc(TraceWrapper): + """See https://plotly.com/python/ohlc-charts/""" + + graph_object = go.Ohlc # type: ignore + + +class Candlestick(TraceWrapper): + """See https://plotly.com/python/candlestick-charts/""" + + graph_object = go.Candlestick # type: ignore + + +class Waterfall(TraceWrapper): + """See https://plotly.com/python/waterfall-charts/""" + + graph_object = go.Waterfall # type: ignore + + +class Funnel(TraceWrapper): + """See https://plotly.com/python/funnel-charts/""" + + graph_object = go.Funnel # type: ignore + + +class Indicator(TraceWrapper): + """See https://plotly.com/python/indicator/""" + + graph_object = go.Indicator # type: ignore + + +# 3d +class Scatter3d(TraceWrapper): + """See https://plotly.com/python/3d-scatter-plots/""" + + graph_object = go.Scatter3d # type: ignore + + +class Surface(TraceWrapper): + """See https://plotly.com/python/3d-surface-plots/""" + + graph_object = go.Surface # type: ignore + + +# Maps +class Scattergeo(TraceWrapper): + """See https://plotly.com/python/scatter-plots-on-maps/""" + + graph_object = go.Scattergeo # type: ignore + + +class Choropleth(TraceWrapper): + """See https://plotly.com/python/choropleth-maps/""" + + graph_object = go.Choropleth # type: ignore + + +class Scattermapbox(TraceWrapper): + """See https://plotly.com/python/scattermapbox/""" + + graph_object = go.Scattermapbox # type: ignore + + +class Choroplethmapbox(TraceWrapper): + """See https://plotly.com/python/mapbox-county-choropleth/""" + + graph_object = go.Choroplethmapbox # type: ignore + + +class Densitymapbox(TraceWrapper): + """See https://plotly.com/python/mapbox-density-heatmaps/""" + + graph_object = go.Densitymapbox # type: ignore + + +# Specialized +class Scatterpolar(TraceWrapper): + """See https://plotly.com/python/polar-chart/""" + + graph_object = go.Scatterpolar # type: ignore + + +class Barpolar(TraceWrapper): + """See https://plotly.com/python/wind-rose-charts/""" + + graph_object = go.Barpolar # type: ignore + + +class Sunburst(TraceWrapper): + """See https://plotly.com/python/sunburst-charts/""" + + graph_object = go.Sunburst # type: ignore + + +class Treemap(TraceWrapper): + """See https://plotly.com/python/treemaps/""" + + graph_object = go.Treemap # type: ignore + + +class Icicle(TraceWrapper): + """See https://plotly.com/python/icicle-charts/""" + + graph_object = go.Icicle # type: ignore + + +class Sankey(TraceWrapper): + """See https://plotly.com/python/sankey-diagram/""" + + graph_object = go.Sankey # type: ignore + + +class Parcoords(TraceWrapper): + """See https://plotly.com/python/parallel-coordinates-plot/""" + + graph_object = go.Parcoords # type: ignore + + +class Parcats(TraceWrapper): + """See https://plotly.com/python/parallel-categories-diagram/""" + + graph_object = go.Parcats # type: ignore + + +class Carpet(TraceWrapper): + """See https://plotly.com/python/carpet-plot/""" + + graph_object = go.Carpet # type: ignore + + +class Scattercarpet(TraceWrapper): + """See https://plotly.com/python/carpet-scatter/""" + + graph_object = go.Scattercarpet # type: ignore + + +class Contourcarpet(TraceWrapper): + """See https://plotly.com/python/carpet-contour/""" + + graph_object = go.Contourcarpet # type: ignore diff --git a/subgrounds/dash_wrappers.py b/subgrounds/dash_wrappers.py index 67c6b00..5f8f8d3 100644 --- a/subgrounds/dash_wrappers.py +++ b/subgrounds/dash_wrappers.py @@ -1,149 +1,21 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import Any, ClassVar, Optional - -import pandas as pd -from dash import dash_table, dcc, html -from dash.dependencies import Input, Output -from pipe import map, where - -from subgrounds.plotly_wrappers import Figure -from subgrounds.subgraph import FieldPath -from subgrounds.subgrounds import Subgrounds - - -class Refreshable(ABC): - @property - @abstractmethod - def dash_dependencies(self) -> list[Output]: - raise NotImplementedError - - @property - @abstractmethod - def dash_dependencies_outputs(self) -> list[Any]: - raise NotImplementedError - - -class Graph(dcc.Graph, Refreshable): - counter: ClassVar[int] = 0 - wrapped_figure: Figure - - def __init__(self, fig: Figure, **kwargs) -> None: - super().__init__(id=f"graph-{Graph.counter}", figure=fig.figure, **kwargs) - Graph.counter += 1 - self.wrapped_figure = fig - - @property - def dash_dependencies(self) -> list[Output]: - return [Output(self.id, "figure")] - - @property - def dash_dependencies_outputs(self) -> list[Any]: - self.wrapped_figure.refresh() - return [self.wrapped_figure.figure] - - -class DataTable(dash_table.DataTable, Refreshable): - counter: ClassVar[int] = 0 - - subgrounds: Subgrounds - data: list[FieldPath] - columns: Optional[list[str]] - concat: bool - append: bool - df: Optional[pd.DataFrame | list[pd.DataFrame]] - - def __init__( - self, - subgrounds: Subgrounds, - data: FieldPath | list[FieldPath], - columns: Optional[list[str]] = None, - concat: bool = False, - append: bool = False, - **kwargs, - ): - self.subgrounds = subgrounds - self.fpaths = data if type(data) == list else [data] - self.column_names = columns - self.concat = concat - self.append = append - self.df = None - - super().__init__(id=f"datatable-{DataTable.counter}", **kwargs) - DataTable.counter += 1 - - self.refresh() - - def refresh(self) -> None: - match (self.df, self.append): - case (None, _) | (_, False): - self.df = self.subgrounds.query_df( - self.fpaths, columns=self.column_names, concat=self.concat - ) - case (_, True): - self.df = pd.concat( - [ - self.df, - self.subgrounds.query_df( - self.fpaths, columns=self.column_names, concat=self.concat - ), - ], - ignore_index=True, - ) - self.df = self.df.drop_duplicates() - - self.columns = [{"name": i, "id": i} for i in self.df.columns] - self.data = self.df.to_dict("records") - - @property - def dash_dependencies(self) -> list[Output]: - return [Output(self.id, "data")] - - @property - def dash_dependencies_outputs(self) -> list[Output]: - self.refresh() - return [self.df.to_dict("records")] - - -class AutoUpdate(html.Div): - counter: ClassVar[int] = 0 - - def __init__(self, app, sec_interval: int = 1, children=[], **kwargs): - id = f"interval-{AutoUpdate.counter}" - - super().__init__( - children=[ - dcc.Interval(id=id, interval=sec_interval * 1000, n_intervals=0), - *children, - ], - **kwargs, - ) - - AutoUpdate.counter += 1 - - def flatten(l): - return [item for sublist in l for item in sublist] - - subgrounds_children = list( - children | where(lambda child: isinstance(child, Refreshable)) - ) - deps = flatten( - list(subgrounds_children | map(lambda child: child.dash_dependencies)) - ) - - def update(n): - outputs = flatten( - list( - subgrounds_children - | map(lambda child: child.dash_dependencies_outputs) - ) - ) - - if len(outputs) == 1: - return outputs[0] - else: - return outputs - - # Register callback - app.callback(*deps, Input(id, "n_intervals"))(update) +""" +DEPRECIATED: Use `subgrounds.contrib.dash` instead +""" + +import warnings + +from subgrounds.contrib.dash import AutoUpdate, DataTable, Graph, Refreshable + +__all__ = [ + "Refreshable", + "Graph", + "DataTable", + "AutoUpdate", +] + +warnings.warn( + "Importing from `subgrounds.plotly_wrappers` is deprecated." + " Use `subgrounds.contrib.plotly` instead.\n" + "Will be removed in a future version.", + DeprecationWarning, +) diff --git a/subgrounds/plotly_wrappers.py b/subgrounds/plotly_wrappers.py index 568d53a..cbd1a3f 100644 --- a/subgrounds/plotly_wrappers.py +++ b/subgrounds/plotly_wrappers.py @@ -1,301 +1,91 @@ -from abc import ABC -from typing import Any - -import plotly.graph_objects as go -from pipe import map, traverse -from plotly.basedatatypes import BaseTraceType - -from subgrounds.query import DataRequest -from subgrounds.subgraph import FieldPath -from subgrounds.subgrounds import Subgrounds - - -class TraceWrapper(ABC): - graph_object: BaseTraceType - - fpaths: dict[str, FieldPath] - args: dict[str, Any] - - def __init__(self, **kwargs) -> None: - self.fpaths = {} - self.args = {} - - for key, arg in kwargs.items(): - match arg: - case FieldPath(): - self.fpaths[key] = arg - case _: - self.args[key] = arg - - def mk_trace(self, data: list[dict[str, Any]] | dict[str, Any]) -> BaseTraceType: - fpath_data = {} - for key, fpath in self.fpaths.items(): - item = fpath._extract_data(data) - if type(item) == list and len(item) == 1: - fpath_data[key] = item[0] - else: - fpath_data[key] = item - - # print(f'mk_trace: {fpath_data}') - # for key, item in fpath_data.items(): - # print(f'{key}: {len(item)} datapoints') - - return self.graph_object(**(fpath_data | self.args)) - - @property - def field_paths(self) -> list[FieldPath]: - return [fpath for _, fpath in self.fpaths.items()] - - -# Simple -class Scatter(TraceWrapper): - """See https://plotly.com/python/line-and-scatter/""" - - graph_object = go.Scatter - - -class Pie(TraceWrapper): - """See https://plotly.com/python/pie-charts/""" - - graph_object = go.Pie - - -class Bar(TraceWrapper): - """See https://plotly.com/python/bar-charts/""" - - graph_object = go.Bar - - -class Heatmap(TraceWrapper): - """See https://plotly.com/python/heatmaps/""" - - graph_object = go.Heatmap - - -class Contour(TraceWrapper): - """See https://plotly.com/python/contour-plots/""" - - graph_object = go.Contour - - -class Table(TraceWrapper): - """See https://plotly.com/python/contour-plots/""" - - graph_object = go.Table - - -# Distributions -class Box(TraceWrapper): - """See https://plotly.com/python/box-plots/""" - - graph_object = go.Box - - -class Violin(TraceWrapper): - """See https://plotly.com/python/violin/""" - - graph_object = go.Violin - - -class Histogram(TraceWrapper): - """See https://plotly.com/python/histograms/""" - - graph_object = go.Histogram - - -class Histogram2d(TraceWrapper): - """See https://plotly.com/python/2D-Histogram/""" - - graph_object = go.Histogram2d - - -class Histogram2dContour(TraceWrapper): - """See https://plotly.com/python/2d-histogram-contour/""" - - graph_object = go.Histogram2dContour - - -# Finance -class Ohlc(TraceWrapper): - """See https://plotly.com/python/ohlc-charts/""" - - graph_object = go.Ohlc - - -class Candlestick(TraceWrapper): - """See https://plotly.com/python/candlestick-charts/""" - - graph_object = go.Candlestick - - -class Waterfall(TraceWrapper): - """See https://plotly.com/python/waterfall-charts/""" - - graph_object = go.Waterfall - - -class Funnel(TraceWrapper): - """See https://plotly.com/python/funnel-charts/""" - - graph_object = go.Funnel - - -class Indicator(TraceWrapper): - """See https://plotly.com/python/indicator/""" - - graph_object = go.Indicator - - -# 3d -class Scatter3d(TraceWrapper): - """See https://plotly.com/python/3d-scatter-plots/""" - - graph_object = go.Scatter3d - - -class Surface(TraceWrapper): - """See https://plotly.com/python/3d-surface-plots/""" - - graph_object = go.Surface - - -# Maps -class Scattergeo(TraceWrapper): - """See https://plotly.com/python/scatter-plots-on-maps/""" - - graph_object = go.Scattergeo - - -class Choropleth(TraceWrapper): - """See https://plotly.com/python/choropleth-maps/""" - - graph_object = go.Choropleth - - -class Scattermapbox(TraceWrapper): - """See https://plotly.com/python/scattermapbox/""" - - graph_object = go.Scattermapbox - - -class Choroplethmapbox(TraceWrapper): - """See https://plotly.com/python/mapbox-county-choropleth/""" - - graph_object = go.Choroplethmapbox - - -class Densitymapbox(TraceWrapper): - """See https://plotly.com/python/mapbox-density-heatmaps/""" - - graph_object = go.Densitymapbox - - -# Specialized -class Scatterpolar(TraceWrapper): - """See https://plotly.com/python/polar-chart/""" - - graph_object = go.Scatterpolar - - -class Barpolar(TraceWrapper): - """See https://plotly.com/python/wind-rose-charts/""" - - graph_object = go.Barpolar - - -class Sunburst(TraceWrapper): - """See https://plotly.com/python/sunburst-charts/""" - - graph_object = go.Sunburst - - -class Treemap(TraceWrapper): - """See https://plotly.com/python/treemaps/""" - - graph_object = go.Treemap - - -class Icicle(TraceWrapper): - """See https://plotly.com/python/icicle-charts/""" - - graph_object = go.Icicle - - -class Sankey(TraceWrapper): - """See https://plotly.com/python/sankey-diagram/""" - - graph_object = go.Sankey - - -class Parcoords(TraceWrapper): - """See https://plotly.com/python/parallel-coordinates-plot/""" - - graph_object = go.Parcoords - - -class Parcats(TraceWrapper): - """See https://plotly.com/python/parallel-categories-diagram/""" - - graph_object = go.Parcats - - -class Carpet(TraceWrapper): - """See https://plotly.com/python/carpet-plot/""" - - graph_object = go.Carpet - - -class Scattercarpet(TraceWrapper): - """See https://plotly.com/python/carpet-scatter/""" - - graph_object = go.Scattercarpet - - -class Contourcarpet(TraceWrapper): - """See https://plotly.com/python/carpet-contour/""" - - graph_object = go.Contourcarpet - - -class Figure: - subgrounds: Subgrounds - traces: list[TraceWrapper] - req: DataRequest - data: list[dict[str, Any]] - figure: go.Figure - - args: dict[str, Any] - - def __init__( - self, - subgrounds: Subgrounds, - traces: TraceWrapper | list[TraceWrapper], - **kwargs - ) -> None: - self.subgrounds = subgrounds - self.traces = list([traces] | traverse) - - traces = list(self.traces | map(lambda trace: trace.field_paths) | traverse) - if len(traces) > 0: - self.req = self.subgrounds.mk_request(traces) - self.data = self.subgrounds.execute(self.req) - else: - self.req = None - self.data = None - - self.args = kwargs - self.refresh() - - def refresh(self) -> None: - # TODO: Modify this to support x/y in different documents - self.figure = go.Figure(**self.args) - - if self.req is not None: - self.data = self.subgrounds.execute(self.req) - - for trace in self.traces: - self.figure.add_trace(trace.mk_trace(self.data)) - - # @staticmethod - # def mk_subplots(rows, cols, **kwargs): - # return make_subplots(rows, cols, **kwargs) +""" +DEPRECIATED: Use `subgrounds.contrib.plotly` instead +""" + +import warnings + +from subgrounds.contrib.plotly import ( + Bar, + Barpolar, + Box, + Candlestick, + Carpet, + Choropleth, + Choroplethmapbox, + Contour, + Contourcarpet, + Densitymapbox, + Figure, + Funnel, + Heatmap, + Histogram, + Histogram2d, + Histogram2dContour, + Icicle, + Indicator, + Ohlc, + Parcats, + Parcoords, + Pie, + Sankey, + Scatter, + Scatter3d, + Scattercarpet, + Scattergeo, + Scattermapbox, + Scatterpolar, + Sunburst, + Surface, + Table, + TraceWrapper, + Treemap, + Violin, + Waterfall, +) + +__all__ = [ + "Figure", + "TraceWrapper", + "Scatter", + "Pie", + "Bar", + "Heatmap", + "Contour", + "Table", + "Box", + "Violin", + "Histogram", + "Histogram2d", + "Histogram2dContour", + "Ohlc", + "Candlestick", + "Waterfall", + "Funnel", + "Indicator", + "Scatter3d", + "Surface", + "Scattergeo", + "Choropleth", + "Scattermapbox", + "Choroplethmapbox", + "Densitymapbox", + "Scatterpolar", + "Barpolar", + "Sunburst", + "Treemap", + "Icicle", + "Sankey", + "Parcoords", + "Parcats", + "Carpet", + "Scattercarpet", + "Contourcarpet", +] + + +warnings.warn( + "Importing from `subgrounds.plotly_wrappers` is deprecated." + " Use `subgrounds.contrib.plotly` instead.\n" + "Will be removed in a future version.", + DeprecationWarning, +) diff --git a/subgrounds/subgrounds.py b/subgrounds/subgrounds.py index 2f35d7e..99b0d1a 100644 --- a/subgrounds/subgrounds.py +++ b/subgrounds/subgrounds.py @@ -191,7 +191,7 @@ def execute_document(doc: Document) -> dict: ) else: return client.query( - doc.url, doc.graphql, variables=doc.variables, headers=headers + doc.url, doc.graphql, variables=doc.variables, headers=self.headers ) def transform_doc(transforms: list[DocumentTransform], doc: Document) -> dict: