All versions of this documentation
X

GraphQL

This examples shows how to handle a GraphQL endpoint to visualize a graph. The GraphQL queries make use of GraphQL Aliases to simplify the convertion flow, but it is not required.

The GraphQL endpoint can be explored here, and is based on a MongoDB + Relay GraphQL server.
The network data is based on the Northwind database: is a sample database used by Microsoft that contains the sales data for Northwind Traders, a fictitious specialty foods export-­import company.
No GraphQL client library is used in this demo, but feel free to check your favourite GraphQL client library.

Open in a new window.
          <!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <script src="../build/ogma.min.js"></script>
    <link href="fonts/font-awesome/css/font-awesome.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.19.0/themes/prism.min.css"
        integrity="sha256-cuvic28gVvjQIo3Q4hnRpQSNB0aMw3C+kjkR0i+hrWg=" crossorigin="anonymous" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.19.0/prism.min.js"
        integrity="sha256-YZQM6/hLBZYkb01VYf17isoQM4qpaOP+aX96hhYrWhg=" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.19.0/plugins/autoloader/prism-autoloader.min.js"
        integrity="sha256-WIuEtgHNTdrDT2obGtHYz/emxxAj04sJBdMhRjDXd8I=" crossorigin="anonymous"></script>
    <style>
        #graph-container {
            top: 0;
            bottom: 0;
            left: 0;
            right: 0;
            position: absolute;
            margin: 0;
            overflow: hidden;
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            -o-user-select: none;
            user-select: none;
        }

        #info {
            position: absolute;
            color: #fff;
            background: #141229;
            font-size: 12px;
            font-family: monospace;
            padding: 5px;
            top: 0;
            left: 0;
            white-space: pre;
        }

        .toolbar {
            display: block;
            position: absolute;
            top: 20px;
            right: 20px;
            padding: 10px;
            box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
            border-radius: 4px;
            background: #ffffff;
            color: #222222;
            font-weight: 300;
            min-width: 200px;
        }

        .toolbar h3 {
            display: block;
            font-weight: 300;
            border-bottom: 1px solid #ddd;
            color: #606060;
            font-size: 1rem;
            font-family: Arial, Helvetica, sans-serif;
        }

        .controls {
            text-align: center;
            margin-top: 5px;
        }

        .btn {
            padding: 6px 8px;
            background-color: white;
            cursor: pointer;
            font-size: 18px;
            border: none;
            border-radius: 5px;
            outline: none;
        }

        .btn:hover {
            color: #333;
            background-color: #e6e6e6;
        }

        .menu {
            border: 1px solid #ddd;
            width: 80%;
            font-size: 14px;
            margin-top: 10px;
        }

        pre {
            max-width: 350px;
            max-height: 300px;
            overflow-y: scroll;
        }
    </style>
</head>

