diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
index 7c877035..661b8b03 100644
--- a/DEVELOPMENT.md
+++ b/DEVELOPMENT.md
@@ -43,7 +43,7 @@ To be able to develop and run diagrams locally on you Mac device, you should hav
1. Go to diagrams root directory.
-2. Install poetry, the Python project management packge used by diagrams.
+2. Install poetry, the Python project management package used by diagrams.
```shell
pip install poetry
diff --git a/README.md b/README.md
index 0633255b..f958ab2e 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ Diagrams lets you draw the cloud system architecture **in Python code**. It was



+
## Getting Started
diff --git a/config.py b/config.py
index f527ab6e..cbec9c9d 100644
--- a/config.py
+++ b/config.py
@@ -411,7 +411,8 @@ ALIASES = {
},
"programming": {
"framework": {
- "Fastapi": "FastAPI"
+ "Fastapi": "FastAPI",
+ "Graphql": "GraphQL"
},
"language": {
"Javascript": "JavaScript",
diff --git a/diagrams/c4/__init__.py b/diagrams/c4/__init__.py
new file mode 100644
index 00000000..40577c8c
--- /dev/null
+++ b/diagrams/c4/__init__.py
@@ -0,0 +1,97 @@
+"""
+A set of nodes and edges to visualize software architecture using the C4 model.
+"""
+import html
+import textwrap
+from diagrams import Cluster, Node, Edge
+
+
+def _format_node_label(name, key, description):
+ """Create a graphviz label string for a C4 node"""
+ title = f'{html.escape(name)}
'
+ subtitle = f'[{html.escape(key)}]
' if key else ""
+ text = f'
{_format_description(description)}' if description else ""
+ return f"<{title}{subtitle}{text}>"
+
+
+def _format_description(description):
+ """
+ Formats the description string so it fits into the C4 nodes.
+
+ It line-breaks the description so it fits onto exactly three lines. If there are more
+ than three lines, all further lines are discarded and "..." inserted on the last line to
+ indicate that it was shortened. This will also html-escape the description so it can
+ safely be included in a HTML label.
+ """
+ wrapper = textwrap.TextWrapper(width=40, max_lines=3)
+ lines = [html.escape(line) for line in wrapper.wrap(description)]
+ lines += [""] * (3 - len(lines)) # fill up with empty lines so it is always three
+ return "
".join(lines)
+
+
+def _format_edge_label(description):
+ """Create a graphviz label string for a C4 edge"""
+ wrapper = textwrap.TextWrapper(width=24, max_lines=3)
+ lines = [html.escape(line) for line in wrapper.wrap(description)]
+ text = "
".join(lines)
+ return f'<{text}>'
+
+
+def C4Node(name, technology="", description="", type="Container", **kwargs):
+ key = f"{type}: {technology}" if technology else type
+ node_attributes = {
+ "label": _format_node_label(name, key, description),
+ "labelloc": "c",
+ "shape": "rect",
+ "width": "2.6",
+ "height": "1.6",
+ "fixedsize": "true",
+ "style": "filled",
+ "fillcolor": "dodgerblue3",
+ "fontcolor": "white",
+ }
+ # collapse boxes to a smaller form if they don't have a description
+ if not description:
+ node_attributes.update({"width": "2", "height": "1"})
+ node_attributes.update(kwargs)
+ return Node(**node_attributes)
+
+
+def Container(name, technology="", description="", **kwargs):
+ return C4Node(name, technology=technology, description=description, type="Container")
+
+
+def Database(name, technology="", description="", **kwargs):
+ return C4Node(name, technology=technology, description=description, type="Database", shape="cylinder", labelloc="b")
+
+
+def System(name, description="", external=False, **kwargs):
+ type = "External System" if external else "System"
+ fillcolor = "gray60" if external else "dodgerblue4"
+ return C4Node(name, description=description, type=type, fillcolor=fillcolor)
+
+
+def Person(name, description="", external=False, **kwargs):
+ type = "External Person" if external else "Person"
+ fillcolor = "gray60" if external else "dodgerblue4"
+ style = "rounded,filled"
+ return C4Node(name, description=description, type=type, fillcolor=fillcolor, style=style)
+
+
+def SystemBoundary(name, **kwargs):
+ graph_attributes = {
+ "label": html.escape(name),
+ "bgcolor": "white",
+ "margin": "16",
+ "style": "dashed",
+ }
+ graph_attributes.update(kwargs)
+ return Cluster(name, graph_attr=graph_attributes)
+
+
+def Relationship(label="", **kwargs):
+ edge_attribtues = {"style": "dashed", "color": "gray60"}
+ if label:
+ edge_attribtues.update({"label": _format_edge_label(label)})
+ edge_attribtues.update(kwargs)
+ return Edge(**edge_attribtues)
diff --git a/diagrams/programming/framework.py b/diagrams/programming/framework.py
index 64fd6397..e4ba3b68 100644
--- a/diagrams/programming/framework.py
+++ b/diagrams/programming/framework.py
@@ -36,6 +36,10 @@ class Flutter(_Framework):
_icon = "flutter.png"
+class Graphql(_Framework):
+ _icon = "graphql.png"
+
+
class Laravel(_Framework):
_icon = "laravel.png"
@@ -67,3 +71,4 @@ class Vue(_Framework):
# Aliases
FastAPI = Fastapi
+GraphQL = Graphql
diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile
index 8aed1225..8ec78b36 100644
--- a/docker/dev/Dockerfile
+++ b/docker/dev/Dockerfile
@@ -6,7 +6,7 @@ RUN apk update && apk add --no-cache \
gcc libc-dev g++ graphviz git bash go imagemagick inkscape ttf-opensans curl fontconfig xdg-utils
# install go package.
-RUN go get github.com/mingrammer/round
+RUN go install github.com/mingrammer/round@latest
# install fonts
RUN curl -O https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip \
diff --git a/docs/nodes/c4.md b/docs/nodes/c4.md
new file mode 100644
index 00000000..9c21c2c8
--- /dev/null
+++ b/docs/nodes/c4.md
@@ -0,0 +1,77 @@
+---
+id: c4
+title: C4
+---
+
+## C4 Diagrams
+
+[C4](https://c4model.com/) is a standardized model to visualize software architecture.
+You can generate C4 diagrams by using the node and edge classes from the `diagrams.c4` package:
+
+```python
+from diagrams import Diagram
+from diagrams.c4 import Person, Container, Database, System, SystemBoundary, Relationship
+
+graph_attr = {
+ "splines": "spline",
+}
+
+with Diagram("Container diagram for Internet Banking System", direction="TB", graph_attr=graph_attr):
+ customer = Person(
+ name="Personal Banking Customer", description="A customer of the bank, with personal bank accounts."
+ )
+
+ with SystemBoundary("Internet Banking System"):
+ webapp = Container(
+ name="Web Application",
+ technology="Java and Spring MVC",
+ description="Delivers the static content and the Internet banking single page application.",
+ )
+
+ spa = Container(
+ name="Single-Page Application",
+ technology="Javascript and Angular",
+ description="Provides all of the Internet banking functionality to customers via their web browser.",
+ )
+
+ mobileapp = Container(
+ name="Mobile App",
+ technology="Xamarin",
+ description="Provides a limited subset of the Internet banking functionality to customers via their mobile device.",
+ )
+
+ api = Container(
+ name="API Application",
+ technology="Java and Spring MVC",
+ description="Provides Internet banking functionality via a JSON/HTTPS API.",
+ )
+
+ database = Database(
+ name="Database",
+ technology="Oracle Database Schema",
+ description="Stores user registration information, hashed authentication credentials, access logs, etc.",
+ )
+
+ email = System(name="E-mail System", description="The internal Microsoft Exchange e-mail system.", external=True)
+
+ mainframe = System(
+ name="Mainframe Banking System",
+ description="Stores all of the core banking information about customers, accounts, transactions, etc.",
+ external=True,
+ )
+
+ customer >> Relationship("Visits bigbank.com/ib using [HTTPS]") >> webapp
+ customer >> Relationship("Views account balances, and makes payments using") >> [spa, mobileapp]
+ webapp >> Relationship("Delivers to the customer's web browser") >> spa
+ spa >> Relationship("Make API calls to [JSON/HTTPS]") >> api
+ mobileapp >> Relationship("Make API calls to [JSON/HTTPS]") >> api
+
+ api >> Relationship("reads from and writes to") >> database
+ api >> Relationship("Sends email using [SMTP]") >> email
+ api >> Relationship("Makes API calls to [XML/HTTPS]") >> mainframe
+ customer << Relationship("Sends e-mails to") << email
+```
+
+It will produce the following diagram:
+
+
diff --git a/docs/nodes/onprem.md b/docs/nodes/onprem.md
index 3c9528e2..03c6aff7 100644
--- a/docs/nodes/onprem.md
+++ b/docs/nodes/onprem.md
@@ -161,8 +161,8 @@ Node classes list of onprem provider.
- **diagrams.onprem.monitoring.Dynatrace**
- **diagrams.onprem.monitoring.Grafana**
- **diagrams.onprem.monitoring.Humio**
-- **diagrams.onprem.monitoring.Newrelic**
- **diagrams.onprem.monitoring.Nagios**
+- **diagrams.onprem.monitoring.Newrelic**
- **diagrams.onprem.monitoring.PrometheusOperator**
- **diagrams.onprem.monitoring.Prometheus**
- **diagrams.onprem.monitoring.Sentry**
diff --git a/docs/nodes/programming.md b/docs/nodes/programming.md
index 3bfb9076..df813899 100644
--- a/docs/nodes/programming.md
+++ b/docs/nodes/programming.md
@@ -41,6 +41,7 @@ Node classes list of programming provider.
- **diagrams.programming.framework.Fastapi**, **FastAPI** (alias)
- **diagrams.programming.framework.Flask**
- **diagrams.programming.framework.Flutter**
+- **diagrams.programming.framework.Graphql**, **GraphQL** (alias)
- **diagrams.programming.framework.Laravel**
- **diagrams.programming.framework.Micronaut**
- **diagrams.programming.framework.Rails**
diff --git a/resources/programming/framework/graphql.png b/resources/programming/framework/graphql.png
new file mode 100644
index 00000000..fe9e52d5
Binary files /dev/null and b/resources/programming/framework/graphql.png differ
diff --git a/tests/test_c4.py b/tests/test_c4.py
new file mode 100644
index 00000000..25c85455
--- /dev/null
+++ b/tests/test_c4.py
@@ -0,0 +1,64 @@
+import os
+import random
+import string
+import unittest
+
+from diagrams import Diagram
+from diagrams import setcluster, setdiagram
+from diagrams.c4 import Person, Container, Database, System, SystemBoundary, Relationship
+
+
+class C4Test(unittest.TestCase):
+ def setUp(self):
+ self.name = "diagram-" + "".join([random.choice(string.hexdigits) for n in range(7)])
+
+ def tearDown(self):
+ setdiagram(None)
+ setcluster(None)
+ try:
+ os.remove(self.name + ".png")
+ except FileNotFoundError:
+ pass
+
+ def test_nodes(self):
+ with Diagram(name=self.name, show=False):
+ person = Person("person", "A person.")
+ container = Container("container", "Java application", "The application.")
+ database = Database("database", "Oracle database", "Stores information.")
+
+ def test_external_nodes(self):
+ with Diagram(name=self.name, show=False):
+ external_person = Person("person", external=True)
+ external_system = System("external", external=True)
+
+ def test_systems(self):
+ with Diagram(name=self.name, show=False):
+ system = System("system", "The internal system.")
+ system_without_description = System("unknown")
+
+ def test_edges(self):
+ with Diagram(name=self.name, show=False):
+ c1 = Container("container1")
+ c2 = Container("container2")
+
+ c1 >> c2
+
+ def test_edges_with_labels(self):
+ with Diagram(name=self.name, show=False):
+ c1 = Container("container1")
+ c2 = Container("container2")
+
+ c1 >> Relationship("depends on") >> c2
+ c1 << Relationship("is depended on by") << c2
+
+ def test_edge_without_constraint(self):
+ with Diagram(name=self.name, show=False):
+ s1 = System("system 1")
+ s2 = System("system 2")
+
+ s1 >> Relationship(constraint="False") >> s2
+
+ def test_cluster(self):
+ with Diagram(name=self.name, show=False):
+ with SystemBoundary("System"):
+ Container("container", "type", "description")
diff --git a/website/static/img/c4.png b/website/static/img/c4.png
new file mode 100644
index 00000000..e3ea5cc0
Binary files /dev/null and b/website/static/img/c4.png differ