diff --git a/README.md b/README.md
index 0633255..f958ab2 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ Diagrams lets you draw the cloud system architecture **in Python code**. It was
![generic provider](https://img.shields.io/badge/Generic-orange?color=5f87bf)
![programming provider](https://img.shields.io/badge/Programming-orange?color=5f87bf)
![saas provider](https://img.shields.io/badge/SaaS-orange?color=5f87bf)
+![c4 provider](https://img.shields.io/badge/C4-orange?color=5f87bf)
## Getting Started
diff --git a/diagrams/c4/__init__.py b/diagrams/c4/__init__.py
new file mode 100644
index 0000000..40577c8
--- /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/docs/nodes/c4.md b/docs/nodes/c4.md
new file mode 100644
index 0000000..9c21c2c
--- /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:
+
+![c4](/img/c4.png)
diff --git a/tests/test_c4.py b/tests/test_c4.py
new file mode 100644
index 0000000..25c8545
--- /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 0000000..e3ea5cc
Binary files /dev/null and b/website/static/img/c4.png differ