Edit on GitHub

sqlglot.lineage

  1from __future__ import annotations
  2
  3import json
  4import logging
  5import typing as t
  6from dataclasses import dataclass, field
  7
  8from sqlglot import Schema, exp, maybe_parse
  9from sqlglot.errors import SqlglotError
 10from sqlglot.optimizer import Scope, build_scope, find_all_in_scope, normalize_identifiers, qualify
 11
 12if t.TYPE_CHECKING:
 13    from sqlglot.dialects.dialect import DialectType
 14
 15logger = logging.getLogger("sqlglot")
 16
 17
 18@dataclass(frozen=True)
 19class Node:
 20    name: str
 21    expression: exp.Expression
 22    source: exp.Expression
 23    downstream: t.List[Node] = field(default_factory=list)
 24    source_name: str = ""
 25    reference_node_name: str = ""
 26
 27    def walk(self) -> t.Iterator[Node]:
 28        yield self
 29
 30        for d in self.downstream:
 31            yield from d.walk()
 32
 33    def to_html(self, dialect: DialectType = None, **opts) -> GraphHTML:
 34        nodes = {}
 35        edges = []
 36
 37        for node in self.walk():
 38            if isinstance(node.expression, exp.Table):
 39                label = f"FROM {node.expression.this}"
 40                title = f"<pre>SELECT {node.name} FROM {node.expression.this}</pre>"
 41                group = 1
 42            else:
 43                label = node.expression.sql(pretty=True, dialect=dialect)
 44                source = node.source.transform(
 45                    lambda n: (
 46                        exp.Tag(this=n, prefix="<b>", postfix="</b>") if n is node.expression else n
 47                    ),
 48                    copy=False,
 49                ).sql(pretty=True, dialect=dialect)
 50                title = f"<pre>{source}</pre>"
 51                group = 0
 52
 53            node_id = id(node)
 54
 55            nodes[node_id] = {
 56                "id": node_id,
 57                "label": label,
 58                "title": title,
 59                "group": group,
 60            }
 61
 62            for d in node.downstream:
 63                edges.append({"from": node_id, "to": id(d)})
 64        return GraphHTML(nodes, edges, **opts)
 65
 66
 67def lineage(
 68    column: str | exp.Column,
 69    sql: str | exp.Expression,
 70    schema: t.Optional[t.Dict | Schema] = None,
 71    sources: t.Optional[t.Mapping[str, str | exp.Query]] = None,
 72    dialect: DialectType = None,
 73    scope: t.Optional[Scope] = None,
 74    trim_selects: bool = True,
 75    **kwargs,
 76) -> Node:
 77    """Build the lineage graph for a column of a SQL query.
 78
 79    Args:
 80        column: The column to build the lineage for.
 81        sql: The SQL string or expression.
 82        schema: The schema of tables.
 83        sources: A mapping of queries which will be used to continue building lineage.
 84        dialect: The dialect of input SQL.
 85        scope: A pre-created scope to use instead.
 86        trim_selects: Whether or not to clean up selects by trimming to only relevant columns.
 87        **kwargs: Qualification optimizer kwargs.
 88
 89    Returns:
 90        A lineage node.
 91    """
 92
 93    expression = maybe_parse(sql, dialect=dialect)
 94    column = normalize_identifiers.normalize_identifiers(column, dialect=dialect).name
 95
 96    if sources:
 97        expression = exp.expand(
 98            expression,
 99            {k: t.cast(exp.Query, maybe_parse(v, dialect=dialect)) for k, v in sources.items()},
100            dialect=dialect,
101        )
102
103    if not scope:
104        expression = qualify.qualify(
105            expression,
106            dialect=dialect,
107            schema=schema,
108            **{"validate_qualify_columns": False, "identify": False, **kwargs},  # type: ignore
109        )
110
111        scope = build_scope(expression)
112
113    if not scope:
114        raise SqlglotError("Cannot build lineage, sql must be SELECT")
115
116    if not any(select.alias_or_name == column for select in scope.expression.selects):
117        raise SqlglotError(f"Cannot find column '{column}' in query.")
118
119    return to_node(column, scope, dialect, trim_selects=trim_selects)
120
121
122def to_node(
123    column: str | int,
124    scope: Scope,
125    dialect: DialectType,
126    scope_name: t.Optional[str] = None,
127    upstream: t.Optional[Node] = None,
128    source_name: t.Optional[str] = None,
129    reference_node_name: t.Optional[str] = None,
130    trim_selects: bool = True,
131) -> Node:
132    # Find the specific select clause that is the source of the column we want.
133    # This can either be a specific, named select or a generic `*` clause.
134    select = (
135        scope.expression.selects[column]
136        if isinstance(column, int)
137        else next(
138            (select for select in scope.expression.selects if select.alias_or_name == column),
139            exp.Star() if scope.expression.is_star else scope.expression,
140        )
141    )
142
143    if isinstance(scope.expression, exp.Subquery):
144        for source in scope.subquery_scopes:
145            return to_node(
146                column,
147                scope=source,
148                dialect=dialect,
149                upstream=upstream,
150                source_name=source_name,
151                reference_node_name=reference_node_name,
152                trim_selects=trim_selects,
153            )
154    if isinstance(scope.expression, exp.Union):
155        upstream = upstream or Node(name="UNION", source=scope.expression, expression=select)
156
157        index = (
158            column
159            if isinstance(column, int)
160            else next(
161                (
162                    i
163                    for i, select in enumerate(scope.expression.selects)
164                    if select.alias_or_name == column or select.is_star
165                ),
166                -1,  # mypy will not allow a None here, but a negative index should never be returned
167            )
168        )
169
170        if index == -1:
171            raise ValueError(f"Could not find {column} in {scope.expression}")
172
173        for s in scope.union_scopes:
174            to_node(
175                index,
176                scope=s,
177                dialect=dialect,
178                upstream=upstream,
179                source_name=source_name,
180                reference_node_name=reference_node_name,
181                trim_selects=trim_selects,
182            )
183
184        return upstream
185
186    if trim_selects and isinstance(scope.expression, exp.Select):
187        # For better ergonomics in our node labels, replace the full select with
188        # a version that has only the column we care about.
189        #   "x", SELECT x, y FROM foo
190        #     => "x", SELECT x FROM foo
191        source = t.cast(exp.Expression, scope.expression.select(select, append=False))
192    else:
193        source = scope.expression
194
195    # Create the node for this step in the lineage chain, and attach it to the previous one.
196    node = Node(
197        name=f"{scope_name}.{column}" if scope_name else str(column),
198        source=source,
199        expression=select,
200        source_name=source_name or "",
201        reference_node_name=reference_node_name or "",
202    )
203
204    if upstream:
205        upstream.downstream.append(node)
206
207    subquery_scopes = {
208        id(subquery_scope.expression): subquery_scope for subquery_scope in scope.subquery_scopes
209    }
210
211    for subquery in find_all_in_scope(select, exp.UNWRAPPED_QUERIES):
212        subquery_scope = subquery_scopes.get(id(subquery))
213        if not subquery_scope:
214            logger.warning(f"Unknown subquery scope: {subquery.sql(dialect=dialect)}")
215            continue
216
217        for name in subquery.named_selects:
218            to_node(
219                name,
220                scope=subquery_scope,
221                dialect=dialect,
222                upstream=node,
223                trim_selects=trim_selects,
224            )
225
226    # if the select is a star add all scope sources as downstreams
227    if select.is_star:
228        for source in scope.sources.values():
229            if isinstance(source, Scope):
230                source = source.expression
231            node.downstream.append(Node(name=select.sql(), source=source, expression=source))
232
233    # Find all columns that went into creating this one to list their lineage nodes.
234    source_columns = set(find_all_in_scope(select, exp.Column))
235
236    # If the source is a UDTF find columns used in the UTDF to generate the table
237    if isinstance(source, exp.UDTF):
238        source_columns |= set(source.find_all(exp.Column))
239        derived_tables = [
240            source.expression.parent
241            for source in scope.sources.values()
242            if isinstance(source, Scope) and source.is_derived_table
243        ]
244    else:
245        derived_tables = scope.derived_tables
246
247    source_names = {
248        dt.alias: dt.comments[0].split()[1]
249        for dt in derived_tables
250        if dt.comments and dt.comments[0].startswith("source: ")
251    }
252
253    for c in source_columns:
254        table = c.table
255        source = scope.sources.get(table)
256
257        if isinstance(source, Scope):
258            selected_node, _ = scope.selected_sources.get(table, (None, None))
259            # The table itself came from a more specific scope. Recurse into that one using the unaliased column name.
260            to_node(
261                c.name,
262                scope=source,
263                dialect=dialect,
264                scope_name=table,
265                upstream=node,
266                source_name=source_names.get(table) or source_name,
267                reference_node_name=selected_node.name if selected_node else None,
268                trim_selects=trim_selects,
269            )
270        else:
271            # The source is not a scope - we've reached the end of the line. At this point, if a source is not found
272            # it means this column's lineage is unknown. This can happen if the definition of a source used in a query
273            # is not passed into the `sources` map.
274            source = source or exp.Placeholder()
275            node.downstream.append(Node(name=c.sql(), source=source, expression=source))
276
277    return node
278
279
280class GraphHTML:
281    """Node to HTML generator using vis.js.
282
283    https://visjs.github.io/vis-network/docs/network/
284    """
285
286    def __init__(
287        self, nodes: t.Dict, edges: t.List, imports: bool = True, options: t.Optional[t.Dict] = None
288    ):
289        self.imports = imports
290
291        self.options = {
292            "height": "500px",
293            "width": "100%",
294            "layout": {
295                "hierarchical": {
296                    "enabled": True,
297                    "nodeSpacing": 200,
298                    "sortMethod": "directed",
299                },
300            },
301            "interaction": {
302                "dragNodes": False,
303                "selectable": False,
304            },
305            "physics": {
306                "enabled": False,
307            },
308            "edges": {
309                "arrows": "to",
310            },
311            "nodes": {
312                "font": "20px monaco",
313                "shape": "box",
314                "widthConstraint": {
315                    "maximum": 300,
316                },
317            },
318            **(options or {}),
319        }
320
321        self.nodes = nodes
322        self.edges = edges
323
324    def __str__(self):
325        nodes = json.dumps(list(self.nodes.values()))
326        edges = json.dumps(self.edges)
327        options = json.dumps(self.options)
328        imports = (
329            """<script type="text/javascript" src="https://unpkg.com/vis-data@latest/peer/umd/vis-data.min.js"></script>
330  <script type="text/javascript" src="https://unpkg.com/vis-network@latest/peer/umd/vis-network.min.js"></script>
331  <link rel="stylesheet" type="text/css" href="https://unpkg.com/vis-network/styles/vis-network.min.css" />"""
332            if self.imports
333            else ""
334        )
335
336        return f"""<div>
337  <div id="sqlglot-lineage"></div>
338  {imports}
339  <script type="text/javascript">
340    var nodes = new vis.DataSet({nodes})
341    nodes.forEach(row => row["title"] = new DOMParser().parseFromString(row["title"], "text/html").body.childNodes[0])
342
343    new vis.Network(
344        document.getElementById("sqlglot-lineage"),
345        {{
346            nodes: nodes,
347            edges: new vis.DataSet({edges})
348        }},
349        {options},
350    )
351  </script>
352</div>"""
353
354    def _repr_html_(self) -> str:
355        return self.__str__()
logger = <Logger sqlglot (WARNING)>
@dataclass(frozen=True)
class Node:
19@dataclass(frozen=True)
20class Node:
21    name: str
22    expression: exp.Expression
23    source: exp.Expression
24    downstream: t.List[Node] = field(default_factory=list)
25    source_name: str = ""
26    reference_node_name: str = ""
27
28    def walk(self) -> t.Iterator[Node]:
29        yield self
30
31        for d in self.downstream:
32            yield from d.walk()
33
34    def to_html(self, dialect: DialectType = None, **opts) -> GraphHTML:
35        nodes = {}
36        edges = []
37
38        for node in self.walk():
39            if isinstance(node.expression, exp.Table):
40                label = f"FROM {node.expression.this}"
41                title = f"<pre>SELECT {node.name} FROM {node.expression.this}</pre>"
42                group = 1
43            else:
44                label = node.expression.sql(pretty=True, dialect=dialect)
45                source = node.source.transform(
46                    lambda n: (
47                        exp.Tag(this=n, prefix="<b>", postfix="</b>") if n is node.expression else n
48                    ),
49                    copy=False,
50                ).sql(pretty=True, dialect=dialect)
51                title = f"<pre>{source}</pre>"
52                group = 0
53
54            node_id = id(node)
55
56            nodes[node_id] = {
57                "id": node_id,
58                "label": label,
59                "title": title,
60                "group": group,
61            }
62
63            for d in node.downstream:
64                edges.append({"from": node_id, "to": id(d)})
65        return GraphHTML(nodes, edges, **opts)
Node( name: str, expression: sqlglot.expressions.Expression, source: sqlglot.expressions.Expression, downstream: List[Node] = <factory>, source_name: str = '', reference_node_name: str = '')
name: str
downstream: List[Node]
source_name: str = ''
reference_node_name: str = ''
def walk(self) -> Iterator[Node]:
28    def walk(self) -> t.Iterator[Node]:
29        yield self
30
31        for d in self.downstream:
32            yield from d.walk()
def to_html( self, dialect: Union[str, sqlglot.dialects.dialect.Dialect, Type[sqlglot.dialects.dialect.Dialect], NoneType] = None, **opts) -> GraphHTML:
34    def to_html(self, dialect: DialectType = None, **opts) -> GraphHTML:
35        nodes = {}
36        edges = []
37
38        for node in self.walk():
39            if isinstance(node.expression, exp.Table):
40                label = f"FROM {node.expression.this}"
41                title = f"<pre>SELECT {node.name} FROM {node.expression.this}</pre>"
42                group = 1
43            else:
44                label = node.expression.sql(pretty=True, dialect=dialect)
45                source = node.source.transform(
46                    lambda n: (
47                        exp.Tag(this=n, prefix="<b>", postfix="</b>") if n is node.expression else n
48                    ),
49                    copy=False,
50                ).sql(pretty=True, dialect=dialect)
51                title = f"<pre>{source}</pre>"
52                group = 0
53
54            node_id = id(node)
55
56            nodes[node_id] = {
57                "id": node_id,
58                "label": label,
59                "title": title,
60                "group": group,
61            }
62
63            for d in node.downstream:
64                edges.append({"from": node_id, "to": id(d)})
65        return GraphHTML(nodes, edges, **opts)
def lineage( column: str | sqlglot.expressions.Column, sql: str | sqlglot.expressions.Expression, schema: Union[Dict, sqlglot.schema.Schema, NoneType] = None, sources: Optional[Mapping[str, str | sqlglot.expressions.Query]] = None, dialect: Union[str, sqlglot.dialects.dialect.Dialect, Type[sqlglot.dialects.dialect.Dialect], NoneType] = None, scope: Optional[sqlglot.optimizer.scope.Scope] = None, trim_selects: bool = True, **kwargs) -> Node:
 68def lineage(
 69    column: str | exp.Column,
 70    sql: str | exp.Expression,
 71    schema: t.Optional[t.Dict | Schema] = None,
 72    sources: t.Optional[t.Mapping[str, str | exp.Query]] = None,
 73    dialect: DialectType = None,
 74    scope: t.Optional[Scope] = None,
 75    trim_selects: bool = True,
 76    **kwargs,
 77) -> Node:
 78    """Build the lineage graph for a column of a SQL query.
 79
 80    Args:
 81        column: The column to build the lineage for.
 82        sql: The SQL string or expression.
 83        schema: The schema of tables.
 84        sources: A mapping of queries which will be used to continue building lineage.
 85        dialect: The dialect of input SQL.
 86        scope: A pre-created scope to use instead.
 87        trim_selects: Whether or not to clean up selects by trimming to only relevant columns.
 88        **kwargs: Qualification optimizer kwargs.
 89
 90    Returns:
 91        A lineage node.
 92    """
 93
 94    expression = maybe_parse(sql, dialect=dialect)
 95    column = normalize_identifiers.normalize_identifiers(column, dialect=dialect).name
 96
 97    if sources:
 98        expression = exp.expand(
 99            expression,
100            {k: t.cast(exp.Query, maybe_parse(v, dialect=dialect)) for k, v in sources.items()},
101            dialect=dialect,
102        )
103
104    if not scope:
105        expression = qualify.qualify(
106            expression,
107            dialect=dialect,
108            schema=schema,
109            **{"validate_qualify_columns": False, "identify": False, **kwargs},  # type: ignore
110        )
111
112        scope = build_scope(expression)
113
114    if not scope:
115        raise SqlglotError("Cannot build lineage, sql must be SELECT")
116
117    if not any(select.alias_or_name == column for select in scope.expression.selects):
118        raise SqlglotError(f"Cannot find column '{column}' in query.")
119
120    return to_node(column, scope, dialect, trim_selects=trim_selects)

