Chart Desktop

1   Introduction

This is the documentation for Ravenbrook's Chart Desktop, version 2.0.

In this section, I will introduce you to Chart, get you started with it, take you through a couple of instant demos, and summarise how to set about embedding Chart in your application.

If you're looking for a ten minute pitch, read about what Chart is and what it can do, download the zipfile, and try out those instant demos.

1.1   What is Chart?

Here's an example of a graph. It has five nodes (or vertices) and ten edges.

images/smaller-sample.png

Here's a larger example. This graph has over 1500 nodes and 3300 edges.

images/larger-sample.png

Both of these images were generated by Chart.

There are two varieties of Chart. chart.ravenbrook.com is a free online service which converts graph specifications into images in either png or pdf format. Chart Desktop is a library which allows the user to display graph visualisations, and interact with them.

This document describes how to use Chart Desktop. From now on, I'll refer to the library simply as Chart.

1.2   What can Chart do?

The core of Chart consists of two systems for graph layout and drawing: the first for deciding where best to place the graph nodes in a visualisation, and the other for creating an interactive display based on the node locations. These systems are mature and feature-rich; we've been deploying them internally in our own applications for some years. We now want to make them more widely available and so we're working on a fully documented interface.

This version of Chart has the following features:

  • Creation and manipulation of: displays, graphs, nodes and edges;
  • Spring embedded layout style: user can view and control its iterative progress;
  • Rectangular nodes with optional multi-line labels and tooltip text;
  • Edges with optional arrowheads, multi-line labels and tooltip text;
  • Multi-edges;
  • Multiple simultaneous displays and graphs: any graph can be shown on any number of displays at a time;
  • Displays are top-level windows;
  • Nodes and edges can be added to or removed from a graph after it has been displayed;
  • Introspection;
  • Zooming, with navigation aids;
  • Printing;
  • Support for UTF-8;
  • High data throughput; full thread-safety; error handling with backtraces;
  • Data-driven examples via command line interface;
  • Available as a 32-bit Windows DLL.

Features present in Chart's internal subsystems but not yet exported or documented are all candidates for future versions of Chart. They include:

  • Facility to calculate layouts without displaying them (allowing the application to take full control over visualisation);
  • Provision of displays as child windows owned by the application;
  • Two additional layout styles (hierarchical and circular);
  • Configuration parameters for all three layout styles and for the display;
  • More comprehensive incremental updates;
  • Hiding of nodes and edges;
  • Images associated with nodes;
  • Colours and shapes for nodes, specified per-display;
  • Colours, widths and dash-styles for edges, specified per-display;
  • Interactive displays: selecting, popup menus, node dragging;
  • Gestures for adding nodes and edges to the graph;
  • Menu bar;
  • A wide range of callbacks;
  • Node / edge location tools;
  • Display alignment (meaning: the displays scroll and zoom in tandem);
  • Availability as a 64-bit DLL.

Right now, this is what we're working on inside Chart:

  • Faster, cleaner, better displays;
  • 3-dimensional spring-embedded layout;
  • Other platforms.

Making this happen

We'd love to be able to bring you these improvements.

We want Chart to remain free of charge for all its users.

We welcome funding. Without it we cannot continue working on this.

We are happy to accept donations via this Paypal link. If you would like to fund us to focus on specific issues, or if you wish to purchase a formal support contract, please get in touch first.

1.3   Obtaining and unpacking

Chart is available as a .ZIP archive. You can download the latest release of this version from http://chart.ravenbrook.com/downloads/chart-2.0.4/chart-2.0.4.zip. There's no installation process: just unpack the archive anywhere that's convenient to you.

Once unpacked, you'll find the following:

chart-2.0.4\
   chart.dll                 the Chart DLL
   examples\C\               a brief example
   include\                  C header file
   manual\                   this document, plus supporting files
   Microsoft.VC80.CRT\       runtime libraries used by Chart
   pyChart\                  example Python interface
   release-notes\            release notes and installation guide
   samples\                  some sample graphs

You'll find working with this document, and in particular the Python example, easiest if you keep all these files together.

1.3.1   Chart's Runtime Libraries

Tip

If you don't intend either to copy / move chart.dll after you've unpacked it, or to redistribute an application you've built using Chart, you can skip this small print.

Chart needs access to Microsoft's C Runtime and Standard C++ Libraries. If it can't find them it simply won't run.

images/incorrect-config.png

These runtime libraries can be either installed to a central location on your computer oras in the .ZIP archivepresent in a Microsoft.VC80.CRT\ directory alongside the Chart DLL.

  • Central location

    If the runtime libraries have already been installed on your machine (by the installer of some other application, say) then you can move or copy the Chart DLL as you wish, and it will continue to work.

  • Private copy

    Otherwise, if you move or copy the DLL you should keep a copy of Microsoft.VC80.CRT\ alongside it.

  • Redistribution

    If you redistribute an application based on Chart then you will need to revisit this issue. Advice on Redistributing Visual C++ libraries will be found on the MSDN website: here.

1.4   Instant demos

The samples\ directory contains a few sample graphs, one per file. You can run them either via the Python interface or directly from the command line by invoking the exported function chart_demo with rundll32, for example thus:

C:\home\chart> rundll32 chart.dll,chart_demo samples\simple.txt

1.5   Display controls

1.5.1   During the layout

The spring-embedded layout is an iterative process and Chart is configured to allow up to 100 iterations per subgraph; for subgraphs of 50 nodes or more you will be shown the layout's progress during these iterations.

If at any stage you feel that the layout of a subgraph is now good enough for your purposes you can halt further computation before those 100 iterations are complete, by pressing the Esc key.

images/progress.png

1.5.2   Zooming

There are two ways to zoom the display in or out.

  • With the keyboard

    The plus key enlarges the display by 25%, centred on the co-ordinates of the last mouse click in the display. The minus key reverses that operation and reduces the display. For ease of use, keyboard zooming ignores the shift key, so on a standard keyboard the equals key counts as plus, and the underscore counts as minus.

  • With the mouse

    Alternatively you can use the right mouse button to zoom the graph in or out by any amount you want.

    • To zoom in, sweep out a region of interest with the right mouse button down. This region will be expanded to fill the whole display.
    • To zoom out, sweep out a region with both the Alt key and right mouse button held down. The display will be contracted to fit into this region.

1.6   Using Chart in your application

How much work this is will depend on the language you're working in.

  • Python

    The pyChart\ directory contains a Python (version 2.7) package which uses the ctypes module to connect to Chart's external interface.

    I'll use pyChart in the sections that follow, to demonstrate the features of Chart.

    You can build your application directly on top of this package; see below for an example. So you might choose to skip reading Library infrastructure and go straight on to Working with Chart.

    Chart is a 32-bit library; you'll need a 32-bit Python installation.

  • C

    The distribution includes a header file which declares the library's external interface, the functions which implement it, and a (brief) sample application.

    You'll need to define your side of the interface. I recommend that you work through Library infrastructure and implement each section in turn. Once you can get the tests to pass, move on to Working with Chart.

  • Common Lisp

    Talk to us. Depending on your implementation, we can probably offer you a native interface for part or all of the library.

  • Anything else

    I suggest adapting the header file to your language — in other words, you need to be able to call every exported function — and then proceeding as for C above. Let us know how you get on.