<body>
    <div id="graph-container"></div>
    <div class="toolbar">
        <h3>Current GraphQL query</h3>
        <pre><code class="language-graphql" id="query-text"></code></pre>
        <pre><code class="language-json" id="variables-text"></code></pre>
        <div class="controls">
            <button id="layout" class="btn menu">Layout</button>
        </div>
    </div>
    <div id="info">loading...</div>

    <script>
        'use strict';

        var ogma = new Ogma({
            container: 'graph-container'
        });

        // This is a very simple function that wraps some GraphQL configuration
        function queryGraphQL(query, variables) {
            return fetch('https://graphql-compose.herokuapp.com/northwind/', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    Accept: 'application/json'
                },
                body: JSON.stringify(query)
            }).then(function (response) {
                return response.json();
            });
        }

        function toggleLoading() {
            var el = document.querySelector('#info');
            var enable = el.style.display;
            el.style.display = enable === 'block' ? 'none' : 'block';
        }

        function updateGraphQLQuery(query) {
            // Update the query
            var codeBlock = document.querySelector('#query-text');
            codeBlock.innerHTML = query.query;
            Prism.highlightElement(codeBlock);
            // Update the variables
            var jsonBlock = document.querySelector('#variables-text');
            jsonBlock.innerHTML = JSON.stringify(query.variables);
            Prism.highlightElement(jsonBlock);
        }

        function getStartTemplate() {
            return getExpandProductQuery({ name: 'Northwoods Cranberry Sauce' });
        }

        function getExpandCustomerQuery(filter) {
            return {
                query: [
                    'query getCustomer($filter: FilterFindOneCustomerInput) {',
                    '   viewer {',
                    '       item: customer(filter: $filter){',
                    '           id: customerID,',
                    '           name: companyName,',
                    '           type: __typename',
                    '           outwards: orderConnection(first: 20) {',
                    '               edges {',
                    '                   node {',
                    '                       id: orderID,',
                    '                       name: orderID,',
                    '                       type: __typename',
                    '                   }',
                    '               }',
                    '           }',
                    '       }',
                    '   }',
                    '}'
                ].join('\n'),
                variables: { filter: filter }
            };
        }

        function getExpandOrderQuery(filter) {
            // from the order find ouy all the associated products
            return {
                query: [
                    'query getOrder($filter: FilterFindOneOrderInput) {',
                    '  viewer {',
                    '       item: order(filter: $filter){',
                    '           id: orderID,',
                    '           name: orderID,',
                    '           type: __typename',
                    '           outwards: details {',
                    '               product {',
                    '                   id: productID,',
                    '                   name: name,',
                    '                   type: __typename',
                    '               }',
                    '           }',
                    '           inwards: customer {',
                    '               id: customerID,',
                    '               name: companyName,',
                    '               type: __typename',
                    '           }',
                    '       }',
                    '   }',
                    '}'
                ].join('\n'),
                variables: { filter: filter }
            };
        }

        function getExpandProductQuery(filter) {
            return {
                query: [
                    'query getProduct($filter: FilterFindOneProductInput) {',
                    '  viewer {',
                    '       item: product(filter: $filter){',
                    '           id: productID,',
                    '           name: name,',
                    '           type: __typename',
                    '           inwards: orderList(limit: 20) {',
                    '               id: orderID,',
                    '               name: orderID,',
                    '               type: __typename',
                    '           }',
                    '       }',
                    '   }',
                    '}'
                ].join('\n'),
                variables: { filter: filter }
            };
        }

        function getExpandQuery(id, type) {
            // each query here is designed to always return items in the format
            // { id, name, type, inwards: [], outwards: [] }
            // The shape of in/outwards properties may be different for each type,
            // but we're handling it in a single function
            switch (type) {
                case 'Product':
                    return getExpandProductQuery({ productID: id });
                case 'Customer':
                    return getExpandCustomerQuery({ customerID: id });
                case 'Order':
                    return getExpandOrderQuery({ orderID: id });
                default:
                    return getStartTemplate();
            }
        }

        function createNode(item) {
            return { id: item.id, data: { type: item.type, name: item.name } };
        }

        function createEdge(source, target) {
            return {
                id: 'edge-' + source.id + '-' + target.id,
                source: source.id,
                target: target.id
            };
        }

        function extractWithDirection(item) {
            return {
                out: extractItems(item.outwards),
                in: extractItems(item.inwards)
            };
        }

        // This function must return an array of items
        // here we handle the whole domain complexity of the different relationships shape
        function extractItems(items) {
            if (!items) {
                return [];
            }
            // This edges+nodes structure comes from a Relay GraphQL endpoint,
            // when a <item>Connection() is used in the query (like the productConnection)
            if (items.edges) {
                return items.edges.map(function (wrapper) {
                    return wrapper.node;
                });
            }
            // Orders <--> Products have a to pass thru the details relationship
            // and here we're flattening it
            if (Array.isArray(items)) {
                var isWrapped = items.every(function (item) {
                    return item.product;
                });
                if (isWrapped) {
                    return items.map(function (wrapper) {
                        return wrapper.product;
                    });
                }
                return items;
            }

            return [items];
        }

        // GraphQL format -> RawGraph Ogma's format
        function toOgmaFormat(json) {
            // check inside the json.data.viewer for the data
            var graph = { nodes: [], edges: [] };
            var source = createNode(json.data.viewer.item);
            graph.nodes.push(source);
            // at this point items is in the shape {in: [...nodes], out: [...nodes]}
            var items = extractWithDirection(json.data.viewer.item);
            // iterate now on INs and OUTs sets and create nodes/edges
            items.in.forEach(function (item) {
                var target = createNode(item);
                graph.nodes.push(target);
                graph.edges.push(createEdge(target, source));
            });
            items.out.forEach(function (item) {
                var target = createNode(item);
                graph.nodes.push(target);
                graph.edges.push(createEdge(source, target));
            });
            return graph;
        }

        function sendQuery(id, type) {
            var query = getExpandQuery(id, type);
            toggleLoading();
            updateGraphQLQuery(query);
            return queryGraphQL(query)
                .then(function (json) {
                    // We using GraphQL alias on in queries so that the result is mostly normalized at this point
                    var graph = toOgmaFormat(json);
                    toggleLoading();
                    return ogma.addGraph(graph);
                })
                .then(function () {
                    return layout();
                });
        }

        function layout() {
            return ogma.layouts.forceLink({
                duration: 550,
                locate: { padding: 80 }
            });
        }

        // Define what to use as node and edge captions.
        ogma.styles.addEdgeRule({
            color: function (e) {
                return '#92e5a1';
            },
            shape: 'arrow'
        });

        ogma.styles.addNodeRule({
            text: {
                content: function (n) {
                    if (n.getData('type') === 'Order') {
                        return 'Order #' + n.getId();
                    }
                    return n.getData('name');
                },
                backgroundColor: 'white'
            },
            icon: {
                content: function (n) {
                    var type = n.getData('type');
                    if (type === 'Product') {
                        return '\uf290';
                    }
                    if (type === 'Customer') {
                        return '\uf275';
                    }
                    // type === 'Order'
                    return '\uf07a';
                },
                font: 'FontAwesome',
                color: function (n) {
                    var type = n.getData('type');
                    if (type === 'Customer') {
                        return '#000';
                    }
                    if (type === 'Product') {
                        return 'rgb(61,139,223)';
                    }
                    // type === 'Order'
                    return '#204829';
                },
                minVisibleSize: 0
            },
            outerStroke: {
                color: '#204829',
                width: 2
            },
            color: 'white'
        });

        ogma.events.onDoubleClick(function (evt) {
            if (evt.target && evt.target.isNode) {
                sendQuery(evt.target.getId(), evt.target.getData('type'));
            }
        });

        document.querySelector('#layout').addEventListener('click', function (evt) {
            evt.preventDefault();
            layout();
        });

        sendQuery();
    </script>
</body>

</html>