Build the lineage graph for a column of a SQL query.

Arguments:
  • column: The column to build the lineage for.
  • sql: The SQL string or expression.
  • schema: The schema of tables.
  • sources: A mapping of queries which will be used to continue building lineage.
  • dialect: The dialect of input SQL.
  • scope: A pre-created scope to use instead.
  • trim_selects: Whether or not to clean up selects by trimming to only relevant columns.
  • **kwargs: Qualification optimizer kwargs.
Returns:

A lineage node.

def to_node( column: str | int, scope: sqlglot.optimizer.scope.Scope, dialect: Union[str, sqlglot.dialects.dialect.Dialect, Type[sqlglot.dialects.dialect.Dialect], NoneType], scope_name: Optional[str] = None, upstream: Optional[Node] = None, source_name: Optional[str] = None, reference_node_name: Optional[str] = None, trim_selects: bool = True) -> Node:
123def to_node(
124    column: str | int,
125    scope: Scope,
126    dialect: DialectType,
127    scope_name: t.Optional[str] = None,
128    upstream: t.Optional[Node] = None,
129    source_name: t.Optional[str] = None,
130    reference_node_name: t.Optional[str] = None,
131    trim_selects: bool = True,
132) -> Node:
133    # Find the specific select clause that is the source of the column we want.
134    # This can either be a specific, named select or a generic `*` clause.
135    select = (
136        scope.expression.selects[column]
137        if isinstance(column, int)
138        else next(
139            (select for select in scope.expression.selects if select.alias_or_name == column),
140            exp.Star() if scope.expression.is_star else scope.expression,
141        )
142    )
143
144    if isinstance(scope.expression, exp.Subquery):
145        for source in scope.subquery_scopes:
146            return to_node(
147                column,
148                scope=source,
149                dialect=dialect,
150                upstream=upstream,
151                source_name=source_name,
152                reference_node_name=reference_node_name,
153                trim_selects=trim_selects,
154            )
155    if isinstance(scope.expression, exp.Union):
156        upstream = upstream or Node(name="UNION", source=scope.expression, expression=select)
157
158        index = (
159            column
160            if isinstance(column, int)
161            else next(
162                (
163                    i
164                    for i, select in enumerate(scope.expression.selects)
165                    if select.alias_or_name == column or select.is_star
166                ),
167                -1,  # mypy will not allow a None here, but a negative index should never be returned
168            )
169        )
170
171        if index == -1:
172            raise ValueError(f"Could not find {column} in {scope.expression}")
173
174        for s in scope.union_scopes:
175            to_node(
176                index,
177                scope=s,
178                dialect=dialect,
179                upstream=upstream,
180                source_name=source_name,
181                reference_node_name=reference_node_name,
182                trim_selects=trim_selects,
183            )
184
185        return upstream
186
187    if trim_selects and isinstance(scope.expression, exp.Select):
188        # For better ergonomics in our node labels, replace the full select with
189        # a version that has only the column we care about.
190        #   "x", SELECT x, y FROM foo
191        #     => "x", SELECT x FROM foo
192        source = t.cast(exp.Expression, scope.expression.select(select, append=False))
193    else:
194        source = scope.expression
195
196    # Create the node for this step in the lineage chain, and attach it to the previous one.
197    node = Node(
198        name=f"{scope_name}.{column}" if scope_name else str(column),
199        source=source,
200        expression=select,
201        source_name=source_name or "",
202        reference_node_name=reference_node_name or "",
203    )
204
205    if upstream:
206        upstream.downstream.append(node)
207
208    subquery_scopes = {
209        id(subquery_scope.expression): subquery_scope for subquery_scope in scope.subquery_scopes
210    }
211
212    for subquery in find_all_in_scope(select, exp.UNWRAPPED_QUERIES):
213        subquery_scope = subquery_scopes.get(id(subquery))
214        if not subquery_scope:
215            logger.warning(f"Unknown subquery scope: {subquery.sql(dialect=dialect)}")
216            continue
217
218        for name in subquery.named_selects:
219            to_node(
220                name,
221                scope=subquery_scope,
222                dialect=dialect,
223                upstream=node,
224                trim_selects=trim_selects,
225            )
226
227    # if the select is a star add all scope sources as downstreams
228    if select.is_star:
229        for source in scope.sources.values():
230            if isinstance(source, Scope):
231                source = source.expression
232            node.downstream.append(Node(name=select.sql(), source=source, expression=source))
233
234    # Find all columns that went into creating this one to list their lineage nodes.
235    source_columns = set(find_all_in_scope(select, exp.Column))
236
237    # If the source is a UDTF find columns used in the UTDF to generate the table
238    if isinstance(source, exp.UDTF):
239        source_columns |= set(source.find_all(exp.Column))
240        derived_tables = [
241            source.expression.parent
242            for source in scope.sources.values()
243            if isinstance(source, Scope) and source.is_derived_table
244        ]
245    else:
246        derived_tables = scope.derived_tables
247
248    source_names = {
249        dt.alias: dt.comments[0].split()[1]
250        for dt in derived_tables
251        if dt.comments and dt.comments[0].startswith("source: ")
252    }
253
254    for c in source_columns:
255        table = c.table
256        source = scope.sources.get(table)
257
258        if isinstance(source, Scope):
259            selected_node, _ = scope.selected_sources.get(table, (None, None))
260            # The table itself came from a more specific scope. Recurse into that one using the unaliased column name.
261            to_node(
262                c.name,
263                scope=source,
264                dialect=dialect,
265                scope_name=table,
266                upstream=node,
267                source_name=source_names.get(table) or source_name,
268                reference_node_name=selected_node.name if selected_node else None,
269                trim_selects=trim_selects,
270            )
271        else:
272            # The source is not a scope - we've reached the end of the line. At this point, if a source is not found
273            # it means this column's lineage is unknown. This can happen if the definition of a source used in a query
274            # is not passed into the `sources` map.
275            source = source or exp.Placeholder()
276            node.downstream.append(Node(name=c.sql(), source=source, expression=source))
277
278    return node
class GraphHTML:
281class GraphHTML:
282    """Node to HTML generator using vis.js.
283
284    https://visjs.github.io/vis-network/docs/network/
285    """
286
287    def __init__(
288        self, nodes: t.Dict, edges: t.List, imports: bool = True, options: t.Optional[t.Dict] = None
289    ):
290        self.imports = imports
291
292        self.options = {
293            "height": "500px",
294            "width": "100%",
295            "layout": {
296                "hierarchical": {
297                    "enabled": True,
298                    "nodeSpacing": 200,
299                    "sortMethod": "directed",
300                },
301            },
302            "interaction": {
303                "dragNodes": False,
304                "selectable": False,
305            },
306            "physics": {
307                "enabled": False,
308            },
309            "edges": {
310                "arrows": "to",
311            },
312            "nodes": {
313                "font": "20px monaco",
314                "shape": "box",
315                "widthConstraint": {
316                    "maximum": 300,
317                },
318            },
319            **(options or {}),
320        }
321
322        self.nodes = nodes
323        self.edges = edges
324
325    def __str__(self):
326        nodes = json.dumps(list(self.nodes.values()))
327        edges = json.dumps(self.edges)
328        options = json.dumps(self.options)
329        imports = (
330            """<script type="text/javascript" src="https://unpkg.com/vis-data@latest/peer/umd/vis-data.min.js"></script>
331  <script type="text/javascript" src="https://unpkg.com/vis-network@latest/peer/umd/vis-network.min.js"></script>
332  <link rel="stylesheet" type="text/css" href="https://unpkg.com/vis-network/styles/vis-network.min.css" />"""
333            if self.imports
334            else ""
335        )
336
337        return f"""<div>
338  <div id="sqlglot-lineage"></div>
339  {imports}
340  <script type="text/javascript">
341    var nodes = new vis.DataSet({nodes})
342    nodes.forEach(row => row["title"] = new DOMParser().parseFromString(row["title"], "text/html").body.childNodes[0])
343
344    new vis.Network(
345        document.getElementById("sqlglot-lineage"),
346        {{
347            nodes: nodes,
348            edges: new vis.DataSet({edges})
349        }},
350        {options},
351    )
352  </script>
353</div>"""
354
355    def _repr_html_(self) -> str:
356        return self.__str__()

Node to HTML generator using vis.js.

https://visjs.github.io/vis-network/docs/network/

GraphHTML( nodes: Dict, edges: List, imports: bool = True, options: Optional[Dict] = None)
287    def __init__(
288        self, nodes: t.Dict, edges: t.List, imports: bool = True, options: t.Optional[t.Dict] = None
289    ):
290        self.imports = imports
291
292        self.options = {
293            "height": "500px",
294            "width": "100%",
295            "layout": {
296                "hierarchical": {
297                    "enabled": True,
298                    "nodeSpacing": 200,
299                    "sortMethod": "directed",
300                },
301            },
302            "interaction": {
303                "dragNodes": False,
304                "selectable": False,
305            },
306            "physics": {
307                "enabled": False,
308            },
309            "edges": {
310                "arrows": "to",
311            },
312            "nodes": {
313                "font": "20px monaco",
314                "shape": "box",
315                "widthConstraint": {
316                    "maximum": 300,
317                },
318            },
319            **(options or {}),
320        }
321
322        self.nodes = nodes
323        self.edges = edges
imports
options
nodes
edges