1.7   Redistributing Chart with your application

The short answer is: "go ahead"; Chart's license permits you to do this under very permissive conditions.

Don't forget to include the runtime libraries in your distribution.

1.8   More instant demos

Before you start on the details of how to drive the Chart library, take a quick look at the following Python definition (which you'll also find in chart.py). Whatever your choice of implementation language, by the time you've built an interface equivalent to the next two sections you'll be able to drive Chart at a high level, like this:

def demo (filename):
    with open(filename) as f:
        (node_data, edge_data) = ast.literal_eval(f.read())
    graph = chart.Graph()
    nodes = graph.new_nodes(map(lambda (label): label.decode('utf-8'), \
                                node_data))
    new_edges(map(lambda ((source, destination, directional)): \
                  (nodes[source], nodes[destination], directional),
                  edge_data))
    display = chart.Display(filename)
    display.draw_graph(graph)
    return display

The function demo reads a graph description in the chart_demo format that we saw in the command line example before. It creates a graph, add nodes and then edges to the graph, opens a display window and, finally, draws the graph in that display.

>>> from pyChart import chart
>>> chart.demo('samples/simple.txt')
<Chart Display handle=0x20065928>
>>>

2   Library infrastructure

This section covers everything you need to know about driving Chart, other than how to work with graphs. I'll cover starting and stopping the library, function calls, error handling, the structures Chart uses, and memory and how to free it.

If you plan to drive Chart from Python then you can probably skip all this and go straight on to Working with Chart.

Otherwise, you'll have to start by implementing your own infrastructure layer, as described below. The C example might help, as should the following files in the Python interface:

invoke.py
Function call wrappers: extract results, check for errors
lib.py
Stubs for all calls into the DLL
objects.py
Implementations of the objects that Chart uses

In the following I'll use extracts from the Python interface to demonstrate how to drive the library. I'll assume some familiarity with Python's ctypes foreign function library.

The library's calling convention is stdcall.

2.1   Initialisation and termination

The Chart library initialises itself automatically, the first time you call any function in its interface. If you find this distasteful, you can initialise the library explicitly by calling chart_init().

Somewhat more important is to close the library gracefully when you're finished it, by calling chart_close(); failure to do may result in a warning message.

The Python interface arranges for this in connect.py, which also sets the global dll to point to the ctypes shared library object. Every export from the library is now a member of the dll object (for example, we can invoke the library function chart_init() by calling dll.chart_init from pyChart).

connect.py, along with everything else you'll need to go with it, is loaded via chart.py:

>>> from pyChart import chart
>>>

2.2   Calling Chart functions

All Chart functions return a result code of type chart_res_t. They indicate success by returning CHART_RES_OK, which is defined to be zero; they signal errors by returning CHART_RES_FAIL, which is non-zero, and retaining a string describing the error. You can retrieve the most recent error string by calling chart_last_error().

In the Python example, the function check in invoke.py examines the return value of a function call into the library. If this value is not OK (zero) then a ChartError is raised.

OK = 0

def check(func, *args):
    """
    Apply func to its args, checking the result code.
    """
    result = func(*args)
    if result != OK:
        raise ChartError()

Library functions which need to return some value in addition to the result code do so by reference: they take a pointer as their first argument and write through it.

Still in invoke.py, the function wrapper val creates just such a pointer. This pointer is prepended to func's argument list, the library is invoked, the result code checked, and the result is extracted. The wrapper void has the same form but is for calls which don't return a useful result.

def val(func):
    """
    Return a function which calls func with an additional pointer argument,
    checks the result, and dereferences the pointer.
    """
    def invoker(*args):
        pointer = ctypes.c_void_p()
        check(func, ctypes.byref(pointer), *args)
        return pointer.value
    return invoker

def void(func):
    """
    Return a function which calls func, checking the result.
    """
    def invoker(*args):
        check(func, *args)
    return invoker

2.3   Data model

Chart functions take arguments of the following types:

  • integer (signed and unsigned);
  • UTF-8 encoded string;
  • handle (denoting a display, graph, node or edge within the library);
  • structure (sequence of values whose length and constituent types are determined by the context in which it's being passed);
  • array (sequence of values whose length is not known in advance);
  • pointer to any of the above (for return values);
  • pointer to function (passed as an unsigned integer).

Handles are only ever generated by the library. When you have no further use for a handle (for instance: when removing a node from a graph, or when dropping an entire graph from the system) you should call chart_remove_objects().

Aggregate values — strings, structures (for example, a pair of integers representing a node's location) and arrays (for example, a list of node locations) — might be generated by either the library or your application.

The contents of any aggregate value created by your application, passed into Chart and retained there — for example, the string to be drawn inside some node, or an array of structures describing new nodes to be added to some graph — are copied by the library before your call returns. So you may immediately recycle the memory associated with such values.

Any aggregate value created within the library and passed to your application as return data will be retained by Chart, until you declare that you have no further use for it by calling the library function chart_free(). If you free a sequence (i.e. a structure or array) which itself contains any aggregate values, then these values will be freed recursively.

The Python interface has a simple stub for invoking chart_free():

def free(pointer):
    invoke.void(dll.chart_free)(pointer)

Note that chart_free() does not free handles, or release them in any sense. Freeing an array of edges releases the resources associated with the array itself and not those of the edges. (See instead chart_remove_objects().)

Warning

The library is designed to catch and recover from errors but it cannot be totally resilient to bad data: the only way for it to interpret an aggregate value (string, structure, array) is to dereference the corresponding pointer. If the pointer is invalid this may lead to an unhandled exception which could crash either the library or your application. If you're working in Python the following minimal check from invoke.py is suggested.

def plausible_address(address):
    """
    Return the address, checking that it refers to valid memory.

    This is only the most basic of checks. The address of a compound
    structure might pass this test but what it points to might be
    rubbish, and we can't check for that without going into a
    description of structure layout.

    The test works by loading one byte from the given address. If
    there's anything wrong with the address, Python will handle the
    error and raise one of its own Exceptions.
    """
    ctypes.string_at(address, 1)
    return address

2.4   Error handling

Let's see how errors might be handled. The ChartError exception defined in invoke.py demonstrates:

  • checking result codes;
  • a Chart function returning a value in addition to its result code;
  • use of chart_free() to release the resources associated with a string which originated inside the library.

Raising this exception calls chart_last_error() and reports the result. As corner cases, the exception checks whether (a) there actually was any error in the first place, (b) it was possible to report the error and (c) it was possible to chart_free() the error report. In the last case a warning note is tacked onto the head of the report rather than risking a chain of recursive errors.

class ChartError(Exception):
    def __init__(self):
        string = ctypes.c_char_p(None)
        error = dll.chart_last_error(ctypes.byref(string))
        if error != OK:
            self.string = 'Chart reports an error, ' \
                          + 'and an error reporting the error.'
        else:
            value = string.value
            if value:
                self.string = value.decode('utf-8')
                if not config.show_backtrace:
                    self.string = self.string.splitlines()[0]
                if dll.chart_free(string) != OK:
                    # Today isn't turning out very well. (Not that I
                    # was seriously expecting to end up on this branch.)
                    warning = '*** Warning: Chart was unable to free ' \
                              + 'the chart_last_error string. ***'
                    self.string = warning + '\n\n' + self.string
            else:
                self.string = 'How did this happen? There was no error in Chart.'

    def __str__(self):
        return self.string

(You'll note that I haven't made use of the bundling functions check, val or void here. That's because any failure they encountered would recursively raise another ChartError, which could rapidly spiral out of control.)

The string returned from chart_last_error() describes the most recent error signalled by the library. If this was a pilot error then you'll get a one line description of the problem. If alternatively some exceptional condition (say: division by zero) was caught then a backtrace will be included. This might be longer and less edifying than you care for; ChartError uses the configuration variable show_backtrace from config.py to decide whether to show you the backtrace.

If there is no error to report, chart_last_error() returns a null pointer.

Let's go under the hood. I'll bypasss all the bundling and checking functions (check etc.) and make a series of low-level calls, at the Python prompt, directly into the DLL:

  1. provoking a deliberate error (result code is not OK),
  2. calling chart_last_error() to obtain a string describing the error,
  3. chart_free()ing the string, and
  4. polling to see if there's another error string (which there isn't).
>>> import ctypes
>>> from pyChart.invoke import dll
>>> dll.chart_free(0xdeadbeef)
-1
>>> string = ctypes.c_char_p(None)
>>> dll.chart_last_error(ctypes.byref(string))
0
>>> string
c_char_p('Pointer to 0xdeadbeef is invalid and cannot be freed.\r\n')
>>> dll.chart_free(string)
0
>>> dll.chart_last_error(ctypes.byref(string))
0
>>> string
c_char_p(None)
>>>

The C equivalent to the above might be based on the following fragments:

#define ASSERT_OK(form)    \
   {chart_res_t res = (form); assert(res == CHART_RES_OK);}
#define ASSERT_FAIL(form)  \
   {chart_res_t res = (form); assert(res == CHART_RES_FAIL);}

chart_aggregate_t pointer;

pointer.string = (char*)0xdeadbeef;
ASSERT_FAIL(chart_free(pointer));

ASSERT_OK(chart_last_error(&(pointer.string)));
assert(strcmp(pointer.string,
              "Pointer to 0xdeadbeef is invalid and cannot be freed.\r\n")
       == 0);
ASSERT_OK(chart_free(pointer));

ASSERT_OK(chart_last_error(&(pointer.string)));
assert(pointer.string == 0);

2.5   Dereferencing

Working from Python, we'll need to be able to switch between ctypes instances and their memory addresses. (With the foreign interfaces of other languages, the corresponding solution might look very different. In C it'll be trivial.)

The pyChart function dereference_address in objects.py takes one argument which it treats as the memory address of a ctypes.c_uint. It dereferences this address and returns the integer. Dereferencing 0 returns None.

The function address_of returns the address of a ctypes instance's underlying data.

In the following example we create a ctypes array which contains an integer and the address of a ctypes string. By dereferencing the address of the array we are able to recover the integer; by dereferencing the address of the following word in memory we recover the address of the string.

>>> from pyChart import objects
>>> s = ctypes.c_char_p('hello')
>>> x = ctypes.pointer((ctypes.c_uint *2)(99, objects.address_of(s)))
>>> objects.address_of(s)
39344980L
>>> objects.address_of(x)
38708808L
>>> objects.dereference_address(38708808L)
99L
>>> objects.dereference_address(38708808L + 4)
39344980L
>>> hex(objects.dereference_address(_))
'0x6c6c6568L'
>>>

Tip

Watch out for the Python garbage collector!

>>> ctypes.c_char_p('goodbye')
c_char_p('goodbye')
>>> objects.address_of(_)
39343764L
>>> objects.dereference_address(_)
0L
>>>

In this case we haven't retained a pointer to the ctypes string. Its data has already been reclaimed and is no longer accessible to us.

2.6   Structures

A structure is a sequence of Chart values in memory.

Both the number of the values which constitute a structure, and their various types, are determined by the context in which that structure is used.

As a very simple example of structures in pyChart, I'll get values into and out of a structure used to hold a node location. In this case the structure consists of two values and both happen to have the same type: signed integers. I'll use the functions construct and deconstruct which are fully documented in objects.py and which basically do what I did the long way round in the dereferencing example above.

>>> from pyChart import objects
>>> location = (101, 234)
>>> x = objects.construct(location)
>>> x
<pyChart.objects.LP_c_ulong_Array_2 object at 0x024EA620>
>>> objects.deconstruct(objects.address_of(x), 2)
(101L, 234L)
>>>

2.7   Arrays

An array is another form of sequence of Chart values in memory .

This time the number of values is not determined by context but must be passed as part of the array. The types of an array's members are always the same.

Under the hood, arrays are implemented as structures. The first member of the structure is the array's length; the remaining members are the array's values.

Two examples, the first in C. The macro CHECK is defined in chart.h; it verifies that a library call has returned CHART_RES_OK.

#define CHART_STRUCTURE_SIZE(n) (offsetof(chart_structure_s, values) \
                                 + (n) * sizeof(chart_value_t))
#define CHART_ARRAY_SIZE(n)     (offsetof(chart_array_s, values)     \
                                 + (n) * sizeof(chart_value_t))

chart_aggregate_t freeable;
chart_array_t node_defs, nodes;
chart_handle_t graph, node;
chart_structure_t node_def;

/* Build a node definition. */
node_def = (chart_structure_t)alloca(CHART_STRUCTURE_SIZE(2));
node_def->values[0].aggregate.string = "foo";
node_def->values[1].aggregate.string = (char*)NULL;

/* Pack the node definition into an array. */
node_defs = (chart_array_t)alloca(CHART_ARRAY_SIZE(1));
node_defs->length = 1;
node_defs->values[0].aggregate.structure = node_def;

/* Add the node to the graph. */
CHECK(chart_new_nodes(&nodes, graph, node_defs));
node = nodes->values[0].handle;

/* Tell Chart it may free the array in which it returned the node. */
freeable.array = nodes;
CHECK(chart_free(freeable));

The second example uses pyChart:

>>> import ctypes
>>> from pyChart import objects
>>> s = map(ctypes.c_char_p, ['hello', 'goodbye'])
>>> x = objects.pack(s)
>>> x
<pyChart.objects.LP_c_ulong_Array_3 object at 0x022B9620>
>>> unwrap = lambda(name): (ctypes.cast(name, ctypes.c_char_p).value)
>>> objects.unpack(objects.address_of(x), unwrapfun=unwrap)
['hello', 'goodbye']
>>>

Pack and unpack are implemented in objects.py, in terms of construct and deconstruct above. Note that by default, unpack is designed to pass its array argument to chart_free() after the array's members have been extracted; this allows the Chart DLL to reclaim the memory pointed to by the array (and by any aggregate values which it contains). Overriding this behaviour is unlikely to be useful to you if the array originated inside the library; once you've unpacked an array there is no further use for it.

2.8   Handles

In Chart, objects are represented by handles. The library provides functions — chart_new_graph(), etc. — for constructing each of the various types of object, and each of these functions returns a handle.

In pyChart we define classes corresponding to each of the Chart object types — these are all subclasses of ChartObject which is defined in objects.py — and create instances of these classes to correspond to each new handle:

_objects = {0:None}

class ChartObject(object):
    def __init__(self, handle):
        _objects[handle] = self
        self.handle = handle

    def box(self):
        return self.handle

    def _discard(self):
        handle = self.handle
        del _objects[handle]
        self.handle = None

def unbox(handle):
    return _objects[handle]

We can now use arrays, box and unbox to demonstrate the use of chart_remove_objects():

def remove_objects(objects):
    box = lambda(x): x.box()
    invalids = unpack(lib.remove_objects(pack(objects, box)), unbox)
    for invalid in invalids:
        invalid._discard()

Note incidentally that:

In this example I use unbox as a debugging aid. Suppose I'm faced with the following error:

pyChart.invoke.ChartError: Source and destination are the same node
(#<Chart Node handle=0x20053748>), which is not permitted.

Then unbox can identify the object with handle 0x20053748, and chart_describe_nodes() can describe it for me:

>>> chart.objects.unbox(0x20053748)
<Chart Node handle=0x20053748>
>>> chart.describe_nodes([_])
[(<Chart Graph handle=0x200536b0>, [], u'Hello World!', None)]
>>>

2.9   Tests

Before moving on, you should conduct a test equivalent to the following. It will ensure that the library is working, that you are communicating with the library successfully and in particular that you are packing and unpacking arrays correctly.

In this test we:

  1. create and record the handles of two vanilla Chart objects;
  2. pass one of them to Chart and observe that chart_return_object() returns precisely the same handle;
  3. pass an array containing the pair to Chart and observe that chart_return_array() returns an equivalent array; and
  4. define an externally callable function which returns its argument, pass this plus one of the objects to Chart, and allow the library to apply our function to that object and confirm that the right value came back to it.
>>> import ctypes
>>> @ctypes.WINFUNCTYPE(ctypes.c_uint, ctypes.c_uint)
... def return_handle(handle):
...   return handle
...
>>> from pyChart.invoke import dll, void, val
>>> obj_1 = val(dll.chart_new_object)()
>>> obj_2 = val(dll.chart_new_object)()
>>> objs = [obj_1, obj_2]
>>> objs
[537280600, 537280728]
>>> val(dll.chart_return_object)(obj_1)
537280600
>>> objects.unpack(val(dll.chart_return_array)(objects.pack(objs)))
[537280600L, 537280728L]
>>> ctypes.c_bool(val(dll.chart_invoke_return_object)(return_handle, obj_1))
c_bool(True)
>>>

See also communications_test in objects.py.

3   Working with Chart

This section covers Chart's higher level functionality and in particular how to build graphs and draw them in displays.

If you plan to drive Chart from Python then you can simply use the classes and functions referenced in this section. They are all defined in chart.py.

Chart is a 32-bit library; you'll need a 32-bit Python installation.

>>> from pyChart import chart
>>>

Otherwise, you'll have to implement equivalents to these functions along with the library infrastructure discussed earlier.

I won't document every possible way to raise an exception while working with this code. Most ChartErrors have fairly obvious causes. Working with discarded objects (for instance: trying to print a display after the user has closed it) is the primary culprit. (The C functions reference is more thorough about error conditions.)

3.1   Objects

The objects with which Chart deals are: displays, graphs, nodes and edges.

A graph is said to contain some number of nodes and edges (including none at all). The nodes are entities with some form of interpretation in your application; the edges are connections between pairs of nodes. Nodes and edges have no meaning outside of their graph; a node can have any number of edges (including none at all); an edge always connects precisely two nodes: its source and its destination. Depending on interpretation an edge might be directional ("is the child of") or non-directional ("likes the same ice creams as").

When a graph is drawn in a display window, its nodes might be represented by (say) small rectangles each containing a short string which labels that node; its edges by lines which connect the nodes; and directionality where present by arrowheads on the edges.

In Chart a display is a window in which a graph can be drawn. Chart supports multiple simultaneous displays and graphs: any graph can be shown on any number of displays at a time.

3.2   Graphs

You can create a new graph by instantiating the class Graph.

When you no longer have any need for a graph, you can recycle its resources (including all of its nodes and edges) by passing a sequence containing the graph to remove_objects. This will also have the effect of passing None to the draw_graph methods of all the graph's displays (each such display is cleared, and no longer associated with the graph) .

>>> (g1, g2) = (chart.Graph(), chart.Graph())
>>> chart.remove_objects([g1])
>>> (g1, g2)
(<Chart discarded Graph>, <Chart Graph handle=0x20064618>)
>>>

3.3   Nodes

You can add nodes to a graph by calling the graph's new_nodes method. Pass one argument: a sequence of node definitions. Each definition corresponds to one new node; it can be either the node's label, or a tuple of length 2 (the node's label, and some popup text). Each label and text can be either a string or None (but consider: unlabelled nodes don't convey that much visual information). Strings can be multi-lined.

new_nodes returns a list of nodes, one for each definition and in the same order as the definitions to which they correspond.

>>> g = chart.Graph()
>>> g.new_nodes('Hello World!'.split())
[<Chart Node handle=0x200640d8>, <Chart Node handle=0x200642b0>]
>>>

Tip

If you're building a large graph, you'll get much better performance by making a small number of calls to new_nodes than you would if you called it once for each node.

A graph's nodes method returns a list of that graph's nodes, in the order in which they were constructed. The function describe_nodes takes a sequence of nodes and returns a list of corresponding node descriptions, each of which is a tuple consisting of: this node's graph, its list of edges, its label and its popup text.

>>> chart.describe_nodes(g.nodes())
[(<Chart Graph handle=0x200647b8>, [], u'Hello', None),
 (<Chart Graph handle=0x200647b8>, [], u'World!', None)]
>>>

To remove a set of nodes (along, automatically, with their edges) from a graph, pass the nodes to remove_objects:

>>> g = chart.Graph()
>>> nodes = g.new_nodes('Round the rugged rocks the ragged rascal ran'.split())
>>> chart.remove_objects(g.nodes()[1:4])
>>> len(g.nodes())
5
>>>

Adding or removing nodes does not force any displays to be redrawn. To update a display, call one of the layout functions.

3.4   Edges

The interface for edges is very close to that for nodes. Construct edges with the function new_edges; list a graph's edges with its edges method and describe a sequence of edges with the function describe_edges.

Edge definitions are always tuples. Each one consists of a new edge's source and its destination, and then optionally whether it is directional (default: True) and its label and text. The source and destination are both nodes. If they are the same node or not from the same graph then a ChartError is raised.

An edge description is a tuple consisting of the edge's graph followed by the elements which made up the edge's definition.

>>> nodes = g.nodes()
>>> chart.new_edges([(nodes[0], nodes[1])])
[<Chart Edge handle=0x200655f0>]
>>> chart.describe_edges(g.edges())
[(<Chart Graph handle=0x200647b8>, <Chart Node handle=0x200640d8>,
                                   <Chart Node handle=0x200642b0>,
                                   True, None, None)]
>>>

To remove a set of edges from a graph, pass them to remove_objects.

Adding or removing edges does not force any displays to be redrawn. To update a display, call one of the layout functions.

3.5   Displays

Making an instance of the class Display will open a new display window. You can specify the following values to the constructor:

  • title is the window's title, defaulting to 'Ravenbrook Chart';
  • dimensions is a tuple of two integers (width, height) defining the initial size of the new window;
  • position is another pair of integers, specifying the co-ordinates of the top-left corner of the window.

If dimensions is not specified the new display will occupy most of the screen; if either that or position is unspecified then the operating system will position the window for you.

>>> d = chart.Display('pyChart demo', (500, 500))
>>> d
<Chart Display handle=0x20053838>
>>>

There may or may not be a graph assigned to each display. This assignment is performed by the method draw_graph (which also invokes a layout operation — I'll postpone for now the mechanics of this). This method takes one argument: either a graph or None (meaning: no graph will be assigned to this display).

The method graph returns a display's graph (or None if no graph is currently set). Each graph can be assigned to any number of displays; the method displays on a graph returns a list of the graph's current displays.

>>> (g1, g2) = (chart.Graph(), chart.Graph())
>>> (d1, d2) = (chart.Display(), chart.Display())
>>> (g1.displays(), d1.graph())
([], None)
>>> d1.draw_graph(g1)
>>> d2.draw_graph(g1)
>>> d2.draw_graph(None)
>>> (g1.displays(), d1.graph())
([<Chart Display handle=0x200538b0>], <Chart Graph handle=0x20053748>)
>>>

The method invoke_print opens a control dialog for printing the display's current graph. This method takes an optional name for the print job, defaulting to the display's title.

To close a display programmatically you should pass it to remove_objects:

>>> chart.remove_objects([d1,d2])
>>> d2
<Chart discarded Display>
>>>

A display can also be closed by user action. When this happens the chart_display_terminated callback is invoked; in pyChart this is configured simply to call remove_objects with the display.

Your application might want to control whether the user may close the display at all. It can do so via the chart_display_terminate_requested callback. In this example, the user is not able to close any display to which a graph is currently assigned:

>>> import ctypes
>>> @ctypes.WINFUNCTYPE(ctypes.c_uint, ctypes.c_uint)
... def display_terminate_requested(displayRef):
...     display = chart.objects.unbox(displayRef)
...     return 1 if display.graph() is None else 0
...
>>> chart.set_callbacks(None, [('chart_display_terminate_requested', \
...                             display_terminate_requested)])
>>> d = chart.Display()
>>> d.draw_graph(chart.Graph())
>>>

The first argument to set_callbacks is either a display or None (meaning: these settings will apply to all displays for which no explicit settings apply). This is followed by a sequence of (callback, function) tuples; specify None for a function to unset that callback.

Tip

Beware that ctypes Booleans can be unreliable in this context. We use 1 for True, and 0 for False. The Chart library is fine with that.

3.6   Layouts

The Display method draw_graph assigns a new graph to a display, performs a layout operation, and shows the result in the display window. The method's argument is either the new graph or None (in which case the display's graph is unset and the display window cleared).

The layout in draw_graph is partial: a location is calculated for every node that needs it. See chart_draw_graph() for a more detailed description.

If you wish to force a full layout of the display's graph, with locations calculated for every node, use the method layout. This method doesn't take any arguments; you should only call it once you've assigned a graph to the display (otherwise a chartError will be raised). Alternatively, use set_node_locations (described below) to null out all the node locations before calling draw_graph.

A node can have different locations in each display. (It can even have locations in a display to which its graph hasn't yet been assigned.)

You can interrogate node locations in a display with its node_locations method, which takes a sequence of nodes and returns a list of corresponding locations (co-ordinate pairs, or None for unset locations).

You can set node locations with a display's set_node_locations method, which takes a sequence of (node, co-ordinate pair) tuples. Pass null co-ordinates to unset that node's location in the display.

>>> d = chart.Display()
>>> g = chart.Graph()
>>> n = g.new_nodes(['foo'])[0]
>>> d.node_locations([n])
[None]
>>> d.set_node_locations([(n, (100, 200))])
>>> d.node_locations([n])
[(100L, 200L)]
>>>

This logic allows your application to (say) save a large graph together with locations for all the nodes, and then restore it in a fresh Chart session without the delay of performing a full layout.

Caution

Because draw_graph finishes by translating and scaling all co-ordinates so that the graph fits neatly into the display, it's entirely possible that the sequence set_node_locations, draw_graph, node_locations will move nodes away from where you left them. If you need to know where a node is currently located, you should call node_locations.

4   Reference

4.1   Calling convention

The library's calling convention is stdcall.

4.2   Types

The following types are declared in chart.h.

chart_aggregate_t

An aggregate value is a string, structure, or array. Note that these are all pointer types.

The contents of any aggregate value created by your application, passed into Chart and retained there — for example, the string to be drawn inside some node — are copied by Chart before your call returns. So you may immediately recycle the memory associated with such values.

The contents of any aggregate value created within Chart and passed to your application will be retained by Chart until you declare that you have no further use for them. See the library function chart_free() for this.

typedef union chart_aggregate_t {
  char *string;               /* UTF-8 */
  struct chart_structure_s *structure;
  struct chart_array_s *array;
} chart_aggregate_t;

chart_array_t

An array is the memory address of a sequence of values whose length is not known in advance. The constituent types are all the same and are determined by context. For example, the edges attached to a particular node might be passed as an array of edge handles. The first item in any array is always the number of remaining items. (If the node has three edges; then the array consists of four values, the first of which is the number 3.)

Compare with structures, whose length is known in advance, but whose constituent types need not be the same.

typedef struct chart_array_s *chart_array_t;
typedef struct chart_array_s {
  chart_ulong_t length;
  chart_value_t values[1];    /* all the same members of chart_value_t */
} chart_array_s;

chart_handle_t

A handle denotes a display, graph, node or edge within the library. Each such object is assigned a handle when Chart creates it; and that handle can be used to unambiguously reference the object.

typedef chart_ulong_t chart_handle_t;

Handles can only be generated by the library. When you have no further use for a handle (for instance: when removing a node from a graph, or when dropping an entire graph from the system) you should call chart_remove_objects().

chart_long_t
chart_ulong_t

All integers are passed as one of these two types.

#include <stdint.h>

typedef int32_t chart_long_t;
typedef uint32_t chart_ulong_t;

chart_res_t

All chart functions return a value of type chart_res_t. This is an alias for int32_t:

typedef chart_long_t chart_res_t;

enum {
  CHART_RES_OK   =  0,        /* success */
  CHART_RES_FAIL = -1         /* failure */
};

If a call into Chart has failed, you should call chart_last_error() to find out why.

chart_structure_t

A structure is the memory address of a sequence of values whose length and constituent types (which need not all be the same) are determined by the context in which it's being passed. For example, the co-ordinates of a given node might be passed as a structure of two integers.

Compare with arrays, whose length is not known in advance, but whose constituent types must be the same.

typedef struct chart_structure_s *chart_structure_t;
typedef struct chart_structure_s {
  chart_value_t values[1];    /* maybe different members of chart_value_t */
} chart_structure_s;

chart_value_t

chart_value_t is a union of all other Chart types. The values passed within arrays and structures are of type chart_value_t. All values passed as arguments to library functions are of type chart_value_t (or, for functions which return some value in addition to the result code, chart_value_t*).

typedef union chart_value_t {
  chart_ulong_t uinteger;
  chart_long_t integer;
  chart_handle_t handle;
  chart_aggregate_t aggregate;
} chart_value_t;

4.3   Functions

The following functions are declared in chart.h. For sample calls into the DLL see also chart.c and brief.c.

Connecting and Disconnecting
chart_close
chart_init
Error Handling
chart_last_error
chart_raise_error
Chart Objects and Communication Tests
chart_invoke_return_object
chart_new_object
chart_request_error
chart_return_array
chart_return_object
Memory
chart_free
chart_remove_objects
Graphs, Nodes, and Edges
chart_new_graph
chart_new_edges
chart_new_nodes
chart_set_node_locations
Displays
chart_draw_graph
chart_layout_display
chart_new_display
chart_print_display
Callbacks
chart_set_callbacks
Introspection
chart_describe_edges
chart_describe_nodes
chart_display_graph
chart_graph_displays
chart_graph_edges
chart_graph_nodes
chart_node_locations
From the Command Line
chart_demo
chart_version
chart_res_t chart_close(void)

Make the Chart library quit.

If this function is not called before your process exits, the Chart library might issue a warning message about not being unloaded cleanly ("Idle process was still alive when DLL was unloaded...").

void chart_demo(void)

chart_demo is designed to be invoked only from the command line via rundll32. It should not be called from your application.

The command line syntax is:

rundll32 chart.dll,chart_demo <file>

The file will be parsed as a graph which will be shown in a display as if by calls to chart_new_display() and chart_draw_graph(). The file should read as a Python tuple of two sequences: a sequence of strings labelling the graph's nodes, and then a sequence of edge descriptions. Each edge description is itself a tuple (source, destination, directional), where source and destination are indices into the nodes and directional is either True or False. For example:

(["Hello", "World!"], [(0, 1, True)])

The files in the samples\ directory conform to this format and are suitable for processing by chart_demo.

When the display is closed the DLL will terminate. chart_demo does not return.

See also the Python function demo.

chart_res_t chart_describe_edges(chart_array_t *descriptions,
                                 chart_array_t edges)

Return an array of structures which describe the edges.

Edges is an array of edge handles. The resulting array contains structures corresponding to each edge, in the order in which the edges were passed. Each structure consists of six elements: the handles of the edge's graph, source and destination, a boolean for whether the edge is directional, the edge's label and finally its popup text, both as strings (or null pointers if these strings have not been set).

When you're done with the top-level array you should chart_free() it; doing so will also free the structures and strings.

chart_res_t chart_describe_nodes(chart_array_t *descriptions,
                                 chart_array_t nodes)

Return an array of structures which describe the nodes.

Nodes is an array of node handles. The resulting array contains structures corresponding to each node, in the order in which the nodes were passed. Each structure consists of four elements: the handle of the node's graph, an array of handles of the node's edges, the node's label and finally its popup text, both as strings (or null pointers if these strings have not been set).

When you're done with the top-level array you should chart_free() it; doing so will also free the structures, arrays of edges, and strings.

chart_res_t chart_display_graph(chart_handle_t *graph, chart_handle_t display)

Return the handle of display's graph.

Display is the handle of a display. If the display's graph isn't currently set - see chart_draw_graph() - then a null pointer is returned.

chart_res_t chart_draw_graph(chart_handle_t display, chart_handle_t graph)

Draw the graph in the display.

The display's chart_display_graph() is set to graph, and display is added to the graph's chart_graph_displays(); the graph is partially laid out (a location is calculated for every node that needs it); and finally the nodes and edges are drawn, so as to fit neatly into the display.

The partial layout operation works as follows: if every node in the graph already has a location in this display then locations are not recalculated. If any unlocated node has an unlocated neighbour then the full layout is recalculated. Otherwise the unlocated node(s) are incrementally added to the existing layout.

Note that this logic allows your application to (say) save a large graph together with locations for all the nodes, and then restore it in a fresh Chart session without the delay of performing a full layout. Conversely, to force a full layout you can null the locations of (for simplicity) all the nodes. See also chart_layout_display().

The argument display is the handle of a display, and graph is either the handle of a graph, or a null pointer to set that display's graph to nothing and clear the display.

If the display has been closed, then an error is signalled.

chart_res_t chart_free(chart_aggregate_t pointer)

Instruct the Chart library to free the memory referenced by pointer.

Pointer may be any aggregate value which originated in the Chart library. If it references a sequence (i.e. a structure or array), then any aggregate values within that sequence will be freed recursively.

If pointer isn't a valid reference (for instance, if it's just some number you made up) then an error is signalled.

chart_res_t chart_graph_displays(chart_array_t *displays, chart_handle_t graph)

Return an array of the graph's displays.

Graph is a graph handle. The resulting array contains the handles of all the graph's displays, in the order in which they were used to draw the graph (i.e. in the order of calls to chart_draw_graph()). When you're done with this array you should chart_free() it.

chart_res_t chart_graph_edges(chart_array_t *edges, chart_handle_t graph)

Return an array of the graph's edges.

Graph is a graph handle. The resulting array contains the handles of all the graph's edges, in the order in which they were constructed. When you're done with this array you should chart_free() it.

chart_res_t chart_graph_nodes(chart_array_t *nodes, chart_handle_t graph)

Return an array of the graph's nodes.

Graph is a graph handle. The resulting array contains the handles of all the graph's nodes, in the order in which they were constructed. When you're done with this array you should chart_free() it.

chart_res_t chart_init(void)

Initialise the Chart library.

Calling this function is optional, in that calling any function in the Chart interface will automatically ensure that Chart has been initialised. If however you prefer to initialise the library explicitly then you're welcome to do so.

chart_res_t chart_invoke_return_object(bool *success,
                                       chart_handle_t(*)(chart_handle_t) funct,
                                       chart_handle_t object)

Ask Chart whether applying the function to the handle returns the same handle.

The only conceivable purpose of this function is to allow you to conduct communication tests with the library while you're porting to it. It answers the questions: are you passing function pointers correctly? and are you successfully interpreting handles to pre-existing objects?

Your function funct should take one argument, a handle. It should interpret this handle as a Chart object, in whatever way is appropriate to your application; then it should convert that object back to a handle and return this new handle. If the application's data model is simply to work with raw handles, then it's fine for this function to just return its argument; doing that otherwise would be defeating the purpose of the test.

Chart decodes the handle argument (object) into one of its own objects. It calls the function with that object's handle. It expects the result of this function call to be a handle which it can decode and compare with the original object, returning true if they were the same.

chart_res_t chart_last_error(char **error_string)

Return a string describing the last error to occur within the Chart library.

The string consists of a brief error message possibly followed by a verbose stack backtrace (note for Python users: with most recent call first).

If you're getting an error you don't understand, please forward it without delay to chart@ravenbrook.com and we'll help you interpret it if the problem was at your end, or get a fix to you if the fault was inside Chart.

If no error has occurred in this session, or at least none since a previous call to chart_last_error(), then chart_last_error() returns a null pointer.

When you're done with the error string you should chart_free() it.

See the ChartError Exception class in invoke.py for a cautious example.

chart_res_t chart_layout_display(chart_handle_t display)

Perform a full layout of the display's graph (locations are calculated for every node), and then redraw the nodes and edges so as to fit neatly into the display.

See also chart_draw_graph().

If the display's graph isn't currently set, or if the display has been closed, then an error is signalled.

chart_res_t chart_new_display(chart_handle_t *display,
                              char *title,
                              chart_structure_t dim,
                              chart_structure_t pos)

Open a display window and return its handle.

Several displays can be open at the same time, showing any number of graphs between them. Conversely, a graph can be simultaneously shown in any number of displays.

The title is an optional string to be written in the display's title bar. If this is null then the display's title will be "Ravenbrook Chart".

The width and height of the new display are specified by dim which is an optional structure of two chart_ulong_t. If this structure is null then the display will occupy most of the screen: 7/8 of its width and 2/3 of its height.

The final argument is another optional structure of two chart_ulong_t: the (x, y) co-ordinates of the top-left corner of the display. (Note: the origin is the top-left of the screen; down is positive for the y co-ordinate.) If either this structure or the dimensions are null then the operating system will position the window.

This is the only library function that generates new display handles.

To close a display programmatically you should chart_remove_objects() an array containing its handle.

If the user tries to close the display you will receive a chart_display_terminate_requested callback.

If the user succeeds in closing the display you will receive a chart_display_terminated callback. At this point the display is still open to introspection; but when you've finished with it you should, as above, chart_remove_objects() an array containing its handle.

chart_res_t chart_new_edges(chart_array_t *edges,
                            chart_array_t definitions)

Return an array of handles for new edges.

The edges are added to the graph's internal structure but no displays are changed. To update a display, see chart_draw_graph() or chart_layout_display().

The last argument is an array of structures, each of five elements which will define one new node: the edge's source (a node handle), its destination (ditto), a boolean for whether the edge is directional (to be drawn with an arrow), its label and its popup text. These last two values are optional strings; use a null pointer for either string if you don't want to specify it.

An error is signalled if the source and destination come from different graphs, or if they're the same node.

The handles in the returned array are in the same order as the definitions to which their edges correspond. This is the only library function that generates new edge handles. When you're done with the array you should chart_free() it.

chart_res_t chart_new_graph(chart_handle_t *graph)

Return the handle for a new graph.

Your next steps might be to make some nodes and edges, and then display the graph; see chart_new_nodes(), chart_new_edges() and chart_draw_graph().

When you're done with the graph you should chart_remove_objects() an array containing its handle.

This is the only library function that generates new graph handles.

chart_res_t chart_new_nodes(chart_array_t *nodes,
                            chart_handle_t graph,
                            chart_array_t definitions)

Return an array of handles for new nodes.

The nodes are added to the graph's internal structure but no displays are changed. To update a display, see chart_draw_graph() or chart_layout_display().

The last argument is an array of structures, each of two elements which will define one new node: the node's label, and its popup text. Both of these strings are optional (but consider: unlabelled nodes don't convey that much visual information). Use a null pointer for either string if you don't want to specify it.

The handles in the resulting array are in the same order as the definitions to which their nodes correspond. This is the only library function that generates new node handles. When you're done with the array you should chart_free() it.

chart_res_t chart_new_object(chart_handle_t *object)

Return the handle for a new, undifferentiated, Chart object.

The only conceivable purpose of such an object is to allow you to conduct communication tests with the library while you're porting to it. See chart_return_object(), chart_return_array() and chart_invoke_return_object() for test functions, and communications_test in objects.py for a complete example.

To create more interesting Chart objects, consider: chart_new_graph(), chart_new_nodes(), chart_new_edges() and chart_new_display().

When you're done with the object you should chart_remove_objects() an array containing its handle.

chart_res_t chart_node_locations(chart_array_t *result,
                                 chart_handle_t display,
                                 chart_array_t nodes)

Return an array of the locations of nodes in display.

There are a number of reasons why a node will have different locations in different displays. Most of these refer to library features which haven't been exported yet; one example which you can easily verify is that the layout process is dependent on the size and in particular the current aspect ratio of the display concerned.

Display is the handle of a display; nodes is an array of node handles. The result is an array of locations, in the same order as the corresponding nodes. Each location is either a structure of two chart_long_t (the x and y co-ordinates of that node), or a null pointer for any node which doesn't have a location in that display. When you're done with this array you should chart_free() it.

The co-ordinate system used for node locations is abstract and does not map one-to-one onto pixel locations: the scaling and origin are arbitrary. The drawing algorithm in chart_draw_graph() internally scales the laid out graph to fit the display and then centres it so that there's even space all round. Note that down is positive for the y co-ordinate.

See also chart_set_node_locations().

Caution

Because both chart_draw_graph() and chart_layout_display() finish by translating and scaling all co-ordinates so that the graph fits neatly into the display, it's entirely possible that the sequence chart_set_node_locations(), chart_draw_graph(), chart_node_locations() will move nodes away from where you left them. If you need to know where a node is currently located, you should call chart_node_locations().

chart_res_t chart_print_display(chart_handle_t display, char *jobname)

Invoke the printer control dialog.

Display is the handle of a display; jobname is an optional name for the print job (a null pointer specifies that the jobname will be the display's title).

If the display's graph isn't currently set, or if the display has been closed, then an error is signalled.

chart_res_t chart_raise_error(char *error_string)

Signal an error whose report is error_string. The string should have originated in a chart_advise_condition callback; it will be automatically passed to chart_free() by this call.

See also chart_request_error().

chart_res_t chart_remove_objects(chart_array_t *result, chart_array_t array)

Invalidate objects within Chart and permit memory to be recycled.

Array may contain any number of handles for displays, graphs, nodes and edges. Once a handle has been removed the corresponding Chart object becomes invalid: you should no longer communicate with Chart about that object and if you attempt to do so an error will be signalled.

Invalidating a graph implicitly invalidates all of that graph's nodes and edges. There's no need to explicitly invalidate those nodes and edges yourself — in other words you need not pass their handles to chart_remove_objects(). Invalidating a graph automatically invokes chart_draw_graph() with a null graph on all of that graph's displays.

Invalidating a node updates its graph so that this no longer refers to the node, and automatically invalidates all the node's edges. There's no need to explicitly invalidate those edges yourself.

Invalidating an edge updates its source and destination nodes, along with their graph, so that none of them refer to that edge any more.

Explicitly invalidating a node or edge does not force a redisplay of its graph. To update a display, see chart_draw_graph() or chart_layout_display().

Invalidating a display closes the display window but does not otherwise affect its graph. You can continue to refer to the graph via its handle, draw it in other displays, and so on.

chart_remove_objects() returns a new array itemising all the handles whose objects have been invalidated. If you've removed a graph this array will include handles for not just the graph but also its nodes and edges; if you've removed a node the array will include handles for the node's edges. When you're done with this array you should chart_free() it.

chart_res_t chart_request_error(chart_handle_t display, char *error_string)

Cause an error to be signalled. If display is null the error will come straight back (via CHART_RES_FAIL); if it's a display handle then the error will occur in that display's thread and the chart_advise_condition callback will be invoked.

Error-string is any text of your choosing.

The only conceivable purpose of this function is to allow you to conduct tests with the library while you're porting to it. It helps answer the question: are you handling errors correctly?

chart_res_t chart_return_array(chart_array_t *result, chart_array_t array)

Return a fresh array, containing the same handles as this one.

The only conceivable purpose of this function is to allow you to conduct communication tests with the library while you're porting to it. It answers the question: can you pack and unpack arrays correctly?

The argument should be an array of handles. Chart unpacks the array to get at the handles, decodes the handles to obtain a sequence of its own objects, and from this packs a fresh array of those objects' handles. As with chart_return_object(), if array isn't a valid array or its constituent handles handles, then an error is signalled. However the onus is on the application to check that interpreting the array as a memory address will not be problematic.

When you're done with the returned array you should chart_free() it.

chart_res_t chart_return_object(chart_handle_t *result, chart_handle_t object)

Return the object's handle.

The only conceivable purpose of this function is to allow you to conduct communication tests with the library while you're porting to it. It answers the question: are you recording new handles correctly?

This function isn't totally trivial: Chart decodes the handle into one of its own objects and then returns that object's handle. If your handle isn't a valid handle (for instance, if it's just some number you made up) then Chart can't process it and an error is signalled.

chart_res_t chart_set_callbacks(chart_handle_t display, chart_array_t callbacks)

Establish callbacks in this display.

The display argument is either the handle of a display or null; callbacks is an array of two-element structures, each containing the name of one of the documented callbacks and either a function pointer to be called when Chart wants to invoke that callback, or a null pointer to remove that callback.

If display is null then each callback set (or removed) applies to all displays for which that callback has not been set explicitly.

It is not an error for Chart to attempt to invoke a callback which isn't currently set: in this case nothing happens. If, for example, if there were a callback called chart_node_double_selected and you didn't want that callback to do anything, you might simply choose not to set that callback. For any callback which returns a value, I will document what happens if the callback isn't set.

chart_res_t chart_set_node_locations(chart_handle_t display,
                                     chart_array_t nodes_and_locations)

Set the locations of nodes in display.

Display is the handle of a display; nodes_and_locations is an array. Each member of the array is a structure, whose first element is a node and whose second is either a structure of two chart_long_t (the new x and y co-ordinates of that node), or a null pointer (to unset the node's location in that display).

Note that there's no requirement for the nodes to be mapped in the display when this function is called.

See also chart_node_locations().

void chart_version(void)

chart_version is designed to be invoked only from the command line via rundll32. It should not be called from your application.

The command line syntax is:

rundll32 chart.dll,chart_version

The current version string is printed to stdout. The DLL will then terminate. chart_version does not return.

For example:

C:\home\chart> rundll32 chart.dll,chart_version
Ravenbrook Chart, Desktop edition, release 2.0.4.
C:\home\chart>

4.4   Callbacks

The following callbacks are declared in chart.h. For how to set and unset callbacks, and what happens if an unset callback is invoked, see chart_set_callbacks().

void chart_advise_condition(chart_handle_t display, char *error_string)

If an error occurs during a call to one of the library functions then an error is signalled.

If an error occurs at some other time then the option of returning the value CHART_RES_FAIL isn't available — there's no function call from which to return anything. Instead, this callback is invoked.

If the error occurred in a thread associated with one of the displays, then display is that display's handle (otherwise display is null).

The error_string is of the same form as the reports returned by calls to chart_last_error().

You have two choices when programming your application to receive this callback.

  • Deal with the error_string and then chart_free() it.
  • Call chart_raise_error() which will turn the error over to whatever mechanism you have for dealing with CHART_RES_FAIL.
void chart_display_terminate_requested(chart_handle_t display)

This callback is invoked if the user attempts to close display. If the callback function returns true then the display will close (and the chart_display_terminated callback will be invoked); if the return value is false the window will not close.

If this callback is unset then an attempt to close a display always succeeds.

bool chart_display_terminated(chart_handle_t display)

This callback is invoked when the user closes display. At this point the display is still open to introspection; but when you've finished with it you should chart_remove_objects() an array containing its handle.

The callback is not invoked when the application closes display by calling chart_remove_objects() with an array containing its handle.

5   Glossary

Directional

Some interpretations of an edge might be symmetric between the two nodes it connects (for example: "went to the same school as"). Others (for example: "likes" when the nodes represent people) might not be, and it is then useful to be able to distinguish the roles of the nodes (who was liking and who was liked).

When the interpretation is asymmetric, the edge is referred to as directional. When the graph is drawn, directional edges might be denoted by arrowheads.

Display

The window in which a graph can be drawn.

Draw

Once the layout has been performed and positions assigned to all the nodes, the graph can be drawn in the display.

In this version of Chart, nodes are drawn as rectangles with white backgrounds, and edges as thin, black lines connecting their nodes. Future versions will permit control over colours, node shapes and edge widths.

Edge

An edge is a link between two nodes.

When the graph is drawn each edge is shown as a line joining its two nodes.

Whether or not an edge is directional, its two nodes are called its source and its destination.

Graph

A graph represents a set of objects, some of which are linked to each other in pairs. Terminology varies; we call the objects nodes and the links edges.

There are many possible uses and interpretations for graphs: a family tree, a social network, similarity of complex organic molecules in a scientific study, actors and films (with the links denoting "participated in"), and so on.

The purpose of Chart is to help you visualise the structure of graphs.

(See also this article in Wikipedia.)

Layout

Before a graph can be drawn, positions need to be assigned to each of its nodes. This process is known as the layout.

A number of different layout styles exist, each best suited to a different interpretation of the graph. This version of Chart supports one layout style: Spring embedded; future versions will support others.

Multi-edge

A multi-edge is a set of two or more edges which all have the same source and the same destination.

Node

A node is one of the constituent objects of a graph. Pairs of nodes are linked together by edges.

When the graph is drawn each node is shown as a small shape, such as a rectangle. Typically some text is used to label the node.

Spring embedded

A layout style in which the edges are treated as springs, and the nodes as repelling magnets. The nodes start off at random locations and the layout is allowed to progress under the influence of these springs and magnets until locations have settled down.

Spring embedded layouts ignore edge directionality. They tend to emphasize symmetry (as opposed to hierarchy) within the graph structure. To their disadvantage, they are costly (i.e. slow) to compute, and can get fooled if the initial random placement of nodes is too tangled.

(See also this article in Wikipedia.)

Subgraph

In this document, a subgraph means some part of a graph (that is: a subset of its nodes and their edges) which is not connected by any other edges to the rest of the graph. For example, this graph has one large subgraph and several dozen smaller ones.

6   Troubleshooting

The key message is: if something's not working and you don't understand why, get in touch. We'll help you interpret the problem, and get a fix to you if the fault was inside Chart.

Please include the version string.

At Ravenbrook we take the confidentiality of our users very seriously. It will help us to see as much context as you are able to send us, in keeping with your own confidentiality and other interests.


images/lisp.jpg

Copyright (c) 2009-2013, Ravenbrook Limited.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.

Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.