LIBCGRAPH(3)
π LIBCGRAPH(3) β the abstract graph library, but make it a group chat π
okay bestie, sit down, hydrate, because this is the manual page for libcgraph β the abstract graph library from the Graphviz universe. it's long. it's iconic. we're doing this in parts, and this is part 1. let's go π
β¨ NAME β¨
libcgraph β abstract graph library. that's it, that's the name, and honestly? it ate. π₯
πΈ SYNOPSIS πΈ
#include <graphviz/cgraph.h>
π TYPES
Agraph_t;
Agnode_t;
Agedge_t;
Agdesc_t;
Agdisc_t;
Agsym_t;
Agrec_t;
Agcbdisc_t;
π GLOBALS
Agiddisc_t AgIdDisc;
Agiodisc_t AgIoDisc;
Agdisc_t AgDefaultDisc;
π GRAPHS
Agraph_t *agopen(char *name, Agdesc_t kind, Agdisc_t *disc);
int agclose(Agraph_t *g);
Agraph_t *agread(void *channel, Agdisc_t *);
Agraph_t *agmemread(char *);
Agraph_t *agconcat(Agraph_t *g, const char *filename, void *channel, Agdisc_t *disc);
int agwrite(Agraph_t *g, void *channel);
int agnnodes(Agraph_t *g),agnedges(Agraph_t *g), agnsubg(Agraph_t * g);
int agisdirected(Agraph_t * g),agisundirected(Agraph_t * g),agisstrict(Agraph_t * g), agissimple(Agraph_t * g);
bool graphviz_acyclic(Agraph_t *g, const graphviz_acyclic_options_t *opts, size_t *num_rev);
void graphviz_tred(Agraph_t *g, const graphviz_tred_options_t *opts);
void graphviz_unflatten(Agraph_t *g, const graphviz_unflatten_options_t *opts);
π SUBGRAPHS
Agraph_t *agsubg(Agraph_t *g, char *name, int createflag);
Agraph_t *agidsubg(Agraph_t * g, unsigned long id, int cflag);
Agraph_t *agfstsubg(Agraph_t *g), agnxtsubg(Agraph_t *);
Agraph_t *agparent(Agraph_t *g);
int agdelsubg(Agraph_t * g, Agraph_t * sub); /* same as agclose() */
π NODES
Agnode_t *agnode(Agraph_t *g, char *name, int createflag);
Agnode_t *agidnode(Agraph_t *g, ulong id, int createflag);
Agnode_t *agsubnode(Agraph_t *g, Agnode_t *n, int createflag);
Agnode_t *agfstnode(Agraph_t *g);
Agnode_t *agnxtnode(Agraph_t *g, Agnode_t *n);
Agnode_t *agprvnode(Agraph_t *g, Agnode_t *n);
Agnode_t *aglstnode(Agraph_t *g);
int agdelnode(Agraph_t *g, Agnode_t *n);
int agdegree(Agraph_t *g, Agnode_t *n, int use_inedges, int use_outedges);
int agcountuniqedges(Agraph_t * g, Agnode_t * n, int in, int out);
π EDGES
Agedge_t *agedge(Agraph_t* g, Agnode_t *t, Agnode_t *h, char *name, int createflag);
Agedge_t *agidedge(Agraph_t * g, Agnode_t * t, Agnode_t * h, unsigned long id, int createflag);
Agedge_t *agsubedge(Agraph_t *g, Agedge_t *e, int createflag);
Agnode_t *aghead(Agedge_t *e), *agtail(Agedge_t *e);
Agedge_t *agfstedge(Agraph_t* g, Agnode_t *n);
Agedge_t *agnxtedge(Agraph_t* g, Agedge_t *e, Agnode_t *n);
Agedge_t *agfstin(Agraph_t* g, Agnode_t *n);
Agedge_t *agnxtin(Agraph_t* g, Agedge_t *e);
Agedge_t *agfstout(Agraph_t* g, Agnode_t *n);
Agedge_t *agnxtout(Agraph_t* g, Agedge_t *e);
int agdeledge(Agraph_t *g, Agedge_t *e);
Agedge_t *agopp(Agedge_t *e);
int ageqedge(Agedge_t *e0, Agedge_t *e1);
π STRING ATTRIBUTES
Agsym_t *agattr_text(Agraph_t *g, int kind, char *name, const char *value);
Agsym_t *agattrsym(void *obj, char *name);
Agsym_t *agnxtattr(Agraph_t *g, int kind, Agsym_t *attr);
char *agget(void *obj, char *name);
char *agxget(void *obj, Agsym_t *sym);
int agset(void *obj, char *name, char *value);
int agxset(void *obj, Agsym_t *sym, char *value);
int agsafeset(void *obj, char *name, char *value, char *def);
int agcopyattr(void *, void *);
π RECORDS
void *agbindrec(void *obj, char *name, unsigned int size, move_to_front);
Agrec_t *aggetrec(void *obj, char *name, int move_to_front);
int agdelrec(Agraph_t *g, void *obj, char *name);
void aginit(Agraph_t * g, int kind, char *rec_name, int rec_size, int move_to_front);
void agclean(Agraph_t * g, int kind, char *rec_name);
π CALLBACKS
int *agpopdisc(Agraph_t *g);
void agpushdisc(Agraph_t *g, Agcbdisc_t *disc);
int agcallbacks(Agraph_t * g, int flag);
π STRINGS
char *agstrdup(Agraph_t *, char *);
char *agstrdup_html(Agraph_t *, char *);
int aghtmlstr(char *);
char *agstrbind(Agraph_t * g, char *);
int strfree(Agraph_t *, char *);
char *agstrcanon(char *, char *);
π GENERIC OBJECTS
Agraph_t *agraphof(void*);
Agraph_t *agroot(void*);
int agcontains(Agraph_t*, void*);
char *agnameof(void*);
void agdelete(Agraph_t *g, void *obj);
int agobjkind(void *obj);
Agrec_t *AGDATA(void *obj);
ulong AGID(void *obj);
int AGTYPE(void *obj);
π ERROR REPORTING
typedef enum { AGWARN, AGERR, AGMAX, AGPREV } agerrlevel_t;
typedef int (*agusererrf) (char*);
agerrlevel_t agerrno;
agerrlevel_t agseterr(agerrlevel_t);
char *aglasterr(void);
int agerr(agerrlevel_t level, char *fmt, ...);
void agerrorf(char *fmt, ...);
void agwarningf(char *fmt, ...);
int agerrors(void);
agusererrf agseterrf(agusererrf);
π¦ DESCRIPTION π¦
so what does libcgraph actually do? bestie, it supports graph programming by keeping your graphs in memory and reading and writing graph files. that's the whole vibe. a graph is made of nodes, edges, and nested subgraphs. π«Ά
and here's the cute part: all these graph objects can be attributed with string name-value pairs and programmer-defined records (peep the Attributes section, iykyk).
every global symbol in libcgraph starts with the prefix ag (case may vary, she's flexible like that π
).
β οΈ okay important thing: in everything below, if a function has a parameterint createflagand the object doesn't exist, the function will create the specified object ifcreateflagis non-zero β otherwise, it returnsNULL. no object for you. π
π GRAPH AND SUBGRAPHS π
a "main" or "root" graph defines a namespace for a whole collection of graph objects (subgraphs, nodes, edges) and their attributes. objects can be named by unique strings or by integer IDs. pick your fighter. β¨
agopenβ creates a new graph with the givennameandkind. the graph kinds areAgdirected,Agundirected,Agstrictdirected, andAgstrictundirected. and heads up: a strict graph cannot have multi-edges or self-arcs, periodt. π« the final argument points to a discipline structure that can be used to tailor I/O and ID allocation β typically aNULLvalue is used to mean the default disciplineAgDefaultDisc.agcloseβ deletes a graph, freeing its associated storage. bye bestie πagread,agwrite, andagconcatβ do file I/O using the graph file language described below.agreadconstructs a new graph, whileagconcatmerges the file contents with a pre-existing graph.agmemreadβ attempts to read a graph from the input string.
β οΈ though I/O methods may be overridden, the default is that thechannelargument is a stdioFILEpointer. in that case, if any of the streams are wide-oriented, the behavior is undefined. like, don't do that. π
the functions agisdirected, agisundirected, agisstrict, and agissimple can be used to query whether a graph is, respectively:
- directed πΉ
- undirected π
- strict (at most one edge with a given tail and head) β
- simple (strict with no loops) π
agsubgβ finds or creates a subgraph by name.agidsubgβ lets a programmer specify the subgraph by a unique integer ID.- a new subgraph is initially empty and is of the same kind as its parent. nested subgraph trees may be created. a subgraph's name is only interpreted relative to its parent β it's a family thing, not a global thing.
agfstsubgandagnxtsubgβ let a program scan subgraphs under a given graph.- a subgraph is deleted with
agdelsubg(oragclose). agparentβ returns the immediate parent graph of a subgraph, or itself if the graph is already a root graph. self-love fr fr. πͺ
by default, nodes are stored in ordered sets for efficient random access to insert, find, and delete nodes. the edges of a node are also stored in ordered sets. these sets are maintained internally as splay tree dictionaries using Phong Vo's cdt library. she's fancy like that. π
agnnodes,agnedges, andagnsubgβ return the sizes of the node, edge and subgraph sets of a graph.agdegreeβ returns the size of the edge set of a node, and takes flags to select in-edges, out-edges, or both.agcountuniqedgesβ also returns the size of the edge set of a node, and takes flags to select in-edges, out-edges, or both. but unlikeagdegree, each loop is only counted once. π
π NODES π
a node is created by giving a unique string name or a programmer defined integer ID, and is represented by a unique internal object. and get this β node equality can be checked by pointer comparison. iconic behavior honestly.
agnodeβ searches in a graph or subgraph for a node with the given name, and returns it if found.agidnodeβ allows a programmer to specify the node by a unique integer ID.agsubnodeβ performs a similar operation on an existing node and a subgraph.agfstnodeandagnxtnodeβ scan node lists.agprvnodeandaglstnodeβ symmetric, but they scan backward. π- the default sequence is order of creation (object timestamp). π°οΈ
agdelnodeβ removes a node from a graph or subgraph.
π₯ EDGES π₯
an abstract edge has two endpoint nodes called tail and head, where all outedges of the same node have it as the tail value, and similarly all inedges have it as the head. in an undirected graph, head and tail are interchangeable β no labels, no drama. and if a graph has multi-edges between the same pair of nodes, the edge's string name behaves as a secondary key.
agedgeβ searches in a graph or subgraph for an edge between the given endpoints (with an optional multi-edge selectorname) and returns it if found or created. note that, in undirected graphs, a search tries both orderings of the tail and head nodes. if the name isNULL, then an anonymous internal value is generated.agidedgeβ allows a programmer to create an edge by giving its unique integer ID.agsubedgeβ performs a similar operation on an existing edge and a subgraph.agfstin,agnxtin,agfstout, andagnxtoutβ visit directed in- and out- edge lists, and ordinarily apply only in directed graphs.agfstedgeandagnxtedgeβ visit all edges incident to a node.agtailandagheadβ get the endpoint of an edge.agdeledgeβ removes an edge from a graph or subgraph.
π okay bestie this next bit is the sneaky one: an abstract edge has two distinct concrete representations β as an in-edge and as an out-edge. in particular, the pointer as an out-edge is different from the pointer as an in-edge. so do not just compare raw pointers and call it a day.
ageqedgeβ canonicalizes the pointers before doing a comparison, so it can be used to test edge equality. βagoppβ flips the sense of an edge. π
π§ INTERNAL ATTRIBUTES π§
programmer-defined values may be dynamically attached to graphs, subgraphs, nodes, and edges. and those values are either:
- character string data (for I/O) π
- uninterpreted binary records (for implementing algorithms efficiently) β‘
both? both. both is good.
okay that's the setup, the anatomy, the whole cast of characters β but the actual attribute machinery, records, callbacks, strings, error reporting and all the β¨spicyβ¨ stuff is coming up in the next part. stay tuned bestie π¦
π STRING ATTRIBUTES π
okay so bestie, string attributes? Libcgraph handles them automatically when it reads and writes graph files. No cap. A string attribute is identified by name and by an internal symbol table entry (Agsym_t) that Libcgraph creates. And listen: attributes of nodes, edges, and graphs (with their subgraphs) have separate namespaces. The contents of an Agsym_t have a char* name for the attribute's name, a char* defval field for the attribute's default value, and an int id field containing the index of the attribute's specific value for an object in the object's array of attribute values. It's giving very organized, honestly.
agattr creates or looks up attributes. kind may be AGRAPH, AGNODE, or AGEDGE. If value is (char*)0, the request is to search for an existing attribute of the given kind and name. Otherwise, if the attribute already exists, its default for creating new objects is set to the given value; if it does not exist, a new attribute is created with the given default, and the default is applied to all pre-existing objects of the given kind. If g is NULL, the default is set for all graphs created subsequently. Slay.
agattrsym is a helper function that looks up an attribute for a graph object given as an argument. agnxtattr permits traversing the list of attributes of a given type. If NULL is passed as an argument it gets the first attribute; otherwise it returns the next one in succession or returns NULL at the end of the list. agget and agset allow fetching and updating a string attribute for an object taking the attribute name as an argument. agxget and agxset do this but with an attribute symbol table entry as an argument (to avoid the cost of the string lookup). Note that agset will fail unless the attribute is first defined using agattr. agsafeset is a convenience function that ensures the given attribute is declared before setting it locally on an object. Pro tip: don't skip the declaration, bestie.
It is sometimes convenient to copy all of the attributes from one object to another. This can be done using agcopyattr. This fails and returns non-zero if argument objects are different kinds, or if all of the attributes of the source object have not been declared for the target object. So like, vibe check your kinds first. π
π§΅ STRINGS π§΅
Libcgraph performs its own storage management of strings as reference-counted strings. The caller does not need to dynamically allocate storage. Iconic behavior, honestly.
agstrdup returns a pointer to a reference-counted copy of the argument string, creating one if necessary. agstrbind returns a pointer to a reference-counted string if it exists, or NULL if not. All uses of cgraph strings need to be freed using agstrfree in order to correctly maintain the reference count. Do not forget to free, or the reference count will be messy.
The cgraph parser handles HTML-like strings. These should be indistinguishable from other strings for most purposes. To create an HTML-like string, use agstrdup_html. The aghtmlstr function can be used to query if a string is an ordinary string or an HTML-like string. HTML strings are lowkey just strings with a glow-up.
agstrcanon returns a pointer to a version of the input string canonicalized for output for later re-parsing. This includes quoting special characters and keywords. The application passes in a buffer as the second argument. Note that the buffer may not be used; if the input string is in canonical form, the function will just return a pointer to it. The input string must have been created using agstrdup or agstrdup_html. No cap, it'll just hand you back your own pointer if you're already canonical.
π¦ RECORDS π¦
Uninterpreted records may be attached to graphs, subgraphs, nodes, and edges for efficient operations on values such as marks, weights, counts, and pointers needed by algorithms. Application programmers define the fields of these records, but they must be declared with a common header as shown below. It's giving custom data, but make it safe.
typedef struct {
Agrec_t header;
/* programmer-defined fields follow */
} user_data_t;
Records are created and managed by Libcgraph. A programmer must explicitly attach them to the objects in a graph, either to individual objects one at a time via agbindrec, or to all the objects of the same class in a graph via aginit. (Note that for graphs, aginit is applied recursively to the graph and its subgraphs if rec_size is negative (of the actual rec_size).) The name argument of a record distinguishes various types of records, and is programmer defined (Libcgraph reserves the prefix _ag). If size is 0, the call to agbindrec is simply a lookup. The function aggetrec can also be used for lookup. agdelrec deletes a named record from one object. agclean does the same for all objects of the same class in an entire graph. Cleanup queen energy.
Internally, records are maintained in circular linked lists attached to graph objects. To allow referencing application-dependent data without function calls or search, Libcgraph allows setting and locking the list pointer of a graph, node, or edge on a particular record. This pointer can be obtained with the macro AGDATA(obj). A cast, generally within a macro or inline function, is usually applied to convert the list pointer to an appropriate programmer-defined type. Cast it, bestie.
To control the setting of this pointer, the move_to_front flag may be TRUE or FALSE. If move_to_front is TRUE, the record will be locked at the head of the list, so it can be accessed directly by AGDATA(obj). The lock can be subsequently released or reset by a call to aggetrec.
π§ DISCIPLINES π§
(This section is not intended for casual users.) Programmer-defined disciplines customize certain resources- ID namespace and I/O - needed by Libcgraph. A discipline struct (or NULL) is passed at graph creation time. Not for the faint of heart, but we love a challenge.
struct Agdisc_s { /* user's discipline */
Agiddisc_t *id;
Agiodisc_t *io;
} ;
A default discipline is supplied when NULL is given for any of these fields.
πͺͺ ID DISCIPLINE πͺͺ
An ID allocator discipline allows a client to control assignment of IDs (uninterpreted integer values) to objects, and possibly how they are mapped to and from strings.
struct Agiddisc_s { /* object ID allocator */
void *(*open) (Agraph_t * g, Agdisc_t*); /* associated with a graph */
long (*map) (void *state, int objtype, char *str, unsigned long *id, int createflag);
long (*alloc) (void *state, int objtype, unsigned long id);
void (*free) (void *state, int objtype, unsigned long id);
char *(*print) (void *state, int objtype, unsigned long id);
void (*close) (void *state);
};
open permits the ID discipline to initialize any data structures that it maintains per individual graph. Its return value is then passed as the first argument (void *state) to all subsequent ID manager calls.
alloc informs the ID manager that Libcgraph is attempting to create an object with a specific ID that was given by a client. The ID manager should return TRUE (nonzero) if the ID can be allocated, or FALSE (which aborts the operation).
free is called to inform the ID manager that the object labeled with the given ID is about to go out of existence. RIP to that object.
map is called to create or look-up IDs by string name (if supported by the ID manager). Returning TRUE (nonzero) in all cases means that the request succeeded (with a valid ID stored through result). There are four cases:
name != NULLandcreateflag == 1: This requests mapping a string (e.g. a name in a graph file) into a new ID. If the ID manager can comply, then it stores the result and returnsTRUE. It is then also responsible for being able to print the ID again as a string. Otherwise the ID manager may returnFALSEbut it must implement the following (at least for graph file reading and writing to work):name == NULLandcreateflag == 1: The ID manager creates a unique new ID of its own choosing. Although it may returnFALSEif it does not support anonymous objects, but this is strongly discouraged (to support "local names" in graph files.)name != NULLandcreateflag == 0: This is a namespace probe. If the name was previously mapped into an allocated ID by the ID manager, then the manager must return this ID. Otherwise, the ID manager may either returnFALSE, or may store any unallocated ID intoresult. (This is convenient, for example, if names are known to be digit strings that are directly converted into integer values.)name == NULLandcreateflag == 0: forbidden. Absolutely not. Do not pass go.
print is allowed to return a pointer to a static buffer; a caller must copy its value if needed past subsequent calls. NULL should be returned by ID managers that do not map names.
The map and alloc calls do not pass a pointer to the newly allocated object. If a client needs to install object pointers in a handle table, it can obtain them via new object callbacks. Callbacks to the rescue.
π‘ IO DISCIPLINE π‘
The I/O discipline provides an abstraction for the reading and writing of graphs.
struct Agiodisc_s {
int (*fread)(void *chan, char *buf, int bufsize);
int (*putstr)(void *chan, char *str);
int (*flush)(void *chan); /* sync */
} ;
Normally, the FILE structure and its related functions are used for I/O. At times, though, an application may need to use a totally different type of character source. The associated state or stream information is provided by the chan argument to agread or agwrite. The discipline function fread and putstr provide the corresponding functions for read and writing. Abstract it, bestie.
π CALLBACKS π
An Agcbdisc_t defines callbacks to be invoked by Libcgraph when initializing, modifying, or finalizing graph objects. Disciplines are kept on a stack. Libcgraph automatically calls the methods on the stack, top-down. Callbacks are installed with agpushdisc, uninstalled with agpopdisc, and can be held pending or released via agcallbacks. Stack energy. Push, pop, slay.
π GENERIC OBJECTS π
agroot takes any graph object (graph, subgraph, node, edge) and returns the root graph in which it lives. agraphof does the same, except it is the identity function on graphs and subgraphs. Note that there is no function to return the least subgraph containing an object, in part because this is not well-defined as nodes and edges may be in incomparable subgraphs. It's giving "we don't define that" and honestly valid.
agcontains(g,obj) returns non-zero if obj is a member of (sub)graph g. agdelete(g,obj) is equivalent to agclose, agdelnode, and agdeledge for obj being a graph, node or edge, respectively. It returns -1 if obj does not belong to g. Not in my graph, bestie.
AGDATA, AGID, and AGTYPE are macros returning the specified fields of the argument object. The first is described in the RECORDS section above. The second returns the unique integer ID associated with the object. The last returns AGRAPH, AGNODE, and AGEDGE depending on the type of the object.
agnameof returns a string descriptor for the object. It returns the name of the node or graph, and the key of an edge. agobjkind is a synonym for AGTYPE. Name check, type check, slay.
β¨ ERROR REPORTING β¨
Okay bestie, let's talk error reporting because this library is literally so considerate. π It provides a variety of mechanisms to control the reporting of errors and warnings. At present, there are basically two types of messages: warnings and errors. β¨
A message is only written if its type has higher priority than a programmer-controlled minimum, which is AGWARN by default. The programmer can set this value using agseterr, which returns the previous value. Calling agseterr(AGMAX) turns off the writing of messages. π
The function agerr is the main entry point for reporting an anomaly. The first argument indicates the type of message. Usually, the first argument is AGWARN or AGERR to indicate warnings and errors, respectively. Sometimes additional context information is only available in functions calling the function where the error is actually caught. In this case, the calling function can indicate that it is continuing the current error by using AGPREV as the first argument. The remaining arguments to agerr are the same as the arguments to printf. π«Ά
The functions agwarningf and agerrorf are shorthand for agerr(AGWARN,...) and agerr(AGERR,...), respectively. Slay.
Some applications desire to directly control the writing of messages. Such an application can use the function agseterrf to register the function that the library should call to actually write the message. The previous error function is returned. By default, the message is written to stderr. π
Errors not written are stored in a log file. The last recorded error can be retrieved by calling aglasterr. Unless the printing of error messages has been completely disabled by a call to agseterr(AGMAX), standard error must not be wide-oriented, even if a user-provided error printing function is provided. π
The function agerrors returns non-zero if errors have been reported. β
πΈ EXAMPLE PROGRAM πΈ
Here's a whole lil program to show you the vibes. No cap, it's a full example:
#include <cgraph.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
typedef struct {
Agrec_t hdr;
int x;
int y;
int z;
} mydata;
int main(int argc, char **argv) {
Agraph_t *g;
mydata *p;
if ((g = agread(stdin, NULL))) {
int cnt = 0;
Agsym_t *attr = NULL;
while ((attr = agnxtattr(g, AGNODE, attr))) {
cnt++;
}
printf("The graph %s has %d attributes\n", agnameof(g), cnt);
// make the graph have a node color attribute, default is blue
attr = agattr_text(g, AGNODE, "color", "blue");
// create a new graph of the same kind as g
Agraph_t *h = agopen("tmp", g->desc, NULL);
// this is a way of counting all the edges of the graph
cnt = 0;
for (Agnode_t *v = agfstnode(g); v != NULL; v = agnxtnode(g, v)) {
for (Agedge_t *e = agfstout(g, v); e != NULL; e = agnxtout(g, e)) {
cnt++;
}
}
// attach records to edges
for (Agnode_t *v = agfstnode(g); v != NULL; v = agnxtnode(g, v)) {
for (Agedge_t *e = agfstout(g, v); e != NULL; e = agnxtout(g, e)) {
p = (mydata *)agbindrec(e, "mydata", sizeof(mydata), true);
p->x = 27; // meaningless data access example
((mydata *)(AGDATA(e)))->y = 999; // another example
}
}
}
return 0;
}
π¦ EXAMPLE GRAPH FILES π¦
Some graph files to bless your eyes:
digraph G {
a -> b;
c [shape=box];
a -> c [weight=29,label="some text"];
subgraph anything {
/* the following affects only x,y,z */
node [shape=circle];
a; x; y -> z; y -> z; /* multiple edges */
}
}
strict graph H {
n0 -- n1 -- n2 -- n0; /* a cycle */
n0 -- {a b c d}; /* a star */
n0 -- n3;
n0 -- n3 [weight=1]; /* same edge because graph is strict */
}
π SEE ALSO π
π BUGS π
Okay so there are some things that are a bit annoying, no cap:
- It is difficult to change endpoints of edges, delete string attributes or modify edge keys. The work-around is to create a new object and copy the contents of an old one (but new object obviously has a different ID, internal address, and object creation timestamp). π
- The API lacks convenient functions to substitute programmer-defined ordering of nodes and edges but in principle this can be supported. π€
- The library is not thread safe. π¨
π AUTHOR π
Stephen North, north@research.att.com, AT&T Research.
That's the whole vibe check, bestie. This page is fromLIBCGRAPH(3)and it's dated28 FEBRUARY 2013. Periodt. π β¨
π¦ where this page comes from
- fedora 44: graphviz-devel 14.1.2-2.fc44 Β· epl-1.0 AND cpl-1.0 AND bsd-3-clause AND mit AND gpl-3.0-or-later WITH bison-exception-2.2 AND apache-1.1 AND lgpl-2.0-or-later WITH libtool-exception AND smlnj AND hpnd-uc
- fedora 44: graphviz-devel 14.1.2-2.fc44 Β· epl-1.0 AND cpl-1.0 AND bsd-3-clause AND mit AND gpl-3.0-or-later WITH bison-exception-2.2 AND apache-1.1 AND lgpl-2.0-or-later WITH libtool-exception AND smlnj AND hpnd-uc
- fedora 44: graphviz-devel 14.1.4-2.fc44 Β· epl-1.0 AND cpl-1.0 AND bsd-3-clause AND mit AND gpl-3.0-or-later WITH bison-exception-2.2 AND apache-1.1 AND lgpl-2.0-or-later WITH libtool-exception AND smlnj AND hpnd-uc
- fedora 44: graphviz-devel 14.1.4-2.fc44 Β· epl-1.0 AND cpl-1.0 AND bsd-3-clause AND mit AND gpl-3.0-or-later WITH bison-exception-2.2 AND apache-1.1 AND lgpl-2.0-or-later WITH libtool-exception AND smlnj AND hpnd-uc
- nixpkgs unstable: graphviz 15.1.1
- ubuntu resolute: libgraphviz-dev 14.1.2-1ubuntu1 Β· EPL-1.0
- ubuntu stonking: libgraphviz-dev 14.1.2-1ubuntu2 Β· EPL-1.0
